openapi: 3.1.0

info:
  title: Sport Analytics API
  version: 1.0.0
  description: |
    Version-controlled contract for the handwritten Sport Analytics backend API.

    The supported business API major version is `v1`; application-data paths are
    served beneath `/api/v1`. Public documentation resources such as `/openapi.yaml`
    sit outside that versioned business surface. URI paths are the only
    version-selection mechanism for the business API.
    Unsupported major-version paths return `404` with
    `UNSUPPORTED_API_VERSION`; request headers do not negotiate another API version.

    Operations marked with `x-implementation-status: planned` document agreed
    contracts that are not yet implemented. Implemented operations describe
    current backend behaviour.
x-ai-declaration: >-
  The preceding OpenAPI document was planned, generated, reviewed and edited
  with the assistance of ChatGPT-Web[GPT-5.6 Sol] and Codex[GPT-5].
x-api-versioning:
  versioning: uri-major
  supportedVersions: [v1]
  basePath: /api/v1
  responseHeader: API-Version
  unsupportedVersion:
    status: 404
    code: UNSUPPORTED_API_VERSION
servers:
  - url: https://statsthegame-dev-api.calmground-aa50efe2.southafricanorth.azurecontainerapps.io
    description: Deployed development API
  - url: http://127.0.0.1:3000
    description: Local development API
security: []

tags:
  - name: Documentation
    description: Public machine-readable API documentation resources.

  - name: Health
    description: Service health and availability.

  - name: Authentication and Profile
    description: Managed-authentication identity and profile-related operations.

  - name: Public Read
    description: >-
      Canonical cricket-resource reads. A missing X-API-Key uses bounded anonymous access; a valid
      key selects that consumer's minute limit, UTC daily quota and safe telemetry. Any supplied
      invalid or revoked key is rejected rather than downgraded to anonymous access.

  - name: Analytics Query
    description: >-
      Anonymous evaluation of a structured query definition against the
      statistics the platform already publishes. Evaluation computes nothing of
      its own; it resolves names and delegates to the published reads.

  - name: Dataset Releases
    description: Discoverable immutable published-data snapshots and artefact downloads.

  - name: Weather
    description: >-
      Contextual weather lookup for a location and date, backed by the
      Open-Meteo external API. See ADR-008 for the integration decision.

  - name: Submissions
    description: Approved-submitter event-submission operations.

  - name: Provenance
    description: Protected submission, event-revision and derived-statistic provenance.

  - name: Submitter Access
    description: Authenticated submitter-access request operations.

  - name: Administration
    description: Administrator-only application-account and submitter-scope operations.

  - name: Consumer API
    description: >-
      Consumer-specific operations and deprecated keyed compatibility aliases. New integrations
      use canonical Public Read paths with optional X-API-Key identification. The access and
      migration contract is documented in `docs/api/consumer-keys.md`.

paths:
  /openapi.yaml:
    get:
      tags: [Documentation]
      summary: Download the authoritative OpenAPI specification
      description: >-
        Returns the same version-controlled OpenAPI document maintained at
        docs/api/openapi.yaml. This documentation resource is public and is not
        part of the versioned /api/v1 business-data surface.
      operationId: getOpenApiSpecification
      x-implementation-status: implemented
      security: []
      responses:
        '200':
          description: Authoritative machine-readable OpenAPI 3.1 specification.
          content:
            application/yaml:
              schema:
                type: string

  /api/v1/dataset-releases:
    get:
      tags: [Dataset Releases]
      summary: List available dataset releases
      operationId: listDatasetReleases
      x-implementation-status: implemented
      security: []
      responses:
        '200':
          description: Available immutable releases, newest first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatasetReleaseCollectionResponse'

  /api/v1/dataset-releases/{version}:
    get:
      tags: [Dataset Releases]
      summary: Get dataset release metadata
      operationId: getDatasetRelease
      x-implementation-status: implemented
      security: []
      parameters:
        - $ref: '#/components/parameters/DatasetReleaseVersion'
      responses:
        '200':
          description: Release scope, creation, schema and checksum metadata.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatasetReleaseResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/dataset-releases/{version}/artifact.json:
    get:
      tags: [Dataset Releases]
      summary: Download a dataset release artefact
      operationId: downloadDatasetReleaseArtifact
      x-implementation-status: implemented
      security: []
      parameters:
        - $ref: '#/components/parameters/DatasetReleaseVersion'
      responses:
        '200':
          description: The exact canonical JSON bytes covered by the release checksum.
          headers:
            Content-Disposition:
              description: Attachment filename for the immutable release.
              schema: { type: string }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatasetReleaseArtifact'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/admin/dataset-releases:
    post:
      tags: [Administration, Dataset Releases]
      summary: Queue an immutable dataset release
      operationId: createDatasetRelease
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [version]
              properties:
                version:
                  $ref: '#/components/schemas/DatasetReleaseVersion'
      responses:
        '200':
          description: Existing immutable release metadata when the version is already published.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatasetReleaseResponse'
        '202':
          description: Durable generation job accepted or reused.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatasetReleaseJobResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '413':
          description: The JSON request body exceeds 1 MB (`PAYLOAD_TOO_LARGE`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }

  /api/v1/admin/dataset-release-jobs/{jobId}:
    get:
      tags: [Administration, Dataset Releases]
      summary: Get dataset release generation status
      operationId: getDatasetReleaseJob
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - name: jobId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Safe generation progress and completed release metadata.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatasetReleaseJobResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/admin/api-consumers:
    get:
      tags: [Administration]
      summary: List administrator-owned API consumers
      operationId: listApiConsumers
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      responses:
        '200':
          description: Safe consumer and key metadata; raw keys are never returned.
          content:
            {
              application/json:
                { schema: { $ref: '#/components/schemas/ApiConsumerListResponse' } },
            }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Administration]
      summary: Issue an external consumer API key
      description: The raw API key is returned exactly once in this response and is not stored by the service.
      operationId: issueApiConsumer
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/ApiConsumerIssue' } } }
      responses:
        '201':
          description: Consumer metadata and the one-time raw API key.
          content:
            {
              application/json:
                { schema: { $ref: '#/components/schemas/ApiConsumerIssueResponse' } },
            }
        '400': { $ref: '#/components/responses/InvalidJsonBody' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '413':
          description: The JSON request body exceeds 1 MB (`PAYLOAD_TOO_LARGE`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '422':
          description: The issue request is invalid.
          content:
            { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } }

  /api/v1/admin/api-consumers/{consumerId}/keys/rotate:
    post:
      tags: [Administration]
      summary: Rotate an API consumer key
      description: Revokes active keys before issuing the replacement. The raw replacement is returned once.
      operationId: rotateApiConsumerKey
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - name: consumerId
          in: path
          required: true
          schema: { $ref: '#/components/schemas/DatabaseIdentifier' }
      responses:
        '200':
          {
            description: Replacement key and safe metadata.,
            content:
              {
                application/json:
                  { schema: { $ref: '#/components/schemas/ApiConsumerRotateResponse' } },
              },
          }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422':
          description: The API consumer identifier is not a numeric identifier (`VALIDATION_FAILED`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }

  /api/v1/admin/api-consumers/{consumerId}/usage:
    get:
      tags: [Administration]
      summary: Retrieve an administrator-owned API consumer's aggregated usage
      description: >-
        Returns safe historical aggregates for the selected consumer without requiring or
        exposing an API key. Visibility follows the existing owner-scoped administrator consumer
        model. Results are grouped by UTC date, normalized route template and response status
        class. The default range is seven UTC dates; the maximum range is 31 dates. Results are
        ordered by date descending, endpoint ascending, then status class ascending. The response
        contains no credentials, headers, bodies, payloads or raw query-string values.
      operationId: getAdministratorApiConsumerUsage
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - name: consumerId
          in: path
          required: true
          schema: { $ref: '#/components/schemas/DatabaseIdentifier' }
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - {
            name: limit,
            in: query,
            schema: { type: integer, minimum: 1, maximum: 100, default: 50 },
          }
      responses:
        '200':
          description: Owner-scoped consumer identity, configuration and bounded usage aggregate.
          content:
            application/json:
              { schema: { $ref: '#/components/schemas/AdministratorApiConsumerUsageResponse' } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: The consumer does not exist or is not visible to this administrator.
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '422':
          description: The API consumer identifier is not a numeric identifier (`VALIDATION_FAILED`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }

  /api/v1/admin/api-consumers/{consumerId}/keys/{keyId}:
    delete:
      tags: [Administration]
      summary: Revoke an API consumer key
      operationId: revokeApiConsumerKey
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - {
            name: consumerId,
            in: path,
            required: true,
            schema: { $ref: '#/components/schemas/DatabaseIdentifier' },
          }
        - {
            name: keyId,
            in: path,
            required: true,
            schema: { $ref: '#/components/schemas/DatabaseIdentifier' },
          }
      responses:
        '204': { description: Key revoked. }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422':
          description: The API consumer or key identifier is not a numeric identifier (`VALIDATION_FAILED`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }

  /api/v1/consumer/usage:
    get:
      tags: [Consumer API]
      summary: Retrieve the authenticated consumer's aggregated API usage
      description: >-
        Returns only the API key's own consumer telemetry. Results are grouped by UTC date,
        normalized route template and response status class. The default range is seven UTC dates;
        the maximum range is 31 dates. Results are ordered by
        date descending, endpoint ascending, then status class ascending.
      operationId: getConsumerUsage
      x-implementation-status: implemented
      security: [{ apiKeyAuth: [] }]
      parameters:
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - {
            name: limit,
            in: query,
            schema: { type: integer, minimum: 1, maximum: 100, default: 50 },
          }
      responses:
        '200':
          description: Bounded own-consumer usage aggregate.
          headers:
            RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
            X-Quota-Limit: { $ref: '#/components/headers/QuotaLimit' }
            X-Quota-Remaining: { $ref: '#/components/headers/QuotaRemaining' }
            X-Quota-Reset: { $ref: '#/components/headers/QuotaReset' }
          content:
            application/json: { schema: { $ref: '#/components/schemas/ConsumerUsageResponse' } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/ApiKeyUnauthorized' }
        '429': { $ref: '#/components/responses/ConsumerLimitExceeded' }
        '503': { $ref: '#/components/responses/ConsumerRateLimitUnavailable' }

  /api/v1/consumer/competitions:
    get:
      tags: [Consumer API]
      summary: List competitions with a consumer API key
      deprecated: true
      description: >-
        Deprecated compatibility alias. Migrate to `GET /api/v1/competitions` and send the
        same `X-API-Key` there for identified-consumer limits, quota and usage accounting.
        Responses include `Deprecation: ?1` and a successor `Link`; no retirement date is scheduled.
      operationId: listConsumerCompetitions
      x-implementation-status: implemented
      security: [{ apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: name
          in: query
          description: Optional competition-name filter.
          schema:
            type: string
            minLength: 1
      responses:
        '200':
          description: Competition page.
          headers:
            RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
            X-Quota-Limit: { $ref: '#/components/headers/QuotaLimit' }
            X-Quota-Remaining: { $ref: '#/components/headers/QuotaRemaining' }
            X-Quota-Reset: { $ref: '#/components/headers/QuotaReset' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CompetitionCollectionResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/ApiKeyUnauthorized' }
        '429': { $ref: '#/components/responses/ConsumerLimitExceeded' }
        '503': { $ref: '#/components/responses/ConsumerRateLimitUnavailable' }

  /api/v1/consumer/fixtures:
    get:
      tags: [Consumer API]
      summary: List fixtures with a consumer API key
      deprecated: true
      description: >-
        Deprecated compatibility alias. Migrate to `GET /api/v1/fixtures` and send the
        same `X-API-Key` there for identified-consumer limits, quota and usage accounting.
        Responses include `Deprecation: ?1` and a successor `Link`; no retirement date is scheduled.
      operationId: listConsumerFixtures
      x-implementation-status: implemented
      security: [{ apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: competitionId
          in: query
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: seasonId
          in: query
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: competitorId
          in: query
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: gender
          in: query
          schema:
            type: string
            minLength: 1
        - name: startDateFrom
          in: query
          description: Inclusive lower fixture-date bound.
          schema:
            type: string
            format: date
        - name: startDateTo
          in: query
          description: Inclusive upper fixture-date bound.
          schema:
            type: string
            format: date
      responses:
        '200':
          description: Fixture page.
          headers:
            RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
            X-Quota-Limit: { $ref: '#/components/headers/QuotaLimit' }
            X-Quota-Remaining: { $ref: '#/components/headers/QuotaRemaining' }
            X-Quota-Reset: { $ref: '#/components/headers/QuotaReset' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FixtureCollectionResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/ApiKeyUnauthorized' }
        '429': { $ref: '#/components/responses/ConsumerLimitExceeded' }
        '503': { $ref: '#/components/responses/ConsumerRateLimitUnavailable' }

  /api/v1/consumer/fixtures/{fixtureId}:
    get:
      tags: [Consumer API]
      summary: Get a fixture with a consumer API key
      deprecated: true
      description: >-
        Deprecated compatibility alias. Migrate to `GET /api/v1/fixtures/{fixtureId}` and send the
        same `X-API-Key` there for identified-consumer limits, quota and usage accounting.
        Responses include `Deprecation: ?1` and a successor `Link`; no retirement date is scheduled.
      operationId: getConsumerFixture
      x-implementation-status: implemented
      security: [{ apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/FixtureId'
      responses:
        '200':
          description: Fixture detail.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FixtureResponse'
        '401': { $ref: '#/components/responses/ApiKeyUnauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/ConsumerLimitExceeded' }
        '503': { $ref: '#/components/responses/ConsumerRateLimitUnavailable' }

  /api/v1/consumer/fixtures/{fixtureId}/events:
    get:
      tags: [Consumer API]
      summary: List accepted fixture events with a consumer API key
      deprecated: true
      description: >-
        Deprecated compatibility alias. Migrate to `GET /api/v1/fixtures/{fixtureId}/events` and send the
        same `X-API-Key` there for identified-consumer limits, quota and usage accounting.
        Responses include `Deprecation: ?1` and a successor `Link`; no retirement date is scheduled.
      operationId: listConsumerFixtureEvents
      x-implementation-status: implemented
      security: [{ apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/FixtureId'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: inningsId
          in: query
          description: Limit events to one innings in the fixture.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: competitorId
          in: query
          description: Limit events to innings batted by this competitor.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: participantId
          in: query
          description: >-
            Limit events to those involving this participant as striker,
            non-striker, bowler, dismissed player or identified fielder.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: overNumber
          in: query
          description: Limit events to one zero-based cricket over number.
          schema:
            type: integer
            minimum: 0
            maximum: 32767
        - name: wicketKind
          in: query
          description: Limit events to deliveries containing this dismissal kind.
          schema:
            type: string
            minLength: 1
      responses:
        '200':
          description: Paginated accepted fixture events in occurrence order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicEventCollectionResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/ApiKeyUnauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/ConsumerLimitExceeded' }
        '503': { $ref: '#/components/responses/ConsumerRateLimitUnavailable' }

  /api/v1/consumer/fixtures/{fixtureId}/events/export.json:
    get:
      tags: [Consumer API]
      summary: Export accepted fixture events as JSON with a consumer API key
      deprecated: true
      description: >-
        Deprecated compatibility alias. Migrate to `GET /api/v1/fixtures/{fixtureId}/events/export.json` and send the
        same `X-API-Key` there for identified-consumer limits, quota and usage accounting.
        Responses include `Deprecation: ?1` and a successor `Link`; no retirement date is scheduled.
      operationId: exportConsumerFixtureEventsJson
      x-implementation-status: implemented
      security: [{ apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/FixtureId'
        - name: inningsId
          in: query
          description: Limit events to one innings in the fixture.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: competitorId
          in: query
          description: Limit events to innings batted by this competitor.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: participantId
          in: query
          description: Limit events to those involving this participant.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: overNumber
          in: query
          description: Limit events to one zero-based cricket over number.
          schema:
            type: integer
            minimum: 0
            maximum: 32767
        - name: wicketKind
          in: query
          description: Limit events to deliveries containing this dismissal kind.
          schema:
            type: string
            minLength: 1
      responses:
        '200':
          description: Every matching accepted event in occurrence order.
          headers:
            Deprecation: { $ref: '#/components/headers/Deprecation' }
            Link: { $ref: '#/components/headers/SuccessorLink' }
            RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
            X-Quota-Limit: { $ref: '#/components/headers/QuotaLimit' }
            X-Quota-Remaining: { $ref: '#/components/headers/QuotaRemaining' }
            X-Quota-Reset: { $ref: '#/components/headers/QuotaReset' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicEventExportResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/ApiKeyUnauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ExportTooLarge' }
        '429': { $ref: '#/components/responses/ConsumerLimitExceeded' }
        '503': { $ref: '#/components/responses/ConsumerRateLimitUnavailable' }

  /api/v1/consumer/fixtures/{fixtureId}/events/export.csv:
    get:
      tags: [Consumer API]
      summary: Export accepted fixture events as CSV with a consumer API key
      deprecated: true
      description: >-
        Deprecated compatibility alias. Migrate to `GET /api/v1/fixtures/{fixtureId}/events/export.csv` and send the
        same `X-API-Key` there for identified-consumer limits, quota and usage accounting.
        Responses include `Deprecation: ?1` and a successor `Link`; no retirement date is scheduled.
      operationId: exportConsumerFixtureEventsCsv
      x-implementation-status: implemented
      security: [{ apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/FixtureId'
        - name: inningsId
          in: query
          description: Limit events to one innings in the fixture.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: competitorId
          in: query
          description: Limit events to innings batted by this competitor.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: participantId
          in: query
          description: Limit events to those involving this participant.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: overNumber
          in: query
          description: Limit events to one zero-based cricket over number.
          schema:
            type: integer
            minimum: 0
            maximum: 32767
        - name: wicketKind
          in: query
          description: Limit events to deliveries containing this dismissal kind.
          schema:
            type: string
            minLength: 1
      responses:
        '200':
          description: A CSV attachment of every matching accepted event in occurrence order.
          headers:
            Content-Disposition:
              description: Attachment filename for the CSV export.
              schema:
                type: string
          content:
            text/csv:
              schema:
                type: string
                format: binary
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/ApiKeyUnauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ExportTooLarge' }
        '429': { $ref: '#/components/responses/ConsumerLimitExceeded' }
        '503': { $ref: '#/components/responses/ConsumerRateLimitUnavailable' }

  /api/v1/consumer/fixtures/{fixtureId}/events/{eventId}:
    get:
      tags: [Consumer API]
      summary: Get an accepted fixture event with a consumer API key
      deprecated: true
      description: >-
        Deprecated compatibility alias. Migrate to `GET /api/v1/fixtures/{fixtureId}/events/{eventId}` and send the
        same `X-API-Key` there for identified-consumer limits, quota and usage accounting.
        Responses include `Deprecation: ?1` and a successor `Link`; no retirement date is scheduled.
      operationId: getConsumerFixtureEvent
      x-implementation-status: implemented
      security: [{ apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/FixtureId'
        - $ref: '#/components/parameters/EventId'
      responses:
        '200':
          description: One current accepted fixture event.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicEventResponse'
        '401': { $ref: '#/components/responses/ApiKeyUnauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/ConsumerLimitExceeded' }
        '503': { $ref: '#/components/responses/ConsumerRateLimitUnavailable' }

  /api/v1/consumer/fixtures/{fixtureId}/statistics:
    get:
      tags: [Consumer API]
      summary: Get fixture statistics with a consumer API key
      deprecated: true
      description: >-
        Deprecated compatibility alias. Migrate to `GET /api/v1/fixtures/{fixtureId}/statistics` and send the
        same `X-API-Key` there for identified-consumer limits, quota and usage accounting.
        Responses include `Deprecation: ?1` and a successor `Link`; no retirement date is scheduled.
      operationId: getConsumerFixtureStatistics
      x-implementation-status: implemented
      security: [{ apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/FixtureId'
        - $ref: '#/components/parameters/IncludeStatisticContributors'
      responses:
        '200':
          description: Derived fixture statistics and completeness state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FixtureStatisticsResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/ApiKeyUnauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/ConsumerLimitExceeded' }
        '503': { $ref: '#/components/responses/ConsumerRateLimitUnavailable' }

  /api/v1/consumer/fixtures/{fixtureId}/statistics/{statisticId}:
    get:
      tags: [Consumer API]
      summary: Get one fixture statistic with a consumer API key
      deprecated: true
      description: >-
        Deprecated compatibility alias. Migrate to `GET /api/v1/fixtures/{fixtureId}/statistics/{statisticId}` and send the
        same `X-API-Key` there for identified-consumer limits, quota and usage accounting.
        Responses include `Deprecation: ?1` and a successor `Link`; no retirement date is scheduled.
      operationId: getConsumerFixtureStatistic
      x-implementation-status: implemented
      security: [{ apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/FixtureId'
        - $ref: '#/components/parameters/StatisticId'
        - $ref: '#/components/parameters/IncludeStatisticContributors'
      responses:
        '200':
          description: One derived fixture statistic resource.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FixtureStatisticResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/ApiKeyUnauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/ConsumerLimitExceeded' }
        '503': { $ref: '#/components/responses/ConsumerRateLimitUnavailable' }

  /api/v1/consumer/fixtures/{fixtureId}/statistics/{statisticId}/events/export.json:
    get:
      tags: [Consumer API]
      summary: Export a fixture statistic trace as JSON with a consumer API key
      deprecated: true
      description: >-
        Deprecated compatibility alias. Migrate to `GET /api/v1/fixtures/{fixtureId}/statistics/{statisticId}/events/export.json` and send the
        same `X-API-Key` there for identified-consumer limits, quota and usage accounting.
        Responses include `Deprecation: ?1` and a successor `Link`; no retirement date is scheduled.
      operationId: exportConsumerFixtureStatisticEventsJson
      x-implementation-status: implemented
      security: [{ apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/FixtureId'
        - $ref: '#/components/parameters/StatisticId'
      responses:
        '200':
          description: The statistic's contributing accepted events in occurrence order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicEventExportResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/ApiKeyUnauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/ExportTraceChanged' }
        '422': { $ref: '#/components/responses/ExportTooLarge' }
        '429': { $ref: '#/components/responses/ConsumerLimitExceeded' }
        '503': { $ref: '#/components/responses/ConsumerRateLimitUnavailable' }

  /api/v1/consumer/fixtures/{fixtureId}/statistics/{statisticId}/events/export.csv:
    get:
      tags: [Consumer API]
      summary: Export a fixture statistic trace as CSV with a consumer API key
      deprecated: true
      description: >-
        Deprecated compatibility alias. Migrate to `GET /api/v1/fixtures/{fixtureId}/statistics/{statisticId}/events/export.csv` and send the
        same `X-API-Key` there for identified-consumer limits, quota and usage accounting.
        Responses include `Deprecation: ?1` and a successor `Link`; no retirement date is scheduled.
      operationId: exportConsumerFixtureStatisticEventsCsv
      x-implementation-status: implemented
      security: [{ apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/FixtureId'
        - $ref: '#/components/parameters/StatisticId'
      responses:
        '200':
          description: A CSV attachment of the statistic's contributing accepted events.
          headers:
            Content-Disposition:
              description: Attachment filename for the CSV export.
              schema:
                type: string
          content:
            text/csv:
              schema:
                type: string
                format: binary
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/ApiKeyUnauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/ExportTraceChanged' }
        '422': { $ref: '#/components/responses/ExportTooLarge' }
        '429': { $ref: '#/components/responses/ConsumerLimitExceeded' }
        '503': { $ref: '#/components/responses/ConsumerRateLimitUnavailable' }

  /api/v1/consumer/participants/{participantId}/statistics:
    get:
      tags: [Consumer API]
      summary: Get participant aggregates with a consumer API key
      deprecated: true
      description: >-
        Deprecated compatibility alias. Migrate to `GET /api/v1/participants/{participantId}/statistics` and send the
        same `X-API-Key` there for identified-consumer limits, quota and usage accounting.
        Responses include `Deprecation: ?1` and a successor `Link`; no retirement date is scheduled.
      operationId: getConsumerParticipantStatistics
      x-implementation-status: implemented
      security: [{ apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/ParticipantId'
        - $ref: '#/components/parameters/ParticipantAggregateScope'
      responses:
        '200':
          description: Participant season, competition, and career aggregates.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParticipantAggregatesResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/ApiKeyUnauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/ConsumerLimitExceeded' }
        '503': { $ref: '#/components/responses/ConsumerRateLimitUnavailable' }

  /api/v1/consumer/participants/{participantId}/statistics/{statisticId}:
    get:
      tags: [Consumer API]
      summary: Get one participant aggregate with a consumer API key
      deprecated: true
      description: >-
        Deprecated compatibility alias. Migrate to `GET /api/v1/participants/{participantId}/statistics/{statisticId}` and send the
        same `X-API-Key` there for identified-consumer limits, quota and usage accounting.
        Responses include `Deprecation: ?1` and a successor `Link`; no retirement date is scheduled.
      operationId: getConsumerParticipantStatistic
      x-implementation-status: implemented
      security: [{ apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/ParticipantId'
        - $ref: '#/components/parameters/StatisticId'
      responses:
        '200':
          description: One derived participant aggregate resource.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParticipantAggregateResponse'
        '401': { $ref: '#/components/responses/ApiKeyUnauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/ConsumerLimitExceeded' }
        '503': { $ref: '#/components/responses/ConsumerRateLimitUnavailable' }
  /api/v1/health:
    get:
      tags:
        - Health
      summary: Check API health
      operationId: getHealth
      x-implementation-status: implemented
      security: []
      responses:
        '200':
          description: API is running.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
              example:
                status: ok
                service: sport-analytics-api
                timestamp: '2026-08-09T14:00:00.000Z'

  /api/v1/auth/me:
    get:
      tags:
        - Authentication and Profile
      summary: Return the current application user profile
      description: |
        Verifies the bearer token through Supabase Auth, creates or synchronizes
        the provider-neutral application account, and returns server-owned role,
        submitter-approval and competition-scope state.

        Authentication never grants or changes approval, role or scope.
      operationId: getCurrentUserProfile
      x-implementation-status: implemented
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Current synchronized application user profile.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CurrentUserProfileResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /api/v1/account:
    delete:
      tags:
        - Authentication and Profile
      summary: Permanently delete the authenticated account
      description: |
        Requires an exact `DELETE` confirmation and a Supabase sign-in no more
        than 15 minutes old. With server-only Supabase administration configured,
        the backend immediately disables the local account, removes its
        authorisation grants, hard-deletes its Supabase Auth user, and tombstones
        local identity fields.

        Cricket submissions, fixtures, deliveries, statistics and their stable
        provenance link remain available without the former display name or
        reusable authentication subject. If the server-only key is absent, the
        operation fails before changing local state.
      operationId: deleteCurrentAccount
      x-implementation-status: implemented
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AccountDeletionRequest'
      responses:
        '200':
          description: Account identity deleted and local session may now be cleared.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountDeletionResponse'
        '400': { $ref: '#/components/responses/InvalidJsonBody' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: The identity was not authenticated recently enough.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '413':
          description: The JSON request body exceeds 1 MB (`PAYLOAD_TOO_LARGE`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '422':
          description: The exact deletion confirmation was not supplied.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '501':
          description: Server-only Supabase Auth administration is not configured.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '503':
          description: |
            Deletion could not be completed. The local account remains disabled
            and the operation can be retried safely.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'

  /api/v1/submitter-access-requests:
    post:
      tags:
        - Submitter Access
      summary: Request submitter access
      description: |
        Creates a competition-scoped submitter-access request for the
        authenticated application account by validating and storing the
        requested competition while transitioning an eligible server-owned
        approval state to `pending`.

        Accounts already in `pending` or `approved` state cannot create another
        active request. A previously rejected account may request access again.
      operationId: requestSubmitterAccess
      x-implementation-status: implemented
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubmitterAccessRequest'
      responses:
        '201':
          description: Submitter access request created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitterAccessRequestResponse'
        '400': { $ref: '#/components/responses/InvalidJsonBody' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: The authenticated account is disabled.
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '409':
          description: The account already has a pending request or is already approved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '413':
          description: The JSON request body exceeds 1 MB (`PAYLOAD_TOO_LARGE`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '422':
          description: The request body or requested competition is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'

  /api/v1/submitter-scope-requests:
    post:
      tags:
        - Submitter Access
      summary: Request an additional competition scope
      description: |
        Creates one pending additional competition-scope request for an already-approved
        submitter. The request does not modify the submitter's authoritative competition
        grants. Existing scopes remain effective until an administrator approves the request
        through the existing submitter-access management workflow.

        A submitter cannot request a competition they already hold and cannot create a second
        additional-scope request while one is awaiting administrator review.
      operationId: requestAdditionalSubmitterScope
      x-implementation-status: implemented
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubmitterAccessRequest'
      responses:
        '201':
          description: Additional competition scope request created without granting access.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitterScopeRequestResponse'
        '400': { $ref: '#/components/responses/InvalidJsonBody' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: The authenticated account is disabled.
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '409':
          description: The account is not an approved submitter, the scope is already granted, or another scope request is pending.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '413':
          description: The JSON request body exceeds 1 MB (`PAYLOAD_TOO_LARGE`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '422':
          description: The request body or requested competition is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'

  /api/v1/admin/users:
    get:
      tags:
        - Administration
      summary: List registered users and competition scope choices
      description: |
        Returns every registered application account with its authoritative role,
        safe email address, compatibility approval state, requested competition, current named
        competition scopes, account state and latest submitter-access audit.
        Only an `admin` may call this operation.
      operationId: listAdministratorUsers
      x-implementation-status: implemented
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Registered users and valid competition scopes.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdministratorUserManagementResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '503':
          description: The server-only identity-provider email lookup is unavailable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'

  /api/v1/admin/users/{userId}/role:
    patch:
      tags:
        - Administration
      summary: Promote a user to administrator
      description: |
        Promotes an active non-administrator account to the authoritative `admin` role.
        Viewer and submitter transitions remain governed by the submitter-access lifecycle
        endpoints, so approval state and scoped grants cannot be bypassed. The acting
        administrator cannot change their own role and existing administrators cannot be
        demoted through this operation.
      operationId: updateAdministratorUserRole
      x-implementation-status: implemented
      security:
        - bearerAuth: []
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/DatabaseIdentifier'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdministratorRoleUpdate'
      responses:
        '200':
          description: Updated administrator account.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdministratorSubmitterAccessResponse'
        '400': { $ref: '#/components/responses/InvalidJsonBody' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: The role transition is protected or must use the submitter-access lifecycle.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '413':
          description: The JSON request body exceeds 1 MB (`PAYLOAD_TOO_LARGE`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '422':
          description: The user identifier or requested role is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '503':
          description: The server-only identity-provider email lookup is unavailable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'

  /api/v1/admin/users/{userId}/submitter-access:
    patch:
      tags:
        - Administration
      summary: Approve, re-scope, or revoke a submitter
      description: |
        Atomically approves a pending viewer request, re-scopes an approved
        submitter, or revokes approved submitter access. Approval requires the
        single competition stored on the pending request. Scope replacement for
        an existing submitter requires at least one existing competition. When an approved
        submitter has a pending additional-scope request, an approval must include the requested
        competition and the pending request is cleared only by the server-side decision.
        Revocation requires an empty scope and removes every existing grant while
        retaining the request's historical `approved` decision state.

        A viewer in `not_requested` or `rejected` cannot be approved directly
        and must create a new request before an administrator can act.

        Rejecting a pending request is a separate operation at
        `POST /api/v1/admin/users/{userId}/submitter-access/rejection`.

        Administrator accounts, disabled accounts and the acting administrator's
        own account cannot be changed through this operation.
      operationId: updateAdministratorSubmitterAccess
      x-implementation-status: implemented
      security:
        - bearerAuth: []
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/DatabaseIdentifier'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdministratorSubmitterAccessUpdate'
      responses:
        '200':
          description: Effective submitter access after the atomic update.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdministratorSubmitterAccessResponse'
        '400': { $ref: '#/components/responses/InvalidJsonBody' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: The target application account does not exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: The requested transition is invalid, or the target is the acting administrator, another administrator, or disabled. Invalid lifecycle transitions use `INVALID_SUBMITTER_ACCESS_TRANSITION`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '413':
          description: The JSON request body exceeds 1 MB (`PAYLOAD_TOO_LARGE`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '422':
          description: The update shape or one of the requested competition scopes is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'

  /api/v1/admin/users/{userId}/submitter-access/rejection:
    post:
      tags:
        - Administration
      summary: Reject a pending submitter-access or additional-scope request
      description: |
        Rejects a genuine pending viewer request or an approved submitter's pending additional
        competition-scope request. Initial-request rejection keeps the authoritative role as
        `viewer` and changes the request state to `rejected`. Additional-scope rejection keeps
        the existing `submitter` role and every current competition grant unchanged. Both paths
        clear the pending requested competition and record the acting administrator and decision.

        This operation is distinct from revocation, which removes access from an existing
        approved submitter through the PATCH operation.
      operationId: rejectAdministratorSubmitterAccessRequest
      x-implementation-status: implemented
      security:
        - bearerAuth: []
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/DatabaseIdentifier'
      responses:
        '200':
          description: Updated viewer account with a rejected request and no competition scopes.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdministratorSubmitterAccessResponse'
        '400': { $ref: '#/components/responses/InvalidJsonBody' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: The target application account does not exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: The target is not a pending viewer, is the acting administrator, is another administrator, or is disabled. Invalid lifecycle transitions use `INVALID_SUBMITTER_ACCESS_TRANSITION`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '413':
          description: The JSON request body exceeds 1 MB (`PAYLOAD_TOO_LARGE`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '422':
          description: The target user identifier is malformed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'

  /api/v1/competitions:
    get:
      tags:
        - Public Read
      summary: List competitions
      operationId: listCompetitions
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: name
          in: query
          description: Optional competition-name filter.
          schema:
            type: string
            minLength: 1
      responses:
        '200':
          description: Paginated competitions.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompetitionCollectionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'

  /api/v1/competitions/{competitionId}:
    get:
      tags:
        - Public Read
      summary: Get a competition
      operationId: getCompetition
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/CompetitionId'
      responses:
        '200':
          description: Competition detail.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompetitionResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/seasons:
    get:
      tags:
        - Public Read
      summary: List seasons
      operationId: listSeasons
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: competitionId
          in: query
          description: Limit results to one competition.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: name
          in: query
          description: Match a season label or competition name.
          schema:
            type: string
            minLength: 1
      responses:
        '200':
          description: Paginated seasons.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SeasonCollectionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'

  /api/v1/seasons/{seasonId}:
    get:
      tags:
        - Public Read
      summary: Get a season
      operationId: getSeason
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/SeasonId'
      responses:
        '200':
          description: Season detail.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SeasonResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/fixtures:
    get:
      tags:
        - Public Read
      summary: List fixtures
      operationId: listFixtures
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'

        - name: competitionId
          in: query
          schema:
            $ref: '#/components/schemas/ApiIdentifier'

        - name: seasonId
          in: query
          schema:
            $ref: '#/components/schemas/ApiIdentifier'

        - name: competitorId
          in: query
          schema:
            $ref: '#/components/schemas/ApiIdentifier'

        - name: gender
          in: query
          schema:
            type: string
            minLength: 1

        - name: startDateFrom
          in: query
          description: Inclusive lower fixture-date bound.
          schema:
            type: string
            format: date

        - name: startDateTo
          in: query
          description: Inclusive upper fixture-date bound.
          schema:
            type: string
            format: date

      responses:
        '200':
          description: Paginated fixtures.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FixtureCollectionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'

  /api/v1/fixtures/{fixtureId}:
    get:
      tags:
        - Public Read
      summary: Get a fixture
      operationId: getFixture
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/FixtureId'
      responses:
        '200':
          description: Fixture detail.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FixtureResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/fixtures/{fixtureId}/events:
    get:
      tags:
        - Public Read
      summary: List accepted fixture events
      description: |
        Returns the current accepted revisions of cricket delivery events in
        deterministic innings and sequence order. Submission ownership,
        revision history and internal audit fields are not exposed.
      operationId: listFixtureEvents
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/FixtureId'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: inningsId
          in: query
          description: Limit events to one innings in the fixture.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: competitorId
          in: query
          description: Limit events to innings batted by this competitor.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: participantId
          in: query
          description: |
            Limit events to those involving this participant as striker,
            non-striker, bowler, dismissed player or identified fielder.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: overNumber
          in: query
          description: Limit events to one zero-based cricket over number.
          schema:
            type: integer
            minimum: 0
            maximum: 32767
        - name: wicketKind
          in: query
          description: Limit events to deliveries containing this dismissal kind.
          schema:
            type: string
            minLength: 1
      responses:
        '200':
          description: Paginated accepted fixture events in occurrence order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicEventCollectionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/fixtures/{fixtureId}/events/{eventId}:
    get:
      tags:
        - Public Read
      summary: Get an accepted fixture event
      operationId: getFixtureEvent
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/FixtureId'
        - $ref: '#/components/parameters/EventId'
      responses:
        '200':
          description: One current accepted fixture event.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicEventResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/fixtures/{fixtureId}/events/export.json:
    get:
      tags:
        - Public Read
      summary: Export accepted fixture events as JSON
      description: |
        Exports every current accepted fixture event matching the filters, in
        deterministic innings and sequence order. It accepts the same filters as
        the public event collection except for cursor and limit: the server
        follows the collection's cursor at the maximum page size until it is
        exhausted. An export that would exceed 5,000 events fails as a whole
        with EXPORT_TOO_LARGE, and a failure on any page returns an error rather
        than the rows read so far, so a partial export is never returned.
        Records pair competition, team and participant identifiers with readable
        names and represent absent extras categories as zero. Submission
        ownership, revision history and internal audit fields are not exposed.

      operationId: exportFixtureEventsJson
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/FixtureId'
        - name: inningsId
          in: query
          description: Limit events to one innings in the fixture.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: competitorId
          in: query
          description: Limit events to innings batted by this competitor.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: participantId
          in: query
          description: Limit events to those involving this participant.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: overNumber
          in: query
          description: Limit events to one zero-based cricket over number.
          schema:
            type: integer
            minimum: 0
            maximum: 32767
        - name: wicketKind
          in: query
          description: Limit events to deliveries containing this dismissal kind.
          schema:
            type: string
            minLength: 1
      responses:
        '200':
          description: Every matching accepted event in occurrence order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicEventExportResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ExportTooLarge'

  /api/v1/fixtures/{fixtureId}/events/export.csv:
    get:
      tags:
        - Public Read
      summary: Export accepted fixture events as CSV
      description: |
        Exports the same complete filtered set as `export.json` in CSV form.
        Rows use the same deterministic occurrence order and the response is an
        attachment whose filename identifies every active trace filter, for
        example `fixture-481-player-30-over-4-events.csv`.
      operationId: exportFixtureEventsCsv
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/FixtureId'
        - name: inningsId
          in: query
          description: Limit events to one innings in the fixture.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: competitorId
          in: query
          description: Limit events to innings batted by this competitor.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: participantId
          in: query
          description: Limit events to those involving this participant.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: overNumber
          in: query
          description: Limit events to one zero-based cricket over number.
          schema:
            type: integer
            minimum: 0
            maximum: 32767
        - name: wicketKind
          in: query
          description: Limit events to deliveries containing this dismissal kind.
          schema:
            type: string
            minLength: 1
      responses:
        '200':
          description: A CSV attachment of every matching accepted event in occurrence order.
          headers:
            Content-Disposition:
              description: Attachment filename for the CSV export.
              schema:
                type: string
          content:
            text/csv:
              schema:
                type: string
                format: binary
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ExportTooLarge'

  /api/v1/fixtures/{fixtureId}/statistics/{statisticId}/events/export.json:
    get:
      tags:
        - Public Read
      summary: Export a calculation trace's accepted events as JSON
      description: |
        Exports exactly the accepted events a fixture statistic was derived
        from, which are the events its calculation trace displays, in
        occurrence order. For a participant statistic these are the
        standard-innings deliveries the participant faced or bowled; unlike the
        participantId filter, they exclude deliveries where the participant was
        only the non-striker, was dismissed or fielded, and super-over
        deliveries. Rows are read in full with the same paging and bound as the
        filtered export. If the accepted events change between reading the trace
        and its rows, the export fails with EXPORT_TRACE_CHANGED rather than
        returning a file that disagrees with the trace.
      operationId: exportFixtureStatisticEventsJson
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/FixtureId'
        - $ref: '#/components/parameters/StatisticId'
      responses:
        '200':
          description: The statistic's contributing accepted events in occurrence order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicEventExportResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/ExportTraceChanged'
        '422':
          $ref: '#/components/responses/ExportTooLarge'

  /api/v1/fixtures/{fixtureId}/statistics/{statisticId}/events/export.csv:
    get:
      tags:
        - Public Read
      summary: Export a calculation trace's accepted events as CSV
      description: |
        Exports the same events as the calculation-trace JSON export in the
        fixture-event CSV format. The attachment is named after the trace, for
        example `fixture-481-innings-900-team-20-events.csv` or
        `fixture-481-player-30-events.csv`.
      operationId: exportFixtureStatisticEventsCsv
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/FixtureId'
        - $ref: '#/components/parameters/StatisticId'
      responses:
        '200':
          description: A CSV attachment of the statistic's contributing accepted events.
          headers:
            Content-Disposition:
              description: Attachment filename for the CSV export.
              schema:
                type: string
          content:
            text/csv:
              schema:
                type: string
                format: binary
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/ExportTraceChanged'
        '422':
          $ref: '#/components/responses/ExportTooLarge'

  /api/v1/fixtures/{fixtureId}/statistics:
    get:
      tags:
        - Public Read
      summary: Get derived fixture statistics
      description: |
        Derives standard fixture, innings and participant statistics from the
        latest accepted revision of each ordered delivery event. Super-over
        innings are excluded from this Basic standard scope.
      operationId: getFixtureStatistics
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/FixtureId'
        - $ref: '#/components/parameters/IncludeStatisticContributors'
      responses:
        '200':
          description: Derived fixture statistics and completeness state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FixtureStatisticsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/fixtures/{fixtureId}/statistics/{statisticId}:
    get:
      tags:
        - Public Read
      summary: Get one derived fixture statistic resource
      description: |
        Resolves a stable statistic identifier returned by the fixture
        statistics endpoint. Contributing accepted event records are opt-in.
      operationId: getFixtureStatistic
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/FixtureId'
        - $ref: '#/components/parameters/StatisticId'
        - $ref: '#/components/parameters/IncludeStatisticContributors'
      responses:
        '200':
          description: One derived fixture statistic resource.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FixtureStatisticResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/weather:
    get:
      tags:
        - Weather
      summary: Get weather for a location and date
      description: |
        Retrieves observed daily weather (temperature range, precipitation and
        maximum wind speed) for a given latitude, longitude and date from the
        Open-Meteo external API. Intended to provide contextual weather for a
        fixture; the caller currently supplies coordinates directly rather
        than a fixture identifier. The backend selects Open-Meteo's forecast
        or archive endpoint based on the requested date; dates outside both
        endpoints' supported ranges return `422 DATE_UNSUPPORTED`.
      operationId: getWeather
      x-implementation-status: implemented
      security: []
      parameters:
        - $ref: '#/components/parameters/WeatherLatitude'
        - $ref: '#/components/parameters/WeatherLongitude'
        - $ref: '#/components/parameters/WeatherDate'
      responses:
        '200':
          description: Observed daily weather for the requested location and date.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WeatherResponse'
              example:
                data:
                  date: '2026-08-19'
                  latitude: -26.2041
                  longitude: 28.0473
                  temperatureMax: 23.4
                  temperatureMin: 10.2
                  precipitationSum: 0
                  windSpeedMax: 18.5
        '400':
          $ref: '#/components/responses/BadRequest'
        '422':
          $ref: '#/components/responses/UnsupportedDate'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
        '504':
          $ref: '#/components/responses/UpstreamTimeout'

  /api/v1/fixtures/{fixtureId}/weather:
    get:
      tags:
        - Weather
      summary: Get contextual weather for a fixture
      description: |
        Resolves the fixture's start date and venue coordinates, reusing stored
        coordinates or resolving the canonical venue and city first and raw venue
        name second when coordinates are absent. Resolved coordinates are persisted before daily
        weather is retrieved through the server-side
        Open-Meteo integration; a changed venue name or city invalidates them.
        Weather is contextual external information and is not authoritative
        fixture, event, or statistic data. If the fixture exists but cannot be
        located after those lookups, or its date falls outside Open-Meteo's
        supported forecast and archive ranges, a successful unavailable
        response is returned and Open-Meteo is not called.
      operationId: getFixtureWeather
      x-implementation-status: implemented
      security: []
      parameters:
        - $ref: '#/components/parameters/FixtureId'
      responses:
        '200':
          description: Contextual fixture weather, or an explicit unavailable state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FixtureWeatherResponse'
              examples:
                available:
                  value:
                    data:
                      fixtureId: '17'
                      date: '2026-08-19'
                      availability: available
                      venue:
                        name: Wits Cricket Oval
                        city: Johannesburg
                      weather:
                        date: '2026-08-19'
                        latitude: -26.1929
                        longitude: 28.0305
                        temperatureMax: 24
                        temperatureMin: 11
                        precipitationSum: 0
                        windSpeedMax: 17
                unavailable:
                  value:
                    data:
                      fixtureId: '17'
                      date: '2026-08-19'
                      availability: unavailable
                      reason: LOCATION_NOT_FOUND
                      venue:
                        name: Wits Cricket Oval
                        city: Johannesburg
                      weather: null
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
        '504':
          $ref: '#/components/responses/UpstreamTimeout'

  /api/v1/competitors:
    get:
      tags:
        - Public Read
      summary: List competitors
      operationId: listCompetitors
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'

        - name: competitionId
          in: query
          schema:
            $ref: '#/components/schemas/ApiIdentifier'

        - name: seasonId
          in: query
          schema:
            $ref: '#/components/schemas/ApiIdentifier'

        - name: name
          in: query
          schema:
            type: string
            minLength: 1

      responses:
        '200':
          description: Paginated competitors.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompetitorCollectionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'

  /api/v1/competitors/{competitorId}:
    get:
      tags:
        - Public Read
      summary: Get a competitor
      operationId: getCompetitor
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/CompetitorId'
      responses:
        '200':
          description: Competitor detail.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompetitorResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/participants:
    get:
      tags:
        - Public Read
      summary: List participants
      operationId: listParticipants
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'

        - name: fixtureId
          in: query
          schema:
            $ref: '#/components/schemas/ApiIdentifier'

        - name: competitorId
          in: query
          schema:
            $ref: '#/components/schemas/ApiIdentifier'

        - name: name
          in: query
          schema:
            type: string
            minLength: 1

      responses:
        '200':
          description: Paginated participants.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParticipantCollectionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'

  /api/v1/participants/{participantId}:
    get:
      tags:
        - Public Read
      summary: Get a participant
      operationId: getParticipant
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/ParticipantId'
      responses:
        '200':
          description: Participant detail.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParticipantResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/participants/{participantId}/fixtures:
    get:
      tags:
        - Public Read
      summary: List a participant's published fixture history and performance
      description: |
        Returns fixtures in which the participant was selected, newest first,
        with readable competition and team context plus the participant's
        available fixture-level batting and bowling figures. A selected player
        remains in the history when they did not bat or bowl. Partial accepted
        source data and fixtures without published delivery statistics are
        represented through statisticsStatus, statisticsWarnings and null
        batting or bowling values. Submission, account and audit data are not
        exposed.
      operationId: listParticipantFixtures
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/ParticipantId'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Paginated published fixture history for the participant.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParticipantFixtureCollectionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/participants/{participantId}/statistics:
    get:
      tags:
        - Public Read
      summary: Get a participant's season, competition and career aggregates
      description: |
        Derives a participant's batting and bowling figures rolled up to three
        levels from the latest accepted revision of each delivery event: by
        competition and season, by competition alone, and across their whole
        career. Grouping is by participant identifier, never by display name.
        Super-over innings are excluded from every level, so a career figure
        equals the sum of the participant's published fixture figures. Rates are
        recomputed over each group rather than averaged across fixtures, and are
        null where their denominator is zero or where the fixtures in a group do
        not share one balls-per-over divisor. Bowling figures separately expose
        bowler-attributable wide and no-ball runs. Byes and leg-byes run off a
        wide are wide runs charged to the bowler (Law 22.6); other byes,
        leg-byes and innings penalty runs remain team extras.
      operationId: getParticipantAggregates
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/ParticipantId'
        - $ref: '#/components/parameters/ParticipantAggregateScope'
      responses:
        '200':
          description: Derived participant aggregates and completeness state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParticipantAggregatesResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/participants/{participantId}/statistics/{statisticId}:
    get:
      tags:
        - Public Read
      summary: Get one derived participant aggregate resource
      description: |
        Resolves a stable statistic identifier returned by the participant
        aggregates endpoint, at whichever level produced it.
      operationId: getParticipantAggregate
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/ParticipantId'
        - $ref: '#/components/parameters/StatisticId'
      responses:
        '200':
          description: One derived participant aggregate resource.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParticipantAggregateResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/statistics/leaderboards:
    get:
      tags:
        - Public Read
      summary: Get a bounded season or competition participant leaderboard
      description: |
        Ranks accepted-current standard-innings participant aggregates with one
        set-based server-side query. `scope=season` requires `seasonId`;
        `scope=competition` requires `competitionId`. Total and batting rate
        metrics sort descending; bowling rate metrics sort ascending. Equal
        metric values are ordered by participant name using database C
        collation, then stable numeric participant ID. Rate qualifications and
        their rationale are returned in the response. Results are limited to at
        most 50 entries and expose no submission, account, review or audit data.
      operationId: getStatisticsLeaderboard
      x-implementation-status: implemented
      security: [{}, { apiKeyAuth: [] }]
      parameters:
        - name: scope
          in: query
          required: true
          schema:
            type: string
            enum: [season, competition]
        - name: seasonId
          in: query
          description: Required only for season scope; the opaque season resource identifier.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: competitionId
          in: query
          description: Required only for competition scope.
          schema:
            $ref: '#/components/schemas/ApiIdentifier'
        - name: metric
          in: query
          required: true
          schema:
            $ref: '#/components/schemas/LeaderboardMetric'
        - name: limit
          in: query
          description: Bounded top-N result size.
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 10
      responses:
        '200':
          description: The requested authoritative leaderboard and qualification metadata.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LeaderboardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/query-definitions/evaluate:
    post:
      tags:
        - Analytics Query
      summary: Evaluate a query definition
      description: |
        Answers a structured query definition from the statistics the platform
        already publishes.

        Evaluation calculates nothing. Each name hint is resolved to an
        identifier through the ordinary parameterised reads, the service that
        already answers that question is called, and the published resource is
        returned unchanged alongside the endpoint that returns it and the
        statistic identifiers inside it that answer the question.

        The operation requires no authentication: it returns only resources the
        public statistics reads already serve anonymously.

        Every evaluated definition is answered with `200`, including one whose
        reference resolves to nothing or to more than one entity, and one the
        published statistics cannot answer. Those are outcomes rather than
        failures. Only a body that fails the query-definition contract is an
        error, and that is `422`.
      operationId: evaluateQueryDefinition
      x-implementation-status: implemented
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnalyticsQueryDefinition'
      responses:
        '200':
          description: The definition was evaluated. The outcome names the result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QueryDefinitionEvaluationResponse'
        '400': { $ref: '#/components/responses/InvalidJsonBody' }
        '413':
          description: The JSON request body exceeds 1 MB (`PAYLOAD_TOO_LARGE`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '422':
          description: >-
            The body does not satisfy the query-definition contract
            (`VALIDATION_FAILED`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '503': { $ref: '#/components/responses/DatabaseStatementTimeout' }

  /api/v1/natural-language-queries:
    post:
      tags:
        - Analytics Query
      summary: Ask a question in natural language
      description: |
        Answers a reader's question from the statistics the platform already
        publishes.

        The question is translated into a query definition by a server-side
        language-model adapter, and that definition is then evaluated exactly as
        `POST /api/v1/query-definitions/evaluate` evaluates one. Nothing new is
        calculated: a figure returned here is the figure the corresponding public
        endpoint returns. This is not the analyst-defined statistic engine; it
        admits no expression and stores no definition.

        Model output can never become data. The adapter validates what the model
        returns against the query-definition contract, so output that does not
        satisfy it is reported as `422 QUERY_NOT_UNDERSTOOD` rather than answered.

        The operation requires no authentication, because it returns only
        resources the public statistics reads already serve anonymously. What
        stands in place of a credential is a durable limiter: a per-client rate
        limit and daily quota keyed on a salted hash of the caller's address, a
        global daily cap across all callers, and the 300-character bound on the
        question. Every limit counts attempts rather than successes, so a failing
        request cannot be used to bypass the budget.
      operationId: askNaturalLanguageQuery
      x-implementation-status: implemented
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NaturalLanguageQuery'
      responses:
        '200':
          description: >-
            The question was translated and evaluated. The evaluation outcome
            names the result, including a question the published statistics
            cannot answer.
          headers:
            RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
            X-Quota-Limit:
              description: Daily question quota for this client.
              schema: { type: integer, minimum: 0 }
            X-Quota-Remaining:
              description: Questions remaining for this client today.
              schema: { type: integer, minimum: 0 }
            X-Quota-Reset:
              description: Seconds until the daily quota resets.
              schema: { type: integer, minimum: 0 }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NaturalLanguageQueryResponse'
        '400': { $ref: '#/components/responses/InvalidJsonBody' }
        '413':
          description: The JSON request body exceeds 1 MB (`PAYLOAD_TOO_LARGE`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '422':
          description: >-
            The question is missing, empty, or longer than 300 characters
            (`VALIDATION_FAILED`), or the model returned something the
            query-definition contract rejects (`QUERY_NOT_UNDERSTOOD`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '429': { $ref: '#/components/responses/NaturalLanguageQueryLimitExceeded' }
        '503':
          description: >-
            The language-model provider is unconfigured, unreachable or too slow
            (`QUERY_SERVICE_UNAVAILABLE`), the limiter could not be read so the
            request failed closed (`RATE_LIMIT_UNAVAILABLE`), or a database
            statement exceeded its bound (`DATABASE_STATEMENT_TIMEOUT`). All three
            are temporary and the request may be retried.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'

  /api/v1/provenance/submissions:
    get:
      tags: [Provenance]
      summary: List protected submission and batch provenance
      description: Submitters see their own sources; administrators see only their assigned competition review scope.
      operationId: listSubmissionProvenance
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: kind
          in: query
          schema: { type: string, enum: [direct, file, batch] }
      responses:
        '200':
          description: Cursor-paginated private provenance records.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProvenanceSubmissionListResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/provenance/submissions/{reference}:
    get:
      tags: [Provenance]
      summary: Inspect one direct, file or batch submission provenance record
      operationId: getSubmissionProvenance
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - name: reference
          in: path
          required: true
          description: Numeric direct/file submission ID or UUID batch reference.
          schema: { type: string, minLength: 1 }
      responses:
        '200':
          description: Source metadata, lifecycle and acceptance/publication decisions.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProvenanceSubmissionDetailResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/provenance/events/{eventId}:
    get:
      tags: [Provenance]
      summary: Trace an event through its current and superseded revisions
      operationId: getEventProvenance
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - name: eventId
          in: path
          required: true
          description: Internal delivery identifier from a statistic contributor trace.
          schema: { $ref: '#/components/schemas/DatabaseIdentifier' }
      responses:
        '200':
          description: Revision lineage with retained source and decision provenance.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EventProvenanceResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/provenance/fixtures/{fixtureId}/statistics/{statisticId}:
    get:
      tags: [Provenance]
      summary: Trace a derived statistic to its current contributing event revisions
      operationId: getStatisticProvenance
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - name: fixtureId
          in: path
          required: true
          schema: { $ref: '#/components/schemas/DatabaseIdentifier' }
        - $ref: '#/components/parameters/StatisticId'
      responses:
        '200':
          description: Current statistic contributors with submission, batch and decision provenance.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatisticProvenanceResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/provenance/participants/{participantId}/statistics/{statisticId}:
    get:
      tags: [Provenance]
      summary: Trace a published participant aggregate to current source events
      operationId: getParticipantAggregateProvenance
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - name: participantId
          in: path
          required: true
          schema: { $ref: '#/components/schemas/DatabaseIdentifier' }
        - $ref: '#/components/parameters/StatisticId'
        - {
            name: limit,
            in: query,
            schema: { type: integer, minimum: 1, maximum: 100, default: 50 },
          }
        - { name: cursor, in: query, schema: { type: string, minLength: 1 } }
      responses:
        '200':
          description: Cursor-paginated aggregate contributors with stable event identities and source provenance.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatisticProvenanceResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/admin/batches:
    get:
      tags: [Batches]
      summary: List global batch history for administrators
      operationId: listAdminBatches
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: cursor, in: query, schema: { type: string, minLength: 1 } }
        - {
            name: limit,
            in: query,
            schema: { type: integer, minimum: 1, maximum: 100, default: 50 },
          }
        - {
            name: status,
            in: query,
            schema:
              {
                type: string,
                enum:
                  [
                    received,
                    stored,
                    validating,
                    rejected,
                    awaiting_review,
                    correction_requested,
                    publishing,
                    published,
                    partially_published,
                    failed,
                    superseded,
                  ],
              },
          }
      responses:
        '200':
          description: Global batch history across all submitters, optionally filtered by lifecycle state.
          content:
            application/json: { schema: { $ref: '#/components/schemas/BatchListResponse' } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/batches:
    get:
      tags: [Batches]
      summary: List batches visible to the authenticated submitter or reviewer
      operationId: listBatches
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: cursor, in: query, schema: { type: string, minLength: 1 } }
        - {
            name: limit,
            in: query,
            schema: { type: integer, minimum: 1, maximum: 100, default: 50 },
          }
        - {
            name: status,
            in: query,
            schema:
              {
                type: string,
                enum:
                  [
                    received,
                    stored,
                    validating,
                    rejected,
                    awaiting_review,
                    correction_requested,
                    publishing,
                    published,
                    partially_published,
                    failed,
                    superseded,
                  ],
              },
          }
      responses:
        '200':
          description: Submitters receive only their own batches; administrator reviewers receive their authorised repository scope.
          content:
            application/json: { schema: { $ref: '#/components/schemas/BatchListResponse' } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Batches]
      summary: Stream a whole-season package into durable private staging
      operationId: createBatchReceipt
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - {
            name: Idempotency-Key,
            in: header,
            required: true,
            schema: { type: string, minLength: 1, maxLength: 255 },
          }
        - {
            name: X-Competition-Id,
            in: header,
            required: true,
            schema: { $ref: '#/components/schemas/DatabaseIdentifier' },
          }
        - {
            name: X-Batch-Package-Version,
            in: header,
            required: true,
            description: '`1.1` permits fixture proposals for genuinely new fixtures.',
            schema: { type: string, enum: ['1.0', '1.1'] },
          }
        - {
            name: X-File-Name,
            in: header,
            required: true,
            schema: { type: string, minLength: 1, maxLength: 255 },
          }
        - name: X-Replaces-Batch-Reference
          in: header
          required: false
          description: Opaque reference of the correction-requested batch replaced by this upload.
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json: { schema: { type: string, format: binary } }
          text/csv: { schema: { type: string, format: binary } }
          application/x-ndjson: { schema: { type: string, format: binary } }
      responses:
        '202':
          description: Private source bytes were durably staged; no event is published.
          headers:
            Location:
              description: Status URL of the staged batch.
              required: true
              schema: { type: string, pattern: '^/api/v1/batches/' }
            RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json: { schema: { $ref: '#/components/schemas/BatchReceiptResponse' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409':
          description: >-
            Active-batch concurrency limit reached; idempotency conflict; or invalid, unauthorised,
            stale, or competing replacement relationship.
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '413':
          {
            description: Payload exceeds 50 MB.,
            content:
              { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } },
          }
        '422':
          {
            description: Unsupported format or invalid receipt metadata.,
            content:
              { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } },
          }
        '429':
          description: The account exceeded 6 batch uploads in 60 seconds (`RATE_LIMIT_EXCEEDED`).
          headers:
            RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
            Retry-After: { $ref: '#/components/headers/RetryAfter' }
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '503':
          {
            description: Private object storage is unavailable.,
            content:
              { application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } } },
          }

  /api/v1/batches/{batchReference}:
    get:
      tags: [Batches]
      summary: Retrieve asynchronous batch validation status and progress
      operationId: getBatchStatus
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: batchReference, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          {
            description: Current durable batch lifecycle state and validation progress.,
            content:
              {
                application/json: { schema: { $ref: '#/components/schemas/BatchStatusResponse' } },
              },
          }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/batches/{batchReference}/report:
    get:
      tags: [Batches]
      summary: Retrieve a paginated item-level batch result report
      operationId: getBatchReport
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: batchReference, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: cursor, in: query, schema: { type: string, minLength: 1 } }
        - {
            name: limit,
            in: query,
            schema: { type: integer, minimum: 1, maximum: 100, default: 50 },
          }
      responses:
        '200':
          description: Summary, grouped rules, source traceability and all faults for this report page.
          content:
            application/json: { schema: { $ref: '#/components/schemas/BatchReportResponse' } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/batches/{batchReference}/report/download:
    get:
      tags: [Batches]
      summary: Download the complete machine-readable batch report
      operationId: downloadBatchReport
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: batchReference, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: Complete JSON report returned as a downloadable attachment.
          headers:
            Content-Disposition:
              schema: { type: string }
          content:
            application/json:
              { schema: { $ref: '#/components/schemas/BatchReportDownloadResponse' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/batches/{batchReference}/review:
    post:
      tags: [Batches]
      summary: Decide and, on approval, publish a validated staged batch
      description: Competition-scoped administrators may approve, reject, or return a batch for correction. Approval records the decision and moves the batch to the resumable publishing state before accepted events are published. Ordinary rejected items remain unpublished and do not block the accepted subset; batch-level or accepted-item validation errors, conflicts, unresolved, invalid, or ambiguous references prevent approval. Rejection and return do not alter public event data.
      operationId: reviewBatch
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: batchReference, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BatchReviewRequest' }
      responses:
        '200':
          description: Persisted decision and current publication lifecycle state.
          content:
            application/json: { schema: { $ref: '#/components/schemas/BatchStatusResponse' } }
        '400': { $ref: '#/components/responses/InvalidJsonBody' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: Approval is unsafe, another decision won the race, or publication is leased.
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '413':
          description: The JSON request body exceeds 16 KB (`PAYLOAD_TOO_LARGE`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '422':
          description: The decision or required reason is invalid.
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }

  /api/v1/batches/{batchReference}/conflicts/resolve:
    post:
      tags: [Batches]
      summary: Resolve one published-delivery conflict
      description: An administrator explicitly keeps the existing published delivery or converts the staged delivery into an immutable correction target. The decision is audited and never silently overwrites published cricket data.
      operationId: resolveBatchPublishedConflict
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: batchReference, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BatchConflictResolutionRequest' }
      responses:
        '200':
          description: Conflict resolved and current batch lifecycle returned.
          content:
            application/json: { schema: { $ref: '#/components/schemas/BatchStatusResponse' } }
        '400': { $ref: '#/components/responses/InvalidJsonBody' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: The conflict is stale, ambiguous, already resolved, or cannot be corrected safely.
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '413':
          description: The JSON request body exceeds 16 KB (`PAYLOAD_TOO_LARGE`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '422':
          description: The conflict resolution request is invalid.
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }

  /api/v1/batches/{batchReference}/reference-mappings:
    post:
      tags: [Batches]
      summary: Map an ambiguous staged reference to a labeled candidate
      description: The owning submitter or a competition-scoped administrator may select an opaque candidate returned by the batch report. The decision is retained and asynchronous validation restarts from the original private source; canonical database identifiers are never accepted from the client.
      operationId: mapBatchReference
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: batchReference, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BatchReferenceMappingRequest' }
      responses:
        '200':
          description: An identical previously applied decision was returned idempotently.
          content:
            application/json:
              { schema: { $ref: '#/components/schemas/BatchReferenceMappingResponse' } }
        '202':
          description: The durable decision was accepted and asynchronous revalidation was queued.
          content:
            application/json:
              { schema: { $ref: '#/components/schemas/BatchReferenceMappingResponse' } }
        '400': { $ref: '#/components/responses/InvalidJsonBody' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: The candidate is stale, unavailable, or conflicts with a prior decision.
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '413':
          description: The JSON request body exceeds 16 KB (`PAYLOAD_TOO_LARGE`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '422':
          description: The mapping request is invalid.
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }

  /api/v1/batches/{batchReference}/participants:
    post:
      tags: [Batches]
      summary: Settle outstanding participant onboarding tasks for a batch
      description: >-
        An administrator answers the participant onboarding tasks a reviewer-created canonical
        fixture could not settle deterministically. Each decision names the task it answers by
        `taskReference` and gives exactly one answer: `personId` picks a candidate the task
        offered, `sourceId` supplies a durable registry identifier, and `teamName` names which of
        the fixture's two teams the participant belongs to. A participant is never matched on a
        name, and no identifier outside the fixture's scope is accepted.

        The whole array is applied in one transaction and the batch is revalidated once, so a
        season-scale batch does not pay for one full revalidation per decision. If any decision is
        invalid nothing is applied. Replay is safe: a decision against an already settled task is
        a no-op, and when nothing changes no revalidation is queued.
      operationId: createBatchParticipantOnboardingDecisions
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: batchReference, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BatchParticipantOnboardingRequest' }
      responses:
        '202':
          description: >-
            The decisions were applied. `revalidationQueued` is false when every decision was
            already settled, because then there is nothing to revalidate.
          content:
            application/json:
              { schema: { $ref: '#/components/schemas/BatchParticipantOnboardingResponse' } }
        '400': { $ref: '#/components/responses/InvalidJsonBody' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403':
          description: The account is not an administrator, or the batch is not visible to it.
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: >-
            A decision could not be applied: an unknown task, a candidate the task did not offer,
            an identifier naming no person, or a team that is not one of the fixture's two
            (`BATCH_PARTICIPANT_ONBOARDING_CONFLICT`). Nothing in the array was applied.
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '413':
          description: The JSON request body exceeds 16 KB (`PAYLOAD_TOO_LARGE`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '422':
          description: The decision array is invalid (`VALIDATION_FAILED`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }

  /api/v1/batches/{batchReference}/canonical-fixtures:
    post:
      tags: [Batches]
      summary: Create a canonical fixture from a staged fixture proposal
      description: >-
        An administrator may create the canonical fixture for an unresolved fixture reference that
        was submitted with a complete fixture proposal in a version `1.1` batch package: a source
        identifier, date, season and two teams. The backend records the decision and queues normal
        reference resolution and validation; it does not publish any staged delivery. Teams,
        participants and seasons must already be canonical records. A proposal whose `outcome` is
        `won` must also name the winning team in `winner`, and it must be one of the proposal's own
        two teams.
      operationId: createBatchCanonicalFixture
      x-implementation-status: implemented
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: batchReference, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BatchCanonicalFixtureRequest' }
      responses:
        '202':
          description: >-
            The canonical fixture decision was recorded. `status` is `queued` while revalidation is
            pending, or `applied` when the stored decision has already been applied.
          content:
            application/json:
              { schema: { $ref: '#/components/schemas/BatchReferenceMappingResponse' } }
        '400': { $ref: '#/components/responses/InvalidJsonBody' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403':
          description: The account is not an administrator, or the batch is not visible to it.
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: >-
            The item does not carry a complete version 1.1 fixture proposal, the proposed winner is
            not one of the fixture's two teams, or the decision conflicts with a prior decision
            (`BATCH_CANONICAL_FIXTURE_CONFLICT`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '413':
          description: The JSON request body exceeds 16 KB (`PAYLOAD_TOO_LARGE`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '422':
          description: The fixture decision is invalid (`VALIDATION_FAILED`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }

  /api/v1/submissions/events/{eventId}:
    put:
      tags:
        - Submissions
      summary: Correct an accepted fixture event
      description: Supersedes a live direct-submission delivery with a revalidated revision; derived statistics use the live revision. Submitters require the fixture competition scope, while administrators may correct any eligible fixture without a scope assignment.
      operationId: correctSubmissionEvent
      x-implementation-status: implemented
      security:
        - bearerAuth: []
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CorrectionRequest'
      responses:
        '200':
          description: The event was atomically superseded by a corrected revision.
          headers:
            RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CorrectionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: The correction conflicts with a concurrently accepted delivery revision.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '413':
          description: The JSON request body exceeds 1 MB (`PAYLOAD_TOO_LARGE`).
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }
        '422':
          description: The correction failed contract, reference, or source-event validation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '429':
          description: The submission-capable account exceeded 30 requests in 60 seconds (`RATE_LIMIT_EXCEEDED`).
          headers:
            RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
            Retry-After: { $ref: '#/components/headers/RetryAfter' }
          content:
            application/json: { schema: { $ref: '#/components/schemas/ApiErrorResponse' } }

  /api/v1/submissions/events/{eventId}/history:
    get:
      tags:
        - Submissions
      summary: Retrieve immutable correction history for an accepted event
      description: Returns ordered before/after revisions, requester and reason, source provenance, explicit predecessor/replacement links, and review provenance where review applies. Submitters require the fixture competition scope; administrators may inspect any eligible fixture.
      operationId: getSubmissionEventCorrectionHistory
      x-implementation-status: implemented
      security:
        - bearerAuth: []
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Ordered immutable correction history.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CorrectionHistoryResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          description: The event identifier is invalid or does not identify an accepted event.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'

  /api/v1/submissions/uploads:
    post:
      tags:
        - Submissions
      summary: Upload fixture event data from a JSON or CSV file
      description: Accepts one JSON or CSV file no larger than 1 MB. The file is normalised into the same SubmissionRequest contract and follows the existing authorisation, validation, and atomic storage path. Submitters require the fixture competition scope, while administrators may upload for any eligible fixture without a scope assignment.
      operationId: createSubmissionUpload
      x-implementation-status: implemented
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/SubmissionUploadRequest'
      responses:
        '201':
          description: Uploaded events were accepted and source-file provenance was stored.
          headers:
            RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmissionResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: An event ID, innings sequence, or delivery position conflicts with accepted data.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '413':
          description: The uploaded file exceeds 1 MB.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '422':
          description: The file type, CSV structure, normalised submission, or domain references are invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '429':
          description: The submission-capable account exceeded 30 requests in 60 seconds.
          headers:
            RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
            Retry-After: { $ref: '#/components/headers/RetryAfter' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'

  /api/v1/submissions:
    post:
      tags:
        - Submissions
      summary: Submit fixture event data
      description: |
        Accept an ordered set of cricket delivery events. The bearer identity
        must have the `submitter` or `admin` application role. An ordinary submitter requires the
        fixture competition in that account's server-owned scope; an administrator may submit for
        any eligible competition without a scope assignment. Validation and storage are synchronous
        and atomic; clients submit events, never statistic totals.
      operationId: createSubmission
      x-implementation-status: implemented
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubmissionRequest'
      responses:
        '201':
          description: Submission and all ordered events were accepted and stored.
          headers:
            RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmissionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: An event ID, innings sequence, or delivery position conflicts with accepted data.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '413':
          description: The JSON payload exceeds 1 MB.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '422':
          description: Submission failed contract or domain validation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '429':
          description: The submission-capable account exceeded 30 requests in 60 seconds.
          headers:
            RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
            Retry-After: { $ref: '#/components/headers/RetryAfter' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        Supabase-issued access token for authenticated application endpoints.
        Enter the raw access token in an interactive OpenAPI client's authorization
        dialog; the client adds the `Bearer` scheme to the request automatically.
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: >-
        External consumer API key sent in the `X-API-Key` request header.
        Real API keys must not be stored in this specification or its examples.
  parameters:
    DatasetReleaseVersion:
      name: version
      in: path
      required: true
      description: Stable caller-selected release identifier.
      schema:
        $ref: '#/components/schemas/DatasetReleaseVersion'

    Limit:
      name: limit
      in: query
      description: Maximum records to return. Default 50; maximum 100.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50

    Cursor:
      name: cursor
      in: query
      description: Opaque cursor returned by the previous collection response.
      schema:
        type: string
        minLength: 1

    CompetitionId:
      name: competitionId
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/ApiIdentifier'

    SeasonId:
      name: seasonId
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/ApiIdentifier'

    FixtureId:
      name: fixtureId
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/ApiIdentifier'

    EventId:
      name: eventId
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/ApiIdentifier'

    WeatherLatitude:
      name: latitude
      in: query
      required: true
      description: Latitude in decimal degrees, between -90 and 90.
      schema:
        type: number
        minimum: -90
        maximum: 90

    WeatherLongitude:
      name: longitude
      in: query
      required: true
      description: Longitude in decimal degrees, between -180 and 180.
      schema:
        type: number
        minimum: -180
        maximum: 180

    WeatherDate:
      name: date
      in: query
      required: true
      description: Date in YYYY-MM-DD format.
      schema:
        type: string
        pattern: '^\d{4}-\d{2}-\d{2}$'

    StatisticId:
      name: statisticId
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/ApiIdentifier'

    IncludeStatisticContributors:
      name: includeContributors
      in: query
      description: Include the ordered accepted delivery records that contributed to the result.
      schema:
        type: boolean
        default: false

    CompetitorId:
      name: competitorId
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/ApiIdentifier'

    ParticipantId:
      name: participantId
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/ApiIdentifier'

    ParticipantAggregateScope:
      name: scope
      in: query
      description: Restrict the response to one aggregate level. All three are returned by default.
      schema:
        type: string
        enum:
          - season
          - competition
          - career

  headers:
    Deprecation:
      description: RFC 9745 Structured Field indicating a compatibility alias is deprecated.
      required: true
      schema: { type: string, example: '?1' }
    SuccessorLink:
      description: RFC 8288 successor-version link to the canonical resource path.
      required: true
      schema:
        type: string
        example: '</api/v1/fixtures/100/events/export.json>; rel="successor-version"'
    RateLimitLimit:
      description: Requests permitted in the current 60-second rate-limit window.
      required: true
      schema: { type: integer, minimum: 0 }
    RateLimitRemaining:
      description: Requests remaining in the current rate-limit window.
      required: true
      schema: { type: integer, minimum: 0 }
    RateLimitReset:
      description: Seconds until the current rate-limit window ends.
      required: true
      schema: { type: integer, minimum: 1 }
    RetryAfter:
      description: Seconds to wait before retrying, until the current rate-limit window ends.
      required: true
      schema: { type: integer, minimum: 1 }
    QuotaLimit:
      description: Requests this API consumer may make per UTC day.
      required: true
      schema: { type: integer, minimum: 0 }
    QuotaRemaining:
      description: Requests remaining for this API consumer in the current UTC day.
      required: true
      schema: { type: integer, minimum: 0 }
    QuotaReset:
      description: Seconds until the daily quota resets at UTC midnight.
      required: true
      schema: { type: integer, minimum: 0 }

  responses:
    BadRequest:
      description: Request validation failed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'

    InvalidJsonBody:
      description: The request body is not valid JSON (`INVALID_JSON`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'

    Unauthorized:
      description: A valid authentication token is required.
      headers:
        WWW-Authenticate:
          description: Bearer authentication challenge.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'

    Forbidden:
      description: The authenticated account is not permitted to perform this operation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'

    NotFound:
      description: The requested resource was not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'

    ExportTooLarge:
      description: >-
        The export would exceed the synchronous bound of 5,000 events. The whole export fails with
        EXPORT_TOO_LARGE so that a partial file is never returned; narrow it with a filter.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'

    ExportTraceChanged:
      description: >-
        The accepted events changed between reading the calculation trace and reading its rows,
        so the export would not match the trace. It fails with EXPORT_TRACE_CHANGED; reload the
        trace and export again.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'

    UnsupportedDate:
      description: >-
        The requested date is validly formatted but falls outside both the Open-Meteo forecast
        and archive endpoints' supported ranges.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'

    UpstreamError:
      description: The Open-Meteo external API returned an error response.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'

    UpstreamTimeout:
      description: The Open-Meteo external API did not respond within the configured timeout.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'

    ServiceUnavailable:
      description: An unclassified failure occurred while contacting the external weather provider.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'

    ApiKeyUnauthorized:
      description: A missing, invalid or revoked API key was supplied.
      headers:
        WWW-Authenticate:
          schema: { type: string, example: ApiKey }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiErrorResponse' }

    NaturalLanguageQueryLimitExceeded:
      description: >-
        A natural-language query limit was reached: the per-client rate limit
        (`RATE_LIMIT_EXCEEDED`), the per-client daily quota (`QUOTA_EXCEEDED`), or
        the global daily cap across all callers (`GLOBAL_DAILY_LIMIT_REACHED`).

        The per-minute limit carries the `RateLimit-*` headers and `Retry-After`.
        The daily quota carries the `RateLimit-*` and `X-Quota-*` headers. The
        global cap carries `Retry-After` alone, because the limit it reports is not
        the caller's own allowance and reporting it as such would say a caller is
        exhausted when it is not.
      headers:
        RateLimit-Limit:
          description: >-
            Requests permitted per minute for this client. Sent for the per-minute
            limit and the daily quota, not for the global daily cap.
          schema: { type: integer, minimum: 0 }
        RateLimit-Remaining:
          description: >-
            Requests remaining in the current minute. Sent for the per-minute
            limit and the daily quota, not for the global daily cap.
          schema: { type: integer, minimum: 0 }
        RateLimit-Reset:
          description: >-
            Seconds until the current minute window ends. Sent for the per-minute
            limit and the daily quota, not for the global daily cap.
          schema: { type: integer, minimum: 1 }
        X-Quota-Limit:
          description: Daily question quota. Sent only when the daily quota is exhausted.
          schema: { type: integer, minimum: 0 }
        X-Quota-Remaining:
          description: Questions remaining today. Sent only when the daily quota is exhausted.
          schema: { type: integer, minimum: 0 }
        X-Quota-Reset:
          description: Seconds until the daily quota resets. Sent only when the daily quota is exhausted.
          schema: { type: integer, minimum: 0 }
        Retry-After:
          description: >-
            Seconds to wait before retrying. Sent for the per-minute limit and for
            the global daily cap, not for the per-client daily quota.
          schema: { type: integer, minimum: 1 }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiErrorResponse' }

    ConsumerLimitExceeded:
      description: >-
        The consumer per-minute rate limit (`RATE_LIMIT_EXCEEDED`) or daily quota
        (`QUOTA_EXCEEDED`) was exhausted. Both carry the `RateLimit-*` headers. The per-minute
        limit also sends `Retry-After` but not `X-Quota-*`; the daily quota sends `X-Quota-*` but
        not `Retry-After`.
      headers:
        RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
        RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
        RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
        X-Quota-Limit:
          description: Daily request quota. Sent only when the daily quota is exhausted.
          schema: { type: integer, minimum: 0 }
        X-Quota-Remaining:
          description: Requests remaining today. Sent only when the daily quota is exhausted.
          schema: { type: integer, minimum: 0 }
        X-Quota-Reset:
          description: Seconds until the daily quota resets. Sent only when the daily quota is exhausted.
          schema: { type: integer, minimum: 0 }
        Retry-After:
          description: Seconds to wait before retrying. Sent only when the per-minute limit is exhausted.
          schema: { type: integer, minimum: 1 }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiErrorResponse' }

    ConsumerRateLimitUnavailable:
      description: >-
        The shared consumer rate-limit store is unavailable. The request is not
        admitted and consumers should retry with bounded backoff.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiErrorResponse' }

    DatabaseStatementTimeout:
      description: >-
        A database statement exceeded the configured execution bound and was
        cancelled, so the request produced no result. The error code is
        `DATABASE_STATEMENT_TIMEOUT`. The condition is temporary and the same
        request may succeed on retry with bounded backoff. Any database-backed
        operation can end this way; it is documented on the operations whose
        reads are heavy enough for the bound to be a realistic outcome.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiErrorResponse' }

  schemas:
    DatasetReleaseVersion:
      type: string
      pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$'

    DatasetReleaseField:
      type: object
      additionalProperties: false
      required: [name, description]
      properties:
        name: { type: string, minLength: 1 }
        description: { type: string, minLength: 1 }

    DatasetRelease:
      type: object
      additionalProperties: false
      required:
        [
          releaseId,
          version,
          createdAt,
          snapshotId,
          snapshotAsOf,
          formatVersion,
          scope,
          eventCount,
          checksum,
          fields,
        ]
      properties:
        releaseId:
          type: string
          format: uuid
        version:
          $ref: '#/components/schemas/DatasetReleaseVersion'
        createdAt: { type: string, format: date-time }
        snapshotId:
          oneOf: [{ type: string, format: uuid }, { type: 'null' }]
          description: Opaque identity of the durable source snapshot; null only for pre-snapshot legacy releases.
        snapshotAsOf:
          oneOf: [{ type: string, format: date-time }, { type: 'null' }]
          description: Database transaction time at which the durable source snapshot was captured.
        formatVersion: { type: string, enum: ['1.0', '1.1'] }
        scope: { type: string, const: published-accepted-deliveries }
        eventCount: { type: integer, minimum: 0 }
        checksum:
          type: string
          pattern: '^[a-f0-9]{64}$'
          description: SHA-256 digest of the exact downloadable artefact bytes.
        fields:
          type: array
          items:
            $ref: '#/components/schemas/DatasetReleaseField'

    DatasetReleaseResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/DatasetRelease'

    DatasetReleaseCollectionResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/DatasetRelease'

    DatasetReleaseJob:
      type: object
      additionalProperties: false
      required:
        [
          jobId,
          version,
          status,
          eventsProcessed,
          bytesWritten,
          pageNumber,
          createdAt,
          startedAt,
          completedAt,
          failureCode,
          failureMessage,
          release,
        ]
      properties:
        jobId: { type: string, format: uuid }
        version: { $ref: '#/components/schemas/DatasetReleaseVersion' }
        status: { type: string, enum: [pending, generating, completed, failed] }
        eventsProcessed: { type: integer, minimum: 0 }
        bytesWritten: { type: integer, minimum: 0 }
        pageNumber: { type: integer, minimum: 0 }
        createdAt: { type: string, format: date-time }
        startedAt: { oneOf: [{ type: string, format: date-time }, { type: 'null' }] }
        completedAt: { oneOf: [{ type: string, format: date-time }, { type: 'null' }] }
        failureCode: { oneOf: [{ type: string, minLength: 1 }, { type: 'null' }] }
        failureMessage: { oneOf: [{ type: string, minLength: 1 }, { type: 'null' }] }
        release: { oneOf: [{ $ref: '#/components/schemas/DatasetRelease' }, { type: 'null' }] }

    DatasetReleaseJobResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/DatasetReleaseJob' }

    DatasetReleaseArtifact:
      type: object
      additionalProperties: false
      required: [formatVersion, scope, fields, events]
      properties:
        formatVersion: { type: string, const: '1.1' }
        scope: { type: string, const: published-accepted-deliveries }
        fields:
          type: array
          items:
            $ref: '#/components/schemas/DatasetReleaseField'
        events:
          type: array
          items:
            type: object
            additionalProperties: true

    ApiConsumerIssue:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        rateLimitPerMinute: { type: integer, minimum: 1, maximum: 10000, default: 60 }
        dailyQuota: { type: integer, minimum: 1, maximum: 10000, default: 10000 }
    ConsumerUsageResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [from, to, totalRequests, quota, entries]
          properties:
            from: { type: string, format: date }
            to: { type: string, format: date }
            totalRequests: { type: integer, minimum: 0 }
            quota:
              type: object
              additionalProperties: false
              required: [limit, used, remaining]
              properties:
                limit: { type: integer, minimum: 1 }
                used: { type: integer, minimum: 0 }
                remaining: { type: integer, minimum: 0 }
            entries:
              type: array
              items: { $ref: '#/components/schemas/ConsumerUsageEntry' }
    ConsumerUsageEntry:
      type: object
      additionalProperties: false
      required: [date, endpoint, statusClass, requestCount]
      properties:
        date: { type: string, format: date }
        endpoint: { type: string, example: 'GET /consumer/fixtures/:fixtureId/events' }
        statusClass: { type: string, enum: ['2xx', '3xx', '4xx', '5xx'] }
        requestCount: { type: integer, minimum: 0 }
    AdministratorApiConsumerUsageResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [consumer, from, to, totalRequests, entries]
          properties:
            consumer:
              type: object
              additionalProperties: false
              required: [id, name, rateLimitPerMinute, dailyQuota]
              properties:
                id: { $ref: '#/components/schemas/ApiIdentifier' }
                name: { type: string }
                rateLimitPerMinute: { type: integer, minimum: 1, maximum: 10000 }
                dailyQuota: { type: integer, minimum: 1, maximum: 10000 }
            from: { type: string, format: date }
            to: { type: string, format: date }
            totalRequests: { type: integer, minimum: 0 }
            entries:
              type: array
              items: { $ref: '#/components/schemas/ConsumerUsageEntry' }
    ApiConsumerKey:
      type: object
      additionalProperties: false
      required: [id, prefix, createdAt, revokedAt]
      properties:
        id: { $ref: '#/components/schemas/ApiIdentifier' }
        prefix: { type: string }
        createdAt: { type: string, format: date-time }
        revokedAt: { type: [string, 'null'], format: date-time }
    ApiConsumer:
      type: object
      additionalProperties: false
      required: [id, name, rateLimitPerMinute, dailyQuota, createdAt, keys]
      properties:
        id: { $ref: '#/components/schemas/ApiIdentifier' }
        name: { type: string }
        rateLimitPerMinute: { type: integer }
        dailyQuota: { type: integer }
        createdAt: { type: string, format: date-time }
        keys: { type: array, items: { $ref: '#/components/schemas/ApiConsumerKey' } }
    ApiConsumerIssueResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/ApiConsumerWithKey' }
    ApiConsumerWithKey:
      description: >-
        An API consumer together with its one-time raw key. Its fields must stay aligned with
        `ApiConsumer`, plus `apiKey`; the OpenAPI contract tests enforce this against real
        responses.
      type: object
      additionalProperties: false
      required: [id, name, rateLimitPerMinute, dailyQuota, createdAt, keys, apiKey]
      properties:
        id: { $ref: '#/components/schemas/ApiIdentifier' }
        name: { type: string }
        rateLimitPerMinute: { type: integer }
        dailyQuota: { type: integer }
        createdAt: { type: string, format: date-time }
        keys: { type: array, items: { $ref: '#/components/schemas/ApiConsumerKey' } }
        apiKey:
          type: string
          description: Raw API key. Returned only in this response and never retrievable again.
    ApiConsumerRotateResponse:
      $ref: '#/components/schemas/ApiConsumerIssueResponse'
    ApiConsumerListResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [consumers]
          properties:
            consumers: { type: array, items: { $ref: '#/components/schemas/ApiConsumer' } }
    ApiIdentifier:
      type: string
      minLength: 1
      description: |
        Stable opaque resource identifier. Consumers must not infer business
        meaning from its representation.

    DatabaseIdentifier:
      type: string
      pattern: '^[1-9]\d*$'
      maxLength: 19
      description: A positive database identifier represented as a JSON string.

    HealthResponse:
      type: object
      additionalProperties: false
      required:
        - status
        - service
        - timestamp
      properties:
        status:
          type: string
          const: ok
        service:
          type: string
          minLength: 1
        timestamp:
          type: string
          format: date-time

    CurrentUserProfileResponse:
      type: object
      additionalProperties: false
      required:
        - user
      properties:
        user:
          type: object
          additionalProperties: false
          required:
            - id
            - subject
            - displayName
            - role
            - approvalState
            - requestedCompetition
            - competitionIds
          properties:
            id:
              $ref: '#/components/schemas/ApiIdentifier'
            subject:
              type: string
              minLength: 1
            displayName:
              oneOf:
                - type: string
                  minLength: 1
                - type: 'null'
            role:
              type: string
              enum:
                - viewer
                - submitter
                - admin
            approvalState:
              type: string
              deprecated: true
              description: Legacy submitter-request workflow state; not an authorization grant.
              enum:
                - not_requested
                - pending
                - approved
                - rejected
            requestedCompetition:
              oneOf:
                - $ref: '#/components/schemas/AdministratorCompetitionScope'
                - type: 'null'
              description: Competition stored on the submitter-access request, or null when none has been requested.
            competitionIds:
              type: array
              uniqueItems: true
              items:
                $ref: '#/components/schemas/ApiIdentifier'

    AccountDeletionRequest:
      type: object
      additionalProperties: false
      required:
        - confirmation
      properties:
        confirmation:
          type: string
          const: DELETE

    AccountDeletionResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          type: object
          additionalProperties: false
          required:
            - status
            - retainedCricketData
          properties:
            status:
              type: string
              const: deleted
            retainedCricketData:
              type: boolean
              const: true

    SubmitterAccessRequest:
      type: object
      additionalProperties: false
      required:
        - competitionId
      properties:
        competitionId:
          $ref: '#/components/schemas/DatabaseIdentifier'

    SubmitterAccessRequestResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          type: object
          additionalProperties: false
          required:
            - accountId
            - approvalState
            - requestedCompetition
          properties:
            accountId:
              $ref: '#/components/schemas/ApiIdentifier'
            approvalState:
              type: string
              enum:
                - pending
            requestedCompetition:
              $ref: '#/components/schemas/AdministratorCompetitionScope'

    SubmitterScopeRequestResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          type: object
          additionalProperties: false
          required:
            - accountId
            - requestedCompetition
          properties:
            accountId:
              $ref: '#/components/schemas/ApiIdentifier'
            requestedCompetition:
              $ref: '#/components/schemas/AdministratorCompetitionScope'

    AdministratorCompetitionScope:
      type: object
      additionalProperties: false
      required:
        - competitionId
        - name
      properties:
        competitionId:
          $ref: '#/components/schemas/ApiIdentifier'
        name:
          type: string
          minLength: 1

    AdministratorAuditActor:
      type: object
      additionalProperties: false
      required:
        - id
        - displayName
      properties:
        id:
          $ref: '#/components/schemas/ApiIdentifier'
        displayName:
          oneOf:
            - type: string
              minLength: 1
            - type: 'null'

    AdministratorManagedUser:
      type: object
      additionalProperties: false
      required:
        - id
        - email
        - displayName
        - role
        - approvalState
        - requestedCompetition
        - competitionScopes
        - disabled
        - updatedAt
        - submitterAccessUpdatedAt
        - submitterAccessUpdatedBy
        - previouslyRevoked
      properties:
        id:
          $ref: '#/components/schemas/ApiIdentifier'
        email:
          type: string
          format: email
          description: Email retrieved by trusted backend code for an administrator only.
        displayName:
          oneOf:
            - type: string
              minLength: 1
            - type: 'null'
        role:
          type: string
          enum:
            - viewer
            - submitter
            - admin
        approvalState:
          type: string
          deprecated: true
          description: Compatibility state for the submitter-request workflow.
          enum:
            - not_requested
            - pending
            - approved
            - rejected
        requestedCompetition:
          oneOf:
            - $ref: '#/components/schemas/AdministratorCompetitionScope'
            - type: 'null'
          description: Competition stored on the submitter-access request, or null for legacy and not-requested accounts.
        competitionScopes:
          type: array
          uniqueItems: true
          items:
            $ref: '#/components/schemas/AdministratorCompetitionScope'
        disabled:
          type: boolean
        updatedAt:
          type: string
          format: date-time
        submitterAccessUpdatedAt:
          oneOf:
            - type: string
              format: date-time
            - type: 'null'
        submitterAccessUpdatedBy:
          oneOf:
            - $ref: '#/components/schemas/AdministratorAuditActor'
            - type: 'null'
        previouslyRevoked:
          type: boolean
          description: Whether this user has a retained prior submitter-access revocation.

    AdministratorUserManagementResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          type: object
          additionalProperties: false
          required:
            - users
            - availableScopes
          properties:
            users:
              type: array
              items:
                $ref: '#/components/schemas/AdministratorManagedUser'
            availableScopes:
              type: array
              uniqueItems: true
              items:
                $ref: '#/components/schemas/AdministratorCompetitionScope'

    AdministratorRoleUpdate:
      type: object
      additionalProperties: false
      required:
        - role
      properties:
        role:
          type: string
          enum:
            - viewer
            - submitter
            - admin
      description: Only `admin` is a direct role transition. Viewer and submitter values are validated but return a conflict directing callers to the submitter-access lifecycle endpoints.

    AdministratorSubmitterAccessUpdate:
      type: object
      additionalProperties: false
      required:
        - approved
        - competitionIds
      properties:
        approved:
          type: boolean
          description: True grants the submitter role; false revokes it.
        competitionIds:
          type: array
          uniqueItems: true
          items:
            $ref: '#/components/schemas/ApiIdentifier'
      description: |
        For a pending viewer approval, `competitionIds` must contain exactly the
        competition stored on the request. For an existing submitter scope
        replacement, it must contain at least one item. When false, the array
        must be empty.

    AdministratorSubmitterAccessResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          $ref: '#/components/schemas/AdministratorManagedUser'

    ApiErrorDetail:
      type: object
      additionalProperties: false
      required:
        - code
        - message
      properties:
        code:
          type: string
          pattern: '^[A-Z][A-Z0-9_]*$'
        message:
          type: string
          minLength: 1
        field:
          type: string
          minLength: 1
        eventIndex:
          type: integer
          minimum: 0
        ruleVersion:
          type: string
          minLength: 1
          description: Stable business-rule version when this detail comes from versioned validation.
        severity:
          type: string
          enum: [error, warning]
          description: Business-rule severity when this detail comes from versioned validation.
        taskReference:
          type: string
          format: uuid
          description: >-
            The item of a submitted array this failure belongs to, so a client can show it
            against that item rather than against the request as a whole. Carried by every
            fault of a participant onboarding array, which is applied all or nothing. This
            names a task, not a property; `field` remains the field-level slot.

    ApiErrorResponse:
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          type: object
          additionalProperties: false
          required:
            - code
            - message
          properties:
            code:
              type: string
              pattern: '^[A-Z][A-Z0-9_]*$'
            message:
              type: string
              minLength: 1
            details:
              type: array
              items:
                $ref: '#/components/schemas/ApiErrorDetail'

    WeatherData:
      type: object
      additionalProperties: false
      required:
        - date
        - latitude
        - longitude
        - temperatureMax
        - temperatureMin
        - precipitationSum
        - windSpeedMax
      properties:
        date:
          type: string
          pattern: '^\d{4}-\d{2}-\d{2}$'
        latitude:
          type: number
          minimum: -90
          maximum: 90
        longitude:
          type: number
          minimum: -180
          maximum: 180
        temperatureMax:
          oneOf:
            - type: number
            - type: 'null'
          description: Maximum daily temperature in degrees Celsius.
        temperatureMin:
          oneOf:
            - type: number
            - type: 'null'
          description: Minimum daily temperature in degrees Celsius.
        precipitationSum:
          oneOf:
            - type: number
            - type: 'null'
          description: Total daily precipitation in millimetres.
        windSpeedMax:
          oneOf:
            - type: number
            - type: 'null'
          description: Maximum daily wind speed in km/h.

    WeatherResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          $ref: '#/components/schemas/WeatherData'

    FixtureWeatherVenue:
      type: object
      additionalProperties: false
      required:
        - name
        - city
      properties:
        name:
          type: string
        city:
          oneOf:
            - type: string
            - type: 'null'

    FixtureWeatherAvailable:
      type: object
      additionalProperties: false
      required:
        - fixtureId
        - date
        - availability
        - venue
        - weather
      properties:
        fixtureId:
          $ref: '#/components/schemas/ApiIdentifier'
        date:
          type: string
          pattern: '^\d{4}-\d{2}-\d{2}$'
        availability:
          type: string
          enum: [available]
        venue:
          $ref: '#/components/schemas/FixtureWeatherVenue'
        weather:
          $ref: '#/components/schemas/WeatherData'

    FixtureWeatherUnavailable:
      type: object
      additionalProperties: false
      required:
        - fixtureId
        - date
        - availability
        - reason
        - venue
        - weather
      properties:
        fixtureId:
          $ref: '#/components/schemas/ApiIdentifier'
        date:
          type: string
          pattern: '^\d{4}-\d{2}-\d{2}$'
        availability:
          type: string
          enum: [unavailable]
        reason:
          type: string
          enum:
            [
              MISSING_VENUE,
              MISSING_COORDINATES,
              UNSUPPORTED_LOCATION,
              LOCATION_NOT_FOUND,
              UNSUPPORTED_DATE,
            ]
        venue:
          oneOf:
            - $ref: '#/components/schemas/FixtureWeatherVenue'
            - type: 'null'
        weather:
          type: 'null'

    FixtureWeatherResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          oneOf:
            - $ref: '#/components/schemas/FixtureWeatherAvailable'
            - $ref: '#/components/schemas/FixtureWeatherUnavailable'

    PaginationMetadata:
      type: object
      additionalProperties: false
      required:
        - nextCursor
      properties:
        nextCursor:
          oneOf:
            - type: string
              minLength: 1
            - type: 'null'

    TotalPagesPaginationMetadata:
      type: object
      additionalProperties: false
      required:
        - nextCursor
        - totalPages
      properties:
        nextCursor:
          oneOf:
            - type: string
              minLength: 1
            - type: 'null'
        totalPages:
          type: integer
          minimum: 0

    Competition:
      type: object
      additionalProperties: false
      required:
        - competitionId
        - name
      properties:
        competitionId:
          $ref: '#/components/schemas/ApiIdentifier'
        name:
          type: string
          minLength: 1

    Season:
      type: object
      additionalProperties: false
      required:
        - seasonId
        - competitionId
        - competitionName
        - label
      properties:
        seasonId:
          $ref: '#/components/schemas/ApiIdentifier'
        competitionId:
          $ref: '#/components/schemas/ApiIdentifier'
        competitionName:
          type: string
          minLength: 1
        label:
          type: string
          minLength: 1

    Fixture:
      type: object
      additionalProperties: false
      required:
        - fixtureId
        - competitionId
        - competitionName
        - seasonId
        - season
        - seasonLabel
        - competitors
        - matchType
        - teamType
        - gender
        - ballsPerOver
        - scheduledOvers
        - venue
        - toss
        - startDate
        - endDate
      properties:
        fixtureId:
          $ref: '#/components/schemas/ApiIdentifier'

        competitionId:
          oneOf:
            - $ref: '#/components/schemas/ApiIdentifier'
            - type: 'null'
        competitionName:
          oneOf:
            - type: string
              minLength: 1
            - type: 'null'

        seasonId:
          oneOf:
            - $ref: '#/components/schemas/ApiIdentifier'
            - type: 'null'

        season:
          type: string
          minLength: 1
        seasonLabel:
          type: string
          minLength: 1

        competitors:
          type: array
          items:
            $ref: '#/components/schemas/Competitor'
        matchType:
          type: string
          minLength: 1

        teamType:
          type: string
          minLength: 1

        gender:
          type: string
          minLength: 1

        ballsPerOver:
          type: integer
          minimum: 1

        scheduledOvers:
          oneOf:
            - type: integer
              minimum: 1
            - type: 'null'

        venue:
          oneOf:
            - $ref: '#/components/schemas/FixtureVenue'
            - type: 'null'

        toss:
          oneOf:
            - $ref: '#/components/schemas/FixtureToss'
            - type: 'null'

        startDate:
          type: string
          format: date

        endDate:
          type: string
          format: date

    FixtureVenue:
      type: object
      additionalProperties: false
      required:
        - name
        - city
      properties:
        name:
          type: string
          minLength: 1
        city:
          oneOf:
            - type: string
              minLength: 1
            - type: 'null'

    FixtureToss:
      type: object
      additionalProperties: false
      required:
        - winnerCompetitorId
        - winnerCompetitorName
        - decision
      properties:
        winnerCompetitorId:
          oneOf:
            - $ref: '#/components/schemas/ApiIdentifier'
            - type: 'null'
        winnerCompetitorName:
          oneOf:
            - type: string
              minLength: 1
            - type: 'null'
        decision:
          oneOf:
            - type: string
              enum:
                - bat
                - field
            - type: 'null'

    Competitor:
      type: object
      additionalProperties: false
      required:
        - competitorId
        - name
      properties:
        competitorId:
          $ref: '#/components/schemas/ApiIdentifier'
        name:
          type: string
          minLength: 1

    Participant:
      type: object
      additionalProperties: false
      required:
        - participantId
        - displayName
      properties:
        participantId:
          $ref: '#/components/schemas/ApiIdentifier'
        displayName:
          type: string
          minLength: 1

    ParticipantFixtureBatting:
      type: object
      additionalProperties: false
      required:
        - runsScored
        - ballsFaced
        - fours
        - sixes
        - strikeRate
      properties:
        runsScored:
          type: integer
          minimum: 0
        ballsFaced:
          type: integer
          minimum: 0
        fours:
          type: integer
          minimum: 0
        sixes:
          type: integer
          minimum: 0
        strikeRate:
          oneOf:
            - type: number
              minimum: 0
            - type: 'null'

    ParticipantAggregateBestBowling:
      type: object
      additionalProperties: false
      required:
        - wicketsTaken
        - runsConceded
      properties:
        wicketsTaken:
          type: integer
          minimum: 0
        runsConceded:
          type: integer
          minimum: 0

    ParticipantFixtureBowling:
      type: object
      additionalProperties: false
      required:
        - runsConceded
        - wides
        - noBalls
        - legalBallsBowled
        - oversBowled
        - wicketsTaken
        - economyRate
      properties:
        runsConceded:
          type: integer
          minimum: 0
        wides:
          type: integer
          minimum: 0
          description: Bowler-attributable wide runs included in runsConceded.
        noBalls:
          type: integer
          minimum: 0
          description: Bowler-attributable no-ball runs included in runsConceded.
        legalBallsBowled:
          type: integer
          minimum: 0
        oversBowled:
          type: string
          pattern: '^\d+\.\d+$'
        wicketsTaken:
          type: integer
          minimum: 0
        economyRate:
          oneOf:
            - type: number
              minimum: 0
            - type: 'null'

    ParticipantAggregateFielding:
      type: object
      additionalProperties: false
      required:
        - catches
        - stumpings
        - runOutInvolvements
      properties:
        catches:
          type: integer
          minimum: 0
        stumpings:
          type: integer
          minimum: 0
        runOutInvolvements:
          type: integer
          minimum: 0

    ParticipantFixture:
      type: object
      additionalProperties: false
      required:
        - fixture
        - competitionName
        - competitors
        - competitor
        - role
        - statisticsStatus
        - statisticsWarnings
        - batting
        - bowling
      properties:
        fixture:
          $ref: '#/components/schemas/Fixture'
        competitionName:
          oneOf:
            - type: string
              minLength: 1
            - type: 'null'
        competitors:
          type: array
          items:
            $ref: '#/components/schemas/Competitor'
        competitor:
          $ref: '#/components/schemas/Competitor'
        role:
          oneOf:
            - type: string
              minLength: 1
            - type: 'null'
        statisticsStatus:
          type: string
          enum:
            - complete
            - partial
        statisticsWarnings:
          type: array
          items:
            $ref: '#/components/schemas/FixtureStatisticsWarning'
        batting:
          oneOf:
            - $ref: '#/components/schemas/ParticipantFixtureBatting'
            - type: 'null'
        bowling:
          oneOf:
            - $ref: '#/components/schemas/ParticipantFixtureBowling'
            - type: 'null'

    PublicEventFielder:
      type: object
      additionalProperties: false
      required:
        - participantId
        - participantName
        - isSubstitute
      properties:
        participantId:
          oneOf:
            - $ref: '#/components/schemas/ApiIdentifier'
            - type: 'null'
        participantName:
          oneOf:
            - type: string
              minLength: 1
            - type: 'null'
        isSubstitute:
          type: boolean

    PublicEventWicket:
      type: object
      additionalProperties: false
      required:
        - wicketId
        - kind
        - playerOutParticipantId
        - playerOutParticipantName
        - fielders
      properties:
        wicketId:
          $ref: '#/components/schemas/ApiIdentifier'
        kind:
          type: string
          minLength: 1
        playerOutParticipantId:
          $ref: '#/components/schemas/ApiIdentifier'
        playerOutParticipantName:
          type: string
          minLength: 1
        fielders:
          type: array
          items:
            $ref: '#/components/schemas/PublicEventFielder'

    PublicEvent:
      type: object
      additionalProperties: false
      required:
        - eventId
        - fixtureId
        - competitionId
        - competitionName
        - inningsId
        - inningsOrdinal
        - sequenceNumber
        - overNumber
        - positionInOver
        - ballNumber
        - battingCompetitorId
        - battingCompetitorName
        - bowlingCompetitorId
        - bowlingCompetitorName
        - strikerParticipantId
        - strikerParticipantName
        - nonStrikerParticipantId
        - nonStrikerParticipantName
        - bowlerParticipantId
        - bowlerParticipantName
        - runs
        - extras
        - wickets
      properties:
        eventId:
          $ref: '#/components/schemas/ApiIdentifier'
        fixtureId:
          $ref: '#/components/schemas/ApiIdentifier'
        competitionId:
          oneOf:
            - $ref: '#/components/schemas/ApiIdentifier'
            - type: 'null'
        competitionName:
          oneOf:
            - type: string
              minLength: 1
            - type: 'null'
        inningsId:
          $ref: '#/components/schemas/ApiIdentifier'
        inningsOrdinal:
          type: integer
          minimum: 0
        sequenceNumber:
          type: integer
          minimum: 1
        overNumber:
          type: integer
          minimum: 0
        positionInOver:
          type: integer
          minimum: 0
        ballNumber:
          type: string
          minLength: 1
          description: Display label only; never used for ordering or identity.
        battingCompetitorId:
          $ref: '#/components/schemas/ApiIdentifier'
        battingCompetitorName:
          type: string
          minLength: 1
        bowlingCompetitorId:
          oneOf:
            - $ref: '#/components/schemas/ApiIdentifier'
            - type: 'null'
        bowlingCompetitorName:
          oneOf:
            - type: string
              minLength: 1
            - type: 'null'
        strikerParticipantId:
          $ref: '#/components/schemas/ApiIdentifier'
        strikerParticipantName:
          type: string
          minLength: 1
        nonStrikerParticipantId:
          $ref: '#/components/schemas/ApiIdentifier'
        nonStrikerParticipantName:
          type: string
          minLength: 1
        bowlerParticipantId:
          $ref: '#/components/schemas/ApiIdentifier'
        bowlerParticipantName:
          type: string
          minLength: 1
        runs:
          type: object
          additionalProperties: false
          required:
            - offBat
            - extras
            - total
            - nonBoundary
          properties:
            offBat:
              type: integer
              minimum: 0
            extras:
              type: integer
              minimum: 0
            total:
              type: integer
              minimum: 0
            nonBoundary:
              type: boolean
        extras:
          type: object
          additionalProperties: false
          required:
            - wides
            - noBalls
            - byes
            - legByes
            - penalty
          properties:
            wides:
              oneOf:
                - type: integer
                  minimum: 0
                - type: 'null'
            noBalls:
              oneOf:
                - type: integer
                  minimum: 0
                - type: 'null'
            byes:
              oneOf:
                - type: integer
                  minimum: 0
                - type: 'null'
            legByes:
              oneOf:
                - type: integer
                  minimum: 0
                - type: 'null'
            penalty:
              oneOf:
                - type: integer
                  minimum: 0
                - type: 'null'
        wickets:
          type: array
          items:
            $ref: '#/components/schemas/PublicEventWicket'

    StatisticContributingEvent:
      type: object
      additionalProperties: false
      required:
        - eventId
        - fixtureId
        - inningsId
        - inningsOrdinal
        - sequenceNumber
        - strikerParticipantId
        - strikerParticipantName
        - bowlerParticipantId
        - bowlerParticipantName
        - runs
        - extras
        - nonBoundary
        - bowlerWickets
        - wicketsLost
      properties:
        eventId:
          $ref: '#/components/schemas/ApiIdentifier'
        fixtureId:
          $ref: '#/components/schemas/ApiIdentifier'
        inningsId:
          $ref: '#/components/schemas/ApiIdentifier'
        inningsOrdinal:
          type: integer
          minimum: 0
        sequenceNumber:
          type: integer
          minimum: 1
        strikerParticipantId:
          $ref: '#/components/schemas/ApiIdentifier'
        strikerParticipantName:
          type: string
          minLength: 1
        bowlerParticipantId:
          $ref: '#/components/schemas/ApiIdentifier'
        bowlerParticipantName:
          type: string
          minLength: 1
        runs:
          type: object
          additionalProperties: false
          required:
            - offBat
            - extras
            - total
          properties:
            offBat:
              type: integer
              minimum: 0
            extras:
              type: integer
              minimum: 0
            total:
              type: integer
              minimum: 0
        extras:
          type: object
          additionalProperties: false
          required:
            - wides
            - noBalls
            - byes
            - legByes
            - penalty
          properties:
            wides:
              oneOf:
                - type: integer
                  minimum: 0
                - type: 'null'
            noBalls:
              oneOf:
                - type: integer
                  minimum: 0
                - type: 'null'
            byes:
              oneOf:
                - type: integer
                  minimum: 0
                - type: 'null'
            legByes:
              oneOf:
                - type: integer
                  minimum: 0
                - type: 'null'
            penalty:
              oneOf:
                - type: integer
                  minimum: 0
                - type: 'null'
        nonBoundary:
          type: boolean
        bowlerWickets:
          type: integer
          minimum: 0
        wicketsLost:
          type: integer
          minimum: 0

    FixtureStatisticCommon:
      type: object
      required:
        - statisticId
        - fixtureId
        - sourceEventCount
      properties:
        statisticId:
          $ref: '#/components/schemas/ApiIdentifier'
        fixtureId:
          $ref: '#/components/schemas/ApiIdentifier'
        sourceEventCount:
          type: integer
          minimum: 0
        contributingEvents:
          type: array
          description: Present only when includeContributors=true.
          items:
            $ref: '#/components/schemas/StatisticContributingEvent'

    InningsTeamStatistic:
      allOf:
        - $ref: '#/components/schemas/FixtureStatisticCommon'
        - type: object
          required:
            - scope
            - statisticCode
            - inningsId
            - inningsOrdinal
            - competitorId
            - competitorName
            - metrics
          properties:
            scope:
              type: string
              const: innings
            statisticCode:
              type: string
              const: team_total
            inningsId:
              $ref: '#/components/schemas/ApiIdentifier'
            inningsOrdinal:
              type: integer
              minimum: 0
            competitorId:
              $ref: '#/components/schemas/ApiIdentifier'
            competitorName:
              type: string
              minLength: 1
            metrics:
              type: object
              additionalProperties: false
              required:
                - deliveryRuns
                - penaltyRuns
                - totalRuns
                - wicketsLost
                - legalBalls
                - overs
                - runRate
                - powerplay
                - extras
              properties:
                deliveryRuns:
                  type: integer
                  minimum: 0
                penaltyRuns:
                  type: integer
                  minimum: 0
                totalRuns:
                  type: integer
                  minimum: 0
                wicketsLost:
                  type: integer
                  minimum: 0
                legalBalls:
                  type: integer
                  minimum: 0
                overs:
                  type: string
                  pattern: '^\d+\.\d+$'
                  example: '20.0'
                runRate:
                  type:
                    - number
                    - 'null'
                  minimum: 0
                  description: Runs per over, or null when no legal ball has been bowled.
                powerplay:
                  oneOf:
                    - type: object
                      additionalProperties: false
                      required:
                        - ranges
                        - runs
                        - wicketsLost
                        - legalBalls
                        - overs
                        - runRate
                        - sourceEventCount
                      properties:
                        ranges:
                          type: array
                          minItems: 1
                          items:
                            type: object
                            additionalProperties: false
                            required: [fromBall, toBall, type]
                            properties:
                              fromBall:
                                type: number
                                minimum: 0
                              toBall:
                                type: number
                                minimum: 0
                              type:
                                type: string
                                minLength: 1
                        runs:
                          type: integer
                          minimum: 0
                        wicketsLost:
                          type: integer
                          minimum: 0
                        legalBalls:
                          type: integer
                          minimum: 0
                        overs:
                          type: string
                          pattern: '^\d+\.\d+$'
                        runRate:
                          type:
                            - number
                            - 'null'
                          minimum: 0
                        sourceEventCount:
                          type: integer
                          minimum: 0
                        contributingEvents:
                          type: array
                          description: Present only when includeContributors=true.
                          items:
                            $ref: '#/components/schemas/StatisticContributingEvent'
                    - type: 'null'
                  description: Authoritative powerplay aggregate, or null when no marker metadata exists.
                extras:
                  type: object
                  additionalProperties: false
                  required:
                    - total
                    - wides
                    - noBalls
                    - byes
                    - legByes
                    - penaltyRuns
                  properties:
                    total:
                      type: integer
                      minimum: 0
                    wides:
                      type: integer
                      minimum: 0
                    noBalls:
                      type: integer
                      minimum: 0
                    byes:
                      type: integer
                      minimum: 0
                    legByes:
                      type: integer
                      minimum: 0
                    penaltyRuns:
                      type: integer
                      minimum: 0

    ParticipantFixtureStatistic:
      allOf:
        - $ref: '#/components/schemas/FixtureStatisticCommon'
        - type: object
          required:
            - participantId
            - participantName
            - competitorId
            - competitorName
            - battingPosition
            - battingParticipation
            - dismissal
            - batting
            - bowling
          properties:
            scope:
              type: string
              const: participant
            statisticCode:
              type: string
              const: participant_fixture
            participantId:
              $ref: '#/components/schemas/ApiIdentifier'
            participantName:
              type: string
              minLength: 1
            competitorId:
              oneOf:
                - $ref: '#/components/schemas/ApiIdentifier'
                - type: 'null'
            competitorName:
              oneOf:
                - type: string
                  minLength: 1
                - type: 'null'
            battingPosition:
              oneOf:
                - type: integer
                  minimum: 1
                - type: 'null'
            battingParticipation:
              type: string
              enum:
                - did_not_bat
                - batted
            dismissal:
              oneOf:
                - type: object
                  additionalProperties: false
                  required:
                    - status
                    - kind
                    - eventId
                  properties:
                    status:
                      type: string
                      enum:
                        - not_out
                        - dismissed
                    kind:
                      oneOf:
                        - type: string
                          minLength: 1
                        - type: 'null'
                    eventId:
                      oneOf:
                        - $ref: '#/components/schemas/ApiIdentifier'
                        - type: 'null'
                - type: 'null'

            batting:
              oneOf:
                - type: object
                  additionalProperties: false
                  required:
                    - runsScored
                    - ballsFaced
                    - strikeRate
                    - fours
                    - sixes
                  properties:
                    runsScored:
                      type: integer
                      minimum: 0
                    ballsFaced:
                      type: integer
                      minimum: 0
                    strikeRate:
                      oneOf:
                        - type: number
                          minimum: 0
                        - type: 'null'
                    fours:
                      type: integer
                      minimum: 0
                    sixes:
                      type: integer
                      minimum: 0
                - type: 'null'
            bowling:
              oneOf:
                - type: object
                  additionalProperties: false
                  required:
                    - runsConceded
                    - wides
                    - noBalls
                    - legalBallsBowled
                    - oversBowled
                    - economyRate
                    - wicketsTaken
                  properties:
                    runsConceded:
                      type: integer
                      minimum: 0
                    wides:
                      type: integer
                      minimum: 0
                    noBalls:
                      type: integer
                      minimum: 0
                    legalBallsBowled:
                      type: integer
                      minimum: 0
                    oversBowled:
                      type: string
                      pattern: '^\d+\.\d+$'
                    economyRate:
                      oneOf:
                        - type: number
                          minimum: 0
                        - type: 'null'
                    wicketsTaken:
                      type: integer
                      minimum: 0
                - type: 'null'

    FixtureStatistic:
      oneOf:
        - $ref: '#/components/schemas/InningsTeamStatistic'
        - $ref: '#/components/schemas/ParticipantFixtureStatistic'

    FixtureStatisticsWarning:
      type: object
      additionalProperties: false
      required:
        - code
        - message
      properties:
        code:
          type: string
          enum:
            - SOURCE_DATA_INCOMPLETE
            - NO_STANDARD_INNINGS
            - NO_ACCEPTED_EVENTS
            - INNINGS_WITHOUT_ACCEPTED_EVENTS
            - PARTICIPANT_COMPETITOR_UNKNOWN
        message:
          type: string
          minLength: 1
        inningsId:
          $ref: '#/components/schemas/ApiIdentifier'
        participantId:
          $ref: '#/components/schemas/ApiIdentifier'
        fields:
          type: array
          items:
            type: string
            minLength: 1

    FixtureOutcome:
      type: object
      additionalProperties: false
      required:
        - kind
        - winnerCompetitorId
        - winnerCompetitorName
        - eliminatorCompetitorId
        - eliminatorCompetitorName
        - margin
        - method
        - decidedByBowlOut
      properties:
        kind:
          type: string
          enum:
            - won
            - tie
            - draw
            - no_result
        winnerCompetitorId:
          oneOf:
            - $ref: '#/components/schemas/ApiIdentifier'
            - type: 'null'
        winnerCompetitorName:
          oneOf:
            - type: string
              minLength: 1
            - type: 'null'
        eliminatorCompetitorId:
          oneOf:
            - $ref: '#/components/schemas/ApiIdentifier'
            - type: 'null'
        eliminatorCompetitorName:
          oneOf:
            - type: string
              minLength: 1
            - type: 'null'
        margin:
          oneOf:
            - type: object
              additionalProperties: false
              required:
                - type
                - value
              properties:
                type:
                  type: string
                  enum:
                    - runs
                    - wickets
                value:
                  type: integer
                  minimum: 0
            - type: 'null'
        method:
          oneOf:
            - type: string
              minLength: 1
            - type: 'null'
        decidedByBowlOut:
          type: boolean

    FixtureHighestScorer:
      type: object
      additionalProperties: false
      required:
        - participantId
        - participantName
        - competitorId
        - competitorName
        - inningsId
        - inningsOrdinal
        - runsScored
        - notOut
      properties:
        participantId:
          $ref: '#/components/schemas/ApiIdentifier'
        participantName:
          type: string
          minLength: 1
        competitorId:
          $ref: '#/components/schemas/ApiIdentifier'
        competitorName:
          type: string
          minLength: 1
        inningsId:
          $ref: '#/components/schemas/ApiIdentifier'
        inningsOrdinal:
          type: integer
          minimum: 0
        runsScored:
          type: integer
          minimum: 0
        notOut:
          type: boolean

    FixtureStatistics:
      type: object
      additionalProperties: false
      required:
        - fixtureId
        - status
        - scope
        - outcome
        - highestScorers
        - warnings
        - statistics
      properties:
        fixtureId:
          $ref: '#/components/schemas/ApiIdentifier'
        status:
          type: string
          enum:
            - complete
            - partial
        scope:
          type: object
          additionalProperties: false
          required:
            - superOversIncluded
          properties:
            superOversIncluded:
              type: boolean
              const: false
        outcome:
          $ref: '#/components/schemas/FixtureOutcome'
        highestScorers:
          type: array
          description: >-
            Batter or batters with the highest single-innings score across the
            published standard innings. Ties are returned in deterministic innings
            and participant order.
          items:
            $ref: '#/components/schemas/FixtureHighestScorer'
        warnings:
          type: array
          items:
            $ref: '#/components/schemas/FixtureStatisticsWarning'
        statistics:
          type: array
          items:
            $ref: '#/components/schemas/FixtureStatistic'

    FixtureStatisticsResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          $ref: '#/components/schemas/FixtureStatistics'

    FixtureStatisticResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          $ref: '#/components/schemas/FixtureStatistic'

    ParticipantAggregateBatting:
      type: object
      additionalProperties: false
      required:
        - innings
        - runsScored
        - ballsFaced
        - dismissals
        - notOuts
        - battingAverage
        - fours
        - sixes
        - fifties
        - hundreds
        - highestScore
        - highestScoreNotOut
        - strikeRate
      properties:
        innings:
          type: integer
          minimum: 0
        runsScored:
          type: integer
          minimum: 0
        ballsFaced:
          type: integer
          minimum: 0
        dismissals:
          type: integer
          minimum: 0
        notOuts:
          type: integer
          minimum: 0
        battingAverage:
          oneOf:
            - type: number
              minimum: 0
            - type: 'null'
        fours:
          type: integer
          minimum: 0
        sixes:
          type: integer
          minimum: 0
        fifties:
          type: integer
          minimum: 0
        hundreds:
          type: integer
          minimum: 0
        highestScore:
          type: integer
          minimum: 0
        highestScoreNotOut:
          type: boolean
        strikeRate:
          oneOf:
            - type: number
              minimum: 0
            - type: 'null'

    ParticipantAggregateBowling:
      type: object
      additionalProperties: false
      required:
        - innings
        - runsConceded
        - wides
        - noBalls
        - legalBallsBowled
        - wicketsTaken
        - bowlingAverage
        - bowlingStrikeRate
        - bestBowling
        - fourWicketHauls
        - fiveWicketHauls
        - ballsPerOver
        - oversBowled
        - economyRate
      properties:
        innings:
          type: integer
          minimum: 0
        runsConceded:
          type: integer
          minimum: 0
        wides:
          type: integer
          minimum: 0
          description: Bowler-attributable wide runs included in runsConceded.
        noBalls:
          type: integer
          minimum: 0
          description: Bowler-attributable no-ball runs included in runsConceded.
        legalBallsBowled:
          type: integer
          minimum: 0
        wicketsTaken:
          type: integer
          minimum: 0
        bowlingAverage:
          oneOf:
            - type: number
              minimum: 0
            - type: 'null'
        bowlingStrikeRate:
          oneOf:
            - type: number
              minimum: 0
            - type: 'null'
        bestBowling:
          $ref: '#/components/schemas/ParticipantAggregateBestBowling'
        fourWicketHauls:
          type: integer
          minimum: 0
        fiveWicketHauls:
          type: integer
          minimum: 0
        ballsPerOver:
          description: |
            The balls-per-over shared by the fixtures in this group, or null
            where they do not agree. Legal balls are always counted from the
            delivery rows and are never assumed to be six an over.
          oneOf:
            - type: integer
              minimum: 1
            - type: 'null'
        oversBowled:
          oneOf:
            - type: string
              pattern: '^\d+\.\d+$'
            - type: 'null'
        economyRate:
          oneOf:
            - type: number
              minimum: 0
            - type: 'null'

    ParticipantSeasonAggregate:
      type: object
      additionalProperties: false
      required:
        - statisticId
        - participantId
        - participantName
        - scope
        - statisticCode
        - competitionId
        - competitionName
        - seasonId
        - season
        - appearances
        - fixtureCount
        - sourceEventCount
        - batting
        - bowling
        - fielding
      properties:
        statisticId:
          $ref: '#/components/schemas/ApiIdentifier'
        participantId:
          $ref: '#/components/schemas/ApiIdentifier'
        participantName:
          type: string
          minLength: 1
        scope:
          type: string
          const: season
        statisticCode:
          type: string
          const: participant_season
        competitionId:
          oneOf:
            - $ref: '#/components/schemas/ApiIdentifier'
            - type: 'null'
        competitionName:
          oneOf:
            - type: string
              minLength: 1
            - type: 'null'
        seasonId:
          oneOf:
            - $ref: '#/components/schemas/ApiIdentifier'
            - type: 'null'
        season:
          type: string
          minLength: 1
        appearances:
          type: integer
          minimum: 0
        fixtureCount:
          type: integer
          minimum: 0
        sourceEventCount:
          type: integer
          minimum: 0
        batting:
          oneOf:
            - $ref: '#/components/schemas/ParticipantAggregateBatting'
            - type: 'null'
        bowling:
          oneOf:
            - $ref: '#/components/schemas/ParticipantAggregateBowling'
            - type: 'null'
        fielding:
          $ref: '#/components/schemas/ParticipantAggregateFielding'

    ParticipantCompetitionAggregate:
      type: object
      additionalProperties: false
      required:
        - statisticId
        - participantId
        - participantName
        - scope
        - statisticCode
        - competitionId
        - competitionName
        - appearances
        - fixtureCount
        - sourceEventCount
        - batting
        - bowling
        - fielding
      properties:
        statisticId:
          $ref: '#/components/schemas/ApiIdentifier'
        participantId:
          $ref: '#/components/schemas/ApiIdentifier'
        participantName:
          type: string
          minLength: 1
        scope:
          type: string
          const: competition
        statisticCode:
          type: string
          const: participant_competition
        competitionId:
          oneOf:
            - $ref: '#/components/schemas/ApiIdentifier'
            - type: 'null'
        competitionName:
          oneOf:
            - type: string
              minLength: 1
            - type: 'null'
        appearances:
          type: integer
          minimum: 0
        fixtureCount:
          type: integer
          minimum: 0
        sourceEventCount:
          type: integer
          minimum: 0
        batting:
          oneOf:
            - $ref: '#/components/schemas/ParticipantAggregateBatting'
            - type: 'null'
        bowling:
          oneOf:
            - $ref: '#/components/schemas/ParticipantAggregateBowling'
            - type: 'null'
        fielding:
          $ref: '#/components/schemas/ParticipantAggregateFielding'

    ParticipantCareerAggregate:
      type: object
      additionalProperties: false
      required:
        - statisticId
        - participantId
        - participantName
        - scope
        - statisticCode
        - appearances
        - fixtureCount
        - sourceEventCount
        - batting
        - bowling
        - fielding
      properties:
        statisticId:
          $ref: '#/components/schemas/ApiIdentifier'
        participantId:
          $ref: '#/components/schemas/ApiIdentifier'
        participantName:
          type: string
          minLength: 1
        scope:
          type: string
          const: career
        statisticCode:
          type: string
          const: participant_career
        appearances:
          type: integer
          minimum: 0
        fixtureCount:
          type: integer
          minimum: 0
        sourceEventCount:
          type: integer
          minimum: 0
        batting:
          oneOf:
            - $ref: '#/components/schemas/ParticipantAggregateBatting'
            - type: 'null'
        bowling:
          oneOf:
            - $ref: '#/components/schemas/ParticipantAggregateBowling'
            - type: 'null'
        fielding:
          $ref: '#/components/schemas/ParticipantAggregateFielding'

    ParticipantAggregate:
      oneOf:
        - $ref: '#/components/schemas/ParticipantSeasonAggregate'
        - $ref: '#/components/schemas/ParticipantCompetitionAggregate'
        - $ref: '#/components/schemas/ParticipantCareerAggregate'
      discriminator:
        propertyName: scope
        mapping:
          season: '#/components/schemas/ParticipantSeasonAggregate'
          competition: '#/components/schemas/ParticipantCompetitionAggregate'
          career: '#/components/schemas/ParticipantCareerAggregate'

    ParticipantAggregatesWarning:
      type: object
      additionalProperties: false
      required:
        - code
        - message
      properties:
        code:
          type: string
          enum:
            - NO_ACCEPTED_EVENTS
            - COMPETITION_UNKNOWN
            - MIXED_BALLS_PER_OVER
        message:
          type: string
          minLength: 1
        competitionId:
          $ref: '#/components/schemas/ApiIdentifier'
        season:
          type: string
          minLength: 1

    ParticipantAggregates:
      type: object
      additionalProperties: false
      required:
        - participantId
        - participantName
        - status
        - scope
        - warnings
        - statistics
      properties:
        participantId:
          $ref: '#/components/schemas/ApiIdentifier'
        participantName:
          type: string
          minLength: 1
        status:
          type: string
          enum:
            - complete
            - partial
        scope:
          type: object
          additionalProperties: false
          required:
            - superOversIncluded
          properties:
            superOversIncluded:
              type: boolean
              const: false
        warnings:
          type: array
          items:
            $ref: '#/components/schemas/ParticipantAggregatesWarning'
        statistics:
          type: array
          items:
            $ref: '#/components/schemas/ParticipantAggregate'

    ParticipantAggregatesResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          $ref: '#/components/schemas/ParticipantAggregates'

    ParticipantAggregateResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          $ref: '#/components/schemas/ParticipantAggregate'

    AnalyticsQueryScope:
      description: >-
        The level a question is asked at, matching the scopes the published
        participant aggregates expose.
      type: string
      enum: [season, competition, career]

    AnalyticsQueryNameHint:
      description: >-
        A name as the reader wrote it, resolved to an identifier server-side. It
        is never an identifier and never reaches the database as anything but a
        bound parameter.
      type: string
      minLength: 1
      maxLength: 100

    AnalyticsQueryParticipantReference:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        name: { $ref: '#/components/schemas/AnalyticsQueryNameHint' }

    AnalyticsQueryCompetitionReference:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        name: { $ref: '#/components/schemas/AnalyticsQueryNameHint' }

    AnalyticsQuerySeasonReference:
      description: >-
        A season is a competition together with a label; it has no name of its
        own.
      type: object
      additionalProperties: false
      required: [competitionName, seasonLabel]
      properties:
        competitionName: { $ref: '#/components/schemas/AnalyticsQueryNameHint' }
        seasonLabel: { $ref: '#/components/schemas/AnalyticsQueryNameHint' }

    AnalyticsQueryDefinition:
      description: >-
        One structured question over the published statistics. The scope-to-
        reference rule is part of the contract but cannot be expressed here: a
        season scope carries a season reference, a competition scope carries a
        competition reference, and a career scope carries neither. A body that
        breaks it is rejected with 422.
      oneOf:
        - $ref: '#/components/schemas/LeaderboardQueryDefinition'
        - $ref: '#/components/schemas/ParticipantStatisticsQueryDefinition'
        - $ref: '#/components/schemas/ParticipantComparisonQueryDefinition'
        - $ref: '#/components/schemas/UnsupportedQueryDefinition'
      discriminator:
        propertyName: kind
        mapping:
          leaderboard: '#/components/schemas/LeaderboardQueryDefinition'
          participant_statistics: '#/components/schemas/ParticipantStatisticsQueryDefinition'
          participant_comparison: '#/components/schemas/ParticipantComparisonQueryDefinition'
          unsupported: '#/components/schemas/UnsupportedQueryDefinition'

    LeaderboardQueryDefinition:
      type: object
      additionalProperties: false
      required: [kind, metric, scope]
      properties:
        kind: { type: string, const: leaderboard }
        metric: { $ref: '#/components/schemas/LeaderboardMetric' }
        scope: { type: string, enum: [season, competition] }
        season: { $ref: '#/components/schemas/AnalyticsQuerySeasonReference' }
        competition: { $ref: '#/components/schemas/AnalyticsQueryCompetitionReference' }
        limit:
          description: Defaults to 10 when omitted.
          type: integer
          minimum: 1
          maximum: 50

    ParticipantStatisticsQueryDefinition:
      type: object
      additionalProperties: false
      required: [kind, participant, scope]
      properties:
        kind: { type: string, const: participant_statistics }
        participant: { $ref: '#/components/schemas/AnalyticsQueryParticipantReference' }
        scope: { $ref: '#/components/schemas/AnalyticsQueryScope' }
        season: { $ref: '#/components/schemas/AnalyticsQuerySeasonReference' }
        competition: { $ref: '#/components/schemas/AnalyticsQueryCompetitionReference' }

    ParticipantComparisonQueryDefinition:
      type: object
      additionalProperties: false
      required: [kind, participants, scope]
      properties:
        kind: { type: string, const: participant_comparison }
        participants:
          description: Exactly two references, in the order they are compared.
          type: array
          items: { $ref: '#/components/schemas/AnalyticsQueryParticipantReference' }
          minItems: 2
          maxItems: 2
        scope: { $ref: '#/components/schemas/AnalyticsQueryScope' }
        season: { $ref: '#/components/schemas/AnalyticsQuerySeasonReference' }
        competition: { $ref: '#/components/schemas/AnalyticsQueryCompetitionReference' }

    UnsupportedQueryDefinition:
      type: object
      additionalProperties: false
      required: [kind, reason]
      properties:
        kind: { type: string, const: unsupported }
        reason: { $ref: '#/components/schemas/UnsupportedQueryReason' }

    UnsupportedQueryReason:
      type: string
      enum:
        - bowler_type
        - batting_hand
        - match_phase
        - venue
        - super_over
        - outside_cricket_statistics
        - ambiguous
        - other

    NaturalLanguageQuery:
      description: A reader's question. It is the only thing a caller may send.
      type: object
      additionalProperties: false
      required: [question]
      properties:
        question:
          description: >-
            The question, trimmed before it is measured. The bound is part of the
            contract rather than configuration, because the text reaches a paid
            provider.
          type: string
          minLength: 1
          maxLength: 300

    NaturalLanguageQueryResult:
      type: object
      additionalProperties: false
      required: [question, model, evaluation]
      properties:
        question:
          description: The question as it was understood, trimmed.
          type: string
          minLength: 1
          maxLength: 300
        model:
          description: >-
            The model that produced the definition, as the provider reported it.
            Token counts are deliberately absent: they are metered in the server
            logs rather than returned.
          type: string
          minLength: 1
          maxLength: 100
        evaluation: { $ref: '#/components/schemas/QueryDefinitionEvaluation' }

    NaturalLanguageQueryResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/NaturalLanguageQueryResult' }

    QueryDefinitionVersion:
      description: >-
        A deterministic digest of the evaluated definition. The same question
        carries the same version, and reordering the body does not change it.
      type: string
      pattern: '^qdv1_[A-Za-z0-9_-]{43}$'

    QueryDefinitionReference:
      description: Which reference in the definition an outcome is about.
      type: string
      enum: [participant, participants.0, participants.1, competition, season]

    QueryDefinitionCandidate:
      description: >-
        One entity a name hint could have meant. It carries an identifier and a
        display name only; many people in the corpus share a display name, so
        two candidates can look identical here.
      type: object
      additionalProperties: false
      required: [id, displayName]
      properties:
        id: { $ref: '#/components/schemas/ApiIdentifier' }
        displayName: { type: string, minLength: 1, maxLength: 200 }

    QueryDefinitionSource:
      description: One upstream published call an answer came from.
      type: object
      additionalProperties: false
      required: [endpoint, statisticIds]
      properties:
        endpoint:
          description: The published path, query string included, that returns this result.
          type: string
          minLength: 1
          maxLength: 500
        statisticIds:
          description: >-
            The statistics within that response that answer the question. Empty
            for a leaderboard, because the published leaderboard carries no
            statistic identifier.
          type: array
          items: { $ref: '#/components/schemas/ApiIdentifier' }
          maxItems: 50

    QueryDefinitionResolution:
      description: Every identifier the evaluation resolved, null where a kind has none.
      type: object
      additionalProperties: false
      required: [participantIds, competitionId, seasonId, season]
      properties:
        participantIds:
          type: array
          items: { $ref: '#/components/schemas/ApiIdentifier' }
          maxItems: 2
        competitionId:
          oneOf:
            - $ref: '#/components/schemas/ApiIdentifier'
            - type: 'null'
        seasonId:
          oneOf:
            - $ref: '#/components/schemas/ApiIdentifier'
            - type: 'null'
        season:
          type: [string, 'null']
          minLength: 1

    QueryDefinitionEvaluation:
      oneOf:
        - $ref: '#/components/schemas/QueryDefinitionAnswered'
        - $ref: '#/components/schemas/QueryDefinitionEntityNotFound'
        - $ref: '#/components/schemas/QueryDefinitionEntityAmbiguous'
        - $ref: '#/components/schemas/QueryDefinitionUnsupported'
      discriminator:
        propertyName: outcome
        mapping:
          answered: '#/components/schemas/QueryDefinitionAnswered'
          entity_not_found: '#/components/schemas/QueryDefinitionEntityNotFound'
          entity_ambiguous: '#/components/schemas/QueryDefinitionEntityAmbiguous'
          unsupported: '#/components/schemas/QueryDefinitionUnsupported'

    QueryDefinitionAnswered:
      type: object
      additionalProperties: false
      required: [outcome, definitionVersion, definition, resolved, sources, result]
      properties:
        outcome: { type: string, const: answered }
        definitionVersion: { $ref: '#/components/schemas/QueryDefinitionVersion' }
        definition: { $ref: '#/components/schemas/AnalyticsQueryDefinition' }
        resolved: { $ref: '#/components/schemas/QueryDefinitionResolution' }
        sources:
          description: >-
            One entry per upstream call: one for a leaderboard or a single
            participant, two for a comparison, in the order the definition named
            them.
          type: array
          items: { $ref: '#/components/schemas/QueryDefinitionSource' }
          minItems: 1
          maxItems: 2
        result:
          description: >-
            The published resource, unmodified: a leaderboard, one participant's
            aggregates, or the two being compared in the order named.
          oneOf:
            - $ref: '#/components/schemas/Leaderboard'
            - $ref: '#/components/schemas/ParticipantAggregates'
            - type: array
              items: { $ref: '#/components/schemas/ParticipantAggregates' }
              minItems: 2
              maxItems: 2

    QueryDefinitionEntityNotFound:
      type: object
      additionalProperties: false
      required: [outcome, definitionVersion, definition, reference, nameHint]
      properties:
        outcome: { type: string, const: entity_not_found }
        definitionVersion: { $ref: '#/components/schemas/QueryDefinitionVersion' }
        definition: { $ref: '#/components/schemas/AnalyticsQueryDefinition' }
        reference: { $ref: '#/components/schemas/QueryDefinitionReference' }
        nameHint: { $ref: '#/components/schemas/AnalyticsQueryNameHint' }

    QueryDefinitionEntityAmbiguous:
      type: object
      additionalProperties: false
      required: [outcome, definitionVersion, definition, reference, nameHint, candidates]
      properties:
        outcome: { type: string, const: entity_ambiguous }
        definitionVersion: { $ref: '#/components/schemas/QueryDefinitionVersion' }
        definition: { $ref: '#/components/schemas/AnalyticsQueryDefinition' }
        reference: { $ref: '#/components/schemas/QueryDefinitionReference' }
        nameHint: { $ref: '#/components/schemas/AnalyticsQueryNameHint' }
        candidates:
          type: array
          items: { $ref: '#/components/schemas/QueryDefinitionCandidate' }
          minItems: 2
          maxItems: 5

    QueryDefinitionUnsupported:
      type: object
      additionalProperties: false
      required: [outcome, definitionVersion, definition, reason]
      properties:
        outcome: { type: string, const: unsupported }
        definitionVersion: { $ref: '#/components/schemas/QueryDefinitionVersion' }
        definition: { $ref: '#/components/schemas/AnalyticsQueryDefinition' }
        reason: { $ref: '#/components/schemas/UnsupportedQueryReason' }

    QueryDefinitionEvaluationResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/QueryDefinitionEvaluation' }

    LeaderboardMetric:
      type: string
      enum:
        - most_runs
        - most_wickets
        - most_fours
        - most_sixes
        - highest_batting_average
        - highest_strike_rate
        - best_bowling_average
        - best_economy_rate
        - best_bowling_strike_rate

    LeaderboardQualification:
      oneOf:
        - type: object
          additionalProperties: false
          required: [field, minimum, rationale]
          properties:
            field:
              type: string
              enum: [dismissals, ballsFaced, wicketsTaken, legalBallsBowled]
            minimum:
              type: integer
              minimum: 1
            rationale:
              type: string
              minLength: 1
        - type: 'null'

    LeaderboardEntry:
      type: object
      additionalProperties: false
      required: [rank, participantId, participantName, value]
      properties:
        rank:
          type: integer
          minimum: 1
        participantId:
          $ref: '#/components/schemas/ApiIdentifier'
        participantName:
          type: string
          minLength: 1
        value:
          type: number
          minimum: 0

    Leaderboard:
      type: object
      additionalProperties: false
      allOf:
        - if:
            properties:
              scope:
                const: season
          then:
            required: [seasonId, season]
            properties:
              seasonId:
                $ref: '#/components/schemas/ApiIdentifier'
              season:
                type: string
                minLength: 1
      required:
        - scope
        - competitionId
        - competitionName
        - metric
        - limit
        - qualification
        - tieBreakers
        - entries
      properties:
        scope:
          type: string
          enum: [season, competition]
        seasonId:
          description: Present and required when scope is season.
          $ref: '#/components/schemas/ApiIdentifier'
        season:
          description: Present and required when scope is season.
          type: string
          minLength: 1
        competitionId:
          $ref: '#/components/schemas/ApiIdentifier'
        competitionName:
          type: string
          minLength: 1
        metric:
          $ref: '#/components/schemas/LeaderboardMetric'
        limit:
          type: integer
          minimum: 1
          maximum: 50
        qualification:
          $ref: '#/components/schemas/LeaderboardQualification'
        tieBreakers:
          type: array
          prefixItems:
            - const: metricValue
            - const: participantName
            - const: participantId
          minItems: 3
          maxItems: 3
        entries:
          type: array
          maxItems: 50
          items:
            $ref: '#/components/schemas/LeaderboardEntry'

    LeaderboardResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/Leaderboard'

    CompetitionResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          $ref: '#/components/schemas/Competition'

    CompetitionCollectionResponse:
      type: object
      additionalProperties: false
      required:
        - data
        - pagination
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Competition'
        pagination:
          $ref: '#/components/schemas/PaginationMetadata'

    SeasonResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          $ref: '#/components/schemas/Season'

    SeasonCollectionResponse:
      type: object
      additionalProperties: false
      required:
        - data
        - pagination
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Season'
        pagination:
          $ref: '#/components/schemas/TotalPagesPaginationMetadata'

    FixtureResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          $ref: '#/components/schemas/Fixture'

    FixturePaginationMetadata:
      type: object
      additionalProperties: false
      required:
        - nextCursor
        - totalPages
      properties:
        nextCursor:
          oneOf:
            - type: string
              minLength: 1
            - type: 'null'
        totalPages:
          type: integer
          minimum: 0

    FixtureCollectionResponse:
      type: object
      additionalProperties: false
      required:
        - data
        - pagination
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Fixture'
        pagination:
          $ref: '#/components/schemas/FixturePaginationMetadata'

    PublicEventResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          $ref: '#/components/schemas/PublicEvent'

    PublicEventCollectionResponse:
      type: object
      additionalProperties: false
      required:
        - data
        - pagination
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/PublicEvent'
        pagination:
          $ref: '#/components/schemas/PaginationMetadata'

    PublicEventExportResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/PublicEvent'

    CompetitorResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          $ref: '#/components/schemas/Competitor'

    CompetitorCollectionResponse:
      type: object
      additionalProperties: false
      required:
        - data
        - pagination
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Competitor'
        pagination:
          $ref: '#/components/schemas/PaginationMetadata'

    ParticipantResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          $ref: '#/components/schemas/Participant'

    ParticipantCollectionResponse:
      type: object
      additionalProperties: false
      required:
        - data
        - pagination
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Participant'
        pagination:
          $ref: '#/components/schemas/TotalPagesPaginationMetadata'

    ParticipantFixtureCollectionResponse:
      type: object
      additionalProperties: false
      required:
        - data
        - pagination
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ParticipantFixture'
        pagination:
          $ref: '#/components/schemas/PaginationMetadata'

    BatchReceipt:
      type: object
      additionalProperties: false
      required: [batchReference, status, statusUrl, receivedAt]
      properties:
        batchReference:
          {
            type: string,
            format: uuid,
            description: Opaque receipt reference; never a database ID.,
          }
        status: { type: string, enum: [received, stored] }
        statusUrl: { type: string, pattern: '^/api/v1/batches/' }
        receivedAt: { type: string, format: date-time }

    BatchReceiptResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/BatchReceipt' }

    BatchProgress:
      type: object
      additionalProperties: false
      required: [total, processed, accepted, rejected]
      properties:
        total: { type: integer, minimum: 0 }
        processed: { type: integer, minimum: 0 }
        accepted: { type: integer, minimum: 0 }
        rejected: { type: integer, minimum: 0 }

    BatchCounts:
      type: object
      additionalProperties: false
      required: [accepted, rejected, unresolved, duplicate, conflicting]
      properties:
        accepted: { type: integer, minimum: 0 }
        rejected: { type: integer, minimum: 0 }
        unresolved: { type: integer, minimum: 0 }
        duplicate: { type: integer, minimum: 0 }
        conflicting: { type: integer, minimum: 0 }

    BatchStatus:
      type: object
      additionalProperties: false
      required:
        [
          batchReference,
          competitionId,
          status,
          statusUrl,
          receivedAt,
          updatedAt,
          progress,
          counts,
          source,
          lineage,
          review,
        ]
      properties:
        batchReference: { type: string, format: uuid }
        competitionId: { $ref: '#/components/schemas/DatabaseIdentifier' }
        status:
          type: string
          enum:
            [
              received,
              stored,
              validating,
              rejected,
              awaiting_review,
              correction_requested,
              publishing,
              published,
              partially_published,
              failed,
              superseded,
            ]
        statusUrl: { type: string, pattern: '^/api/v1/batches/' }
        receivedAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        progress: { $ref: '#/components/schemas/BatchProgress' }
        counts: { $ref: '#/components/schemas/BatchCounts' }
        source:
          type: object
          additionalProperties: false
          required: [fileName, checksum, packageVersion, submitter]
          properties:
            fileName: { type: [string, 'null'] }
            checksum: { type: [string, 'null'], pattern: '^[0-9a-f]{64}$' }
            packageVersion: { type: string, minLength: 1 }
            submitter:
              type: object
              additionalProperties: false
              required: [accountId, displayName]
              properties:
                accountId: { $ref: '#/components/schemas/DatabaseIdentifier' }
                displayName: { type: [string, 'null'] }
        lineage:
          type: object
          additionalProperties: false
          required: [replacesBatchReference, supersededByBatchReference]
          properties:
            replacesBatchReference: { type: [string, 'null'], format: uuid }
            supersededByBatchReference: { type: [string, 'null'], format: uuid }
        review:
          oneOf:
            - { $ref: '#/components/schemas/BatchReviewDecision' }
            - { type: 'null' }

    BatchReviewDecision:
      type: object
      additionalProperties: false
      required: [decision, actor, decidedAt, reason]
      properties:
        decision: { type: string, enum: [approved, rejected, returned_for_correction] }
        actor:
          type: object
          additionalProperties: false
          required: [accountId, displayName]
          properties:
            accountId: { $ref: '#/components/schemas/DatabaseIdentifier' }
            displayName: { type: [string, 'null'] }
        decidedAt: { type: string, format: date-time }
        reason:
          type: string
          minLength: 1
          maxLength: 2000
          description: Reviewer-provided audit reason.

    BatchReviewRequest:
      type: object
      additionalProperties: false
      required: [decision, reason]
      properties:
        decision: { type: string, enum: [approved, rejected, returned_for_correction] }
        reason:
          type: string
          minLength: 1
          maxLength: 2000
          description: Required for every decision; rejected and returned_for_correction decisions require at least 10 characters.

    BatchConflictResolutionRequest:
      type: object
      additionalProperties: false
      required: [itemOrdinal, existingDeliveryId, decision, reason]
      properties:
        itemOrdinal: { type: integer, minimum: 0 }
        existingDeliveryId: { $ref: '#/components/schemas/DatabaseIdentifier' }
        decision: { type: string, enum: [use_existing, replace_published] }
        reason:
          type: string
          minLength: 10
          maxLength: 2000
          description: Reviewer explanation retained with the conflict-resolution audit record.

    BatchStatusResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/BatchStatus' }

    ProvenanceActor:
      type: object
      additionalProperties: false
      required: [accountId, displayName]
      properties:
        accountId:
          oneOf:
            - $ref: '#/components/schemas/ApiIdentifier'
            - type: 'null'
        displayName:
          oneOf:
            - { type: string, minLength: 1 }
            - type: 'null'

    ProvenanceSourceMetadata:
      type: object
      additionalProperties: false
      required: [fileName, mediaType, sizeBytes, checksum, packageVersion]
      properties:
        fileName:
          oneOf: [{ type: string, minLength: 1 }, { type: 'null' }]
        mediaType:
          oneOf: [{ type: string, minLength: 1 }, { type: 'null' }]
        sizeBytes:
          oneOf: [{ type: integer, minimum: 1 }, { type: 'null' }]
        checksum:
          oneOf:
            - { type: string, pattern: '^[0-9a-f]{64}$' }
            - type: 'null'
        packageVersion:
          oneOf: [{ type: string, minLength: 1 }, { type: 'null' }]

    ProvenanceDecision:
      type: object
      additionalProperties: false
      required: [decision, actor, decidedAt, reason]
      properties:
        decision:
          type: string
          enum: [accepted, rejected, approved, returned_for_correction]
        actor:
          oneOf:
            - $ref: '#/components/schemas/ProvenanceActor'
            - type: 'null'
        decidedAt: { type: string, format: date-time }
        reason:
          oneOf: [{ type: string, minLength: 1 }, { type: 'null' }]

    ProvenanceLifecycleEntry:
      type: object
      additionalProperties: false
      required: [fromState, toState, at, actorKind, actorIdentifier, reason]
      properties:
        fromState:
          oneOf: [{ type: string, minLength: 1 }, { type: 'null' }]
        toState: { type: string, minLength: 1 }
        at: { type: string, format: date-time }
        actorKind: { type: string, minLength: 1 }
        actorIdentifier: { type: string, minLength: 1 }
        reason: { type: string, minLength: 1 }

    ProvenanceSubmission:
      type: object
      additionalProperties: false
      required:
        [
          kind,
          reference,
          submissionId,
          batchReference,
          fixtureId,
          competitionId,
          submitter,
          status,
          receivedAt,
          updatedAt,
          eventCount,
          source,
        ]
      properties:
        kind: { type: string, enum: [direct, file, batch] }
        reference: { type: string, minLength: 1 }
        submissionId:
          oneOf: [{ $ref: '#/components/schemas/ApiIdentifier' }, { type: 'null' }]
        batchReference:
          oneOf: [{ type: string, format: uuid }, { type: 'null' }]
        fixtureId:
          oneOf: [{ $ref: '#/components/schemas/ApiIdentifier' }, { type: 'null' }]
        competitionId:
          oneOf: [{ $ref: '#/components/schemas/ApiIdentifier' }, { type: 'null' }]
        submitter: { $ref: '#/components/schemas/ProvenanceActor' }
        status: { type: string, minLength: 1 }
        receivedAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        eventCount: { type: integer, minimum: 0 }
        source: { $ref: '#/components/schemas/ProvenanceSourceMetadata' }

    ProvenanceSubmissionDetail:
      description: >-
        A provenance submission with its lifecycle and decisions. Its fields must stay aligned
        with `ProvenanceSubmission`, plus `lifecycle` and `decisions`; the OpenAPI contract tests
        enforce this against real responses.
      type: object
      additionalProperties: false
      required:
        [
          kind,
          reference,
          submissionId,
          batchReference,
          fixtureId,
          competitionId,
          submitter,
          status,
          receivedAt,
          updatedAt,
          eventCount,
          source,
          lifecycle,
          decisions,
        ]
      properties:
        kind: { type: string, enum: [direct, file, batch] }
        reference: { type: string, minLength: 1 }
        submissionId:
          oneOf: [{ $ref: '#/components/schemas/ApiIdentifier' }, { type: 'null' }]
        batchReference:
          oneOf: [{ type: string, format: uuid }, { type: 'null' }]
        fixtureId:
          oneOf: [{ $ref: '#/components/schemas/ApiIdentifier' }, { type: 'null' }]
        competitionId:
          oneOf: [{ $ref: '#/components/schemas/ApiIdentifier' }, { type: 'null' }]
        submitter: { $ref: '#/components/schemas/ProvenanceActor' }
        status: { type: string, minLength: 1 }
        receivedAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        eventCount: { type: integer, minimum: 0 }
        source: { $ref: '#/components/schemas/ProvenanceSourceMetadata' }
        lifecycle:
          type: array
          items: { $ref: '#/components/schemas/ProvenanceLifecycleEntry' }
        decisions:
          type: array
          items: { $ref: '#/components/schemas/ProvenanceDecision' }

    ProvenanceSubmissionListResponse:
      type: object
      additionalProperties: false
      required: [data, pagination]
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/ProvenanceSubmission' }
        pagination: { $ref: '#/components/schemas/PaginationMetadata' }

    ProvenanceSubmissionDetailResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/ProvenanceSubmissionDetail' }

    ProvenanceEventSource:
      type: object
      additionalProperties: false
      required:
        [
          kind,
          reference,
          submissionId,
          batchReference,
          batchItemId,
          submissionEventOrdinal,
          submitter,
          checksum,
          decision,
        ]
      properties:
        kind: { type: string, enum: [direct, file, batch] }
        reference: { type: string, minLength: 1 }
        submissionId: { $ref: '#/components/schemas/ApiIdentifier' }
        batchReference:
          oneOf: [{ type: string, format: uuid }, { type: 'null' }]
        batchItemId:
          oneOf: [{ $ref: '#/components/schemas/ApiIdentifier' }, { type: 'null' }]
        submissionEventOrdinal:
          oneOf: [{ type: integer, minimum: 0 }, { type: 'null' }]
        submitter: { $ref: '#/components/schemas/ProvenanceActor' }
        checksum:
          oneOf:
            - { type: string, pattern: '^[0-9a-f]{64}$' }
            - type: 'null'
        decision:
          oneOf:
            - $ref: '#/components/schemas/ProvenanceDecision'
            - type: 'null'

    ProvenanceCorrection:
      type: object
      additionalProperties: false
      required: [correctionId, requester, correctedAt, reason, review]
      properties:
        correctionId: { $ref: '#/components/schemas/ApiIdentifier' }
        requester: { $ref: '#/components/schemas/ProvenanceActor' }
        correctedAt: { type: string, format: date-time }
        reason: { type: string, minLength: 1 }
        review:
          oneOf:
            - $ref: '#/components/schemas/ProvenanceDecision'
            - type: 'null'

    EventProvenanceRevision:
      type: object
      additionalProperties: false
      required: [deliveryId, revision, recordedAt, supersededAt, current, source, correction]
      properties:
        deliveryId: { $ref: '#/components/schemas/ApiIdentifier' }
        revision: { type: integer, minimum: 1 }
        recordedAt: { type: string, format: date-time }
        supersededAt:
          oneOf: [{ type: string, format: date-time }, { type: 'null' }]
        current: { type: boolean }
        source: { $ref: '#/components/schemas/ProvenanceEventSource' }
        correction:
          oneOf:
            - $ref: '#/components/schemas/ProvenanceCorrection'
            - type: 'null'

    EventProvenance:
      type: object
      additionalProperties: false
      required: [eventId, sourceEventId, fixtureId, competitionId, currentDeliveryId, revisions]
      properties:
        eventId: { $ref: '#/components/schemas/ApiIdentifier' }
        sourceEventId:
          oneOf: [{ type: string, format: uuid }, { type: 'null' }]
        fixtureId: { $ref: '#/components/schemas/ApiIdentifier' }
        competitionId:
          oneOf: [{ $ref: '#/components/schemas/ApiIdentifier' }, { type: 'null' }]
        currentDeliveryId: { $ref: '#/components/schemas/ApiIdentifier' }
        revisions:
          type: array
          minItems: 1
          items: { $ref: '#/components/schemas/EventProvenanceRevision' }

    EventProvenanceResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/EventProvenance' }

    StatisticProvenanceContributor:
      type: object
      additionalProperties: false
      required: [deliveryId, revision, sourceEventId, source]
      properties:
        deliveryId: { $ref: '#/components/schemas/ApiIdentifier' }
        revision: { type: integer, minimum: 1 }
        sourceEventId:
          oneOf: [{ type: string, format: uuid }, { type: 'null' }]
        source: { $ref: '#/components/schemas/ProvenanceEventSource' }

    StatisticProvenance:
      type: object
      additionalProperties: false
      required:
        [
          statisticId,
          fixtureId,
          participantId,
          statisticCode,
          scope,
          sourceEventCount,
          contributors,
          pagination,
        ]
      properties:
        statisticId: { type: string, minLength: 1 }
        fixtureId:
          oneOf: [{ $ref: '#/components/schemas/ApiIdentifier' }, { type: 'null' }]
        participantId:
          oneOf: [{ $ref: '#/components/schemas/ApiIdentifier' }, { type: 'null' }]
        statisticCode: { type: string, minLength: 1 }
        scope: { type: string, minLength: 1 }
        sourceEventCount: { type: integer, minimum: 0 }
        contributors:
          type: array
          items: { $ref: '#/components/schemas/StatisticProvenanceContributor' }
        pagination: { $ref: '#/components/schemas/PaginationMetadata' }

    StatisticProvenanceResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/StatisticProvenance' }

    BatchListResponse:
      type: object
      additionalProperties: false
      required: [data, pagination]
      properties:
        data: { type: array, items: { $ref: '#/components/schemas/BatchStatus' } }
        pagination: { $ref: '#/components/schemas/PaginationMetadata' }

    BatchReportLocation:
      type: object
      additionalProperties: false
      required: [filePath, sheetName, rowNumber, jsonPath, ordinal]
      properties:
        filePath: { type: [string, 'null'] }
        sheetName: { type: [string, 'null'] }
        rowNumber: { type: [integer, 'null'], minimum: 1 }
        jsonPath: { type: [string, 'null'] }
        ordinal: { type: integer, minimum: 0 }

    BatchReportContext:
      type: object
      additionalProperties: false
      required:
        [
          eventReference,
          fixtureId,
          fixtureLabel,
          inningsId,
          overNumber,
          positionInOver,
          description,
        ]
      properties:
        eventReference: { type: [string, 'null'] }
        fixtureId:
          { oneOf: [{ $ref: '#/components/schemas/DatabaseIdentifier' }, { type: 'null' }] }
        fixtureLabel: { type: [string, 'null'], minLength: 1 }
        inningsId:
          { oneOf: [{ $ref: '#/components/schemas/DatabaseIdentifier' }, { type: 'null' }] }
        overNumber: { type: [integer, 'null'], minimum: 0 }
        positionInOver: { type: [integer, 'null'], minimum: 0 }
        description: { type: string, minLength: 1 }

    BatchReportError:
      type: object
      additionalProperties: false
      required: [ruleCode, message, location, context]
      properties:
        ruleCode: { type: string, pattern: '^[A-Z][A-Z0-9_]*$' }
        message: { type: string, minLength: 1 }
        location: { $ref: '#/components/schemas/BatchReportLocation' }
        context: { $ref: '#/components/schemas/BatchReportContext' }

    BatchReferenceCandidate:
      type: object
      additionalProperties: false
      required: [candidateReference, label]
      properties:
        candidateReference:
          { type: string, format: uuid, description: Opaque candidate token; never a database ID. }
        label: { type: string, minLength: 1 }

    BatchReferenceResolution:
      type: object
      additionalProperties: false
      required:
        [referencePath, entityType, state, submittedReference, reason, requiredAction, candidates]
      properties:
        referencePath: { type: string, minLength: 1 }
        entityType: { type: string, enum: [competition, team, fixture, innings, participant] }
        state: { type: string, enum: [ambiguous, unresolved, invalid] }
        submittedReference: {}
        reason: { type: [string, 'null'], minLength: 1 }
        requiredAction:
          type: string
          enum: [select_candidate, contact_reviewer, onboard_participant]
          description: >-
            `onboard_participant` means this reference is waiting on a participant onboarding
            task that an administrator can settle at
            `POST /api/v1/batches/{batchReference}/participants`.
        candidates: { type: array, items: { $ref: '#/components/schemas/BatchReferenceCandidate' } }
        onboardingTask:
          type: object
          additionalProperties: false
          required: [taskReference, reason, candidates]
          description: >-
            Present when `requiredAction` is `onboard_participant`. `taskReference` is the handle
            a decision addresses, and is never derived from anything the decision supplies.
          properties:
            taskReference: { type: string, format: uuid }
            reason:
              type: string
              enum:
                [team_not_recognised, no_durable_identifier, ambiguous_name, identifier_not_found]
            candidates:
              type: array
              items:
                type: object
                additionalProperties: false
                required: [personId, displayName]
                properties:
                  personId: { $ref: '#/components/schemas/DatabaseIdentifier' }
                  displayName: { type: string, minLength: 1 }

    BatchPublishedConflict:
      type: object
      additionalProperties: false
      required: [existingDeliveryId, existingSourceEventId, correctionPermitted, differences]
      properties:
        existingDeliveryId: { $ref: '#/components/schemas/DatabaseIdentifier' }
        existingSourceEventId:
          oneOf:
            - { type: string, format: uuid }
            - { type: 'null' }
        correctionPermitted: { type: boolean }
        differences:
          type: array
          minItems: 1
          items:
            type: object
            additionalProperties: false
            required: [fieldPath, submittedValue, publishedValue]
            properties:
              fieldPath: { type: string, minLength: 1 }
              submittedValue: {}
              publishedValue: {}

    BatchReportItem:
      type: object
      additionalProperties: false
      required:
        [
          ordinal,
          outcome,
          location,
          context,
          stagedRecordId,
          acceptedRecordId,
          operation,
          correctionTarget,
          referenceResolutions,
          errors,
        ]
      properties:
        ordinal: { type: integer, minimum: 0 }
        outcome:
          { type: string, enum: [pending, accepted, rejected, unresolved, duplicate, conflicting] }
        location: { $ref: '#/components/schemas/BatchReportLocation' }
        context: { $ref: '#/components/schemas/BatchReportContext' }
        stagedRecordId:
          { oneOf: [{ $ref: '#/components/schemas/DatabaseIdentifier' }, { type: 'null' }] }
        acceptedRecordId:
          { oneOf: [{ $ref: '#/components/schemas/DatabaseIdentifier' }, { type: 'null' }] }
        operation: { type: string, enum: [upsert, correction] }
        correctionTarget:
          oneOf:
            - type: object
              additionalProperties: false
              required: [sourceEventId, resolvedDeliveryId]
              properties:
                sourceEventId:
                  type: string
                  minLength: 1
                  maxLength: 512
                  description: Exact external delivery identity supplied in correctsEventId.
                resolvedDeliveryId:
                  oneOf:
                    - { $ref: '#/components/schemas/DatabaseIdentifier' }
                    - { type: 'null' }
            - { type: 'null' }
        publishedConflict:
          oneOf:
            - { $ref: '#/components/schemas/BatchPublishedConflict' }
            - { type: 'null' }
        referenceResolutions:
          { type: array, items: { $ref: '#/components/schemas/BatchReferenceResolution' } }
        errors: { type: array, items: { $ref: '#/components/schemas/BatchReportError' } }

    BatchReportRuleGroup:
      type: object
      additionalProperties: false
      required: [ruleCode, count]
      properties:
        ruleCode: { type: string, pattern: '^[A-Z][A-Z0-9_]*$' }
        count: { type: integer, minimum: 1 }

    BatchFixtureSummary:
      type: object
      additionalProperties: false
      required: [fixtureId, label, total, accepted, rejected, unresolved]
      properties:
        fixtureId:
          { oneOf: [{ $ref: '#/components/schemas/DatabaseIdentifier' }, { type: 'null' }] }
        label: { type: string, minLength: 1 }
        total: { type: integer, minimum: 0 }
        accepted: { type: integer, minimum: 0 }
        rejected: { type: integer, minimum: 0 }
        unresolved: { type: integer, minimum: 0 }

    BatchReviewSummary:
      type: object
      additionalProperties: false
      required: [validation, resolution, approvalBlocked, blockingReasons]
      properties:
        validation:
          type: object
          additionalProperties: false
          required: [accepted, rejected, blockingErrors, duplicate, conflicting]
          properties:
            accepted: { type: integer, minimum: 0 }
            rejected: { type: integer, minimum: 0 }
            blockingErrors:
              type: integer
              minimum: 0
              description: Active batch-level or otherwise publishable-item validation errors that prevent approval; ordinary rejected-item errors are excluded.
            duplicate: { type: integer, minimum: 0 }
            conflicting: { type: integer, minimum: 0 }
        resolution:
          type: object
          additionalProperties: false
          required: [resolved, ambiguous, unresolved, invalid, proposed]
          properties:
            resolved: { type: integer, minimum: 0 }
            ambiguous: { type: integer, minimum: 0 }
            unresolved: { type: integer, minimum: 0 }
            invalid: { type: integer, minimum: 0 }
            proposed: { type: integer, minimum: 0 }
        approvalBlocked: { type: boolean }
        blockingReasons: { type: array, items: { type: string, minLength: 1 } }

    BatchParticipantOnboardingTask:
      type: object
      additionalProperties: false
      required:
        [taskReference, fixtureId, submittedName, submittedTeamName, reason, candidates, teams]
      description: >-
        One outstanding participant onboarding task, listed once for the batch. The same task
        also appears as an `onboard_participant` action on every reference waiting on it, which
        for a player named in three hundred deliveries is three hundred appearances of one
        decision. This list is that work deduplicated.
      properties:
        taskReference: { type: string, format: uuid }
        fixtureId: { $ref: '#/components/schemas/DatabaseIdentifier' }
        submittedName: { type: string, minLength: 1 }
        submittedTeamName: { type: [string, 'null'], minLength: 1 }
        reason:
          type: string
          enum: [team_not_recognised, no_durable_identifier, ambiguous_name, identifier_not_found]
        candidates:
          type: array
          items:
            type: object
            additionalProperties: false
            required: [personId, displayName]
            properties:
              personId: { $ref: '#/components/schemas/DatabaseIdentifier' }
              displayName: { type: string, minLength: 1 }
        teams:
          type: array
          description: >-
            The two teams of this task's fixture, in fixture order. A team decision is checked
            by exact name against these, so without them a reviewer answering
            `team_not_recognised` would be typing a name the platform already knows and could
            simply have offered. Listing them makes that answer a choice between two.
          items:
            type: object
            additionalProperties: false
            required: [teamId, name]
            properties:
              teamId: { $ref: '#/components/schemas/DatabaseIdentifier' }
              name: { type: string, minLength: 1 }

    BatchReport:
      type: object
      additionalProperties: false
      required:
        [
          batch,
          errorGroups,
          reviewSummary,
          fixtureSummaries,
          participantOnboarding,
          acceptedSamples,
          blockingItems,
          items,
          pagination,
          downloadUrl,
        ]
      properties:
        batch: { $ref: '#/components/schemas/BatchStatus' }
        errorGroups: { type: array, items: { $ref: '#/components/schemas/BatchReportRuleGroup' } }
        reviewSummary: { $ref: '#/components/schemas/BatchReviewSummary' }
        fixtureSummaries:
          { type: array, items: { $ref: '#/components/schemas/BatchFixtureSummary' } }
        participantOnboarding:
          type: array
          items: { $ref: '#/components/schemas/BatchParticipantOnboardingTask' }
        acceptedSamples:
          { type: array, maxItems: 15, items: { $ref: '#/components/schemas/BatchReportItem' } }
        blockingItems:
          type: array
          description: Approval-blocking reference and conflict items selected independently of ordinary report pagination.
          items: { $ref: '#/components/schemas/BatchReportItem' }
        items: { type: array, items: { $ref: '#/components/schemas/BatchReportItem' } }
        pagination: { $ref: '#/components/schemas/PaginationMetadata' }
        downloadUrl: { type: string, pattern: '^/api/v1/batches/' }

    BatchReportResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/BatchReport' }

    BatchReportDownload:
      type: object
      additionalProperties: false
      required:
        [
          batch,
          errorGroups,
          reviewSummary,
          fixtureSummaries,
          participantOnboarding,
          acceptedSamples,
          items,
        ]
      properties:
        batch: { $ref: '#/components/schemas/BatchStatus' }
        errorGroups: { type: array, items: { $ref: '#/components/schemas/BatchReportRuleGroup' } }
        reviewSummary: { $ref: '#/components/schemas/BatchReviewSummary' }
        fixtureSummaries:
          { type: array, items: { $ref: '#/components/schemas/BatchFixtureSummary' } }
        participantOnboarding:
          type: array
          items: { $ref: '#/components/schemas/BatchParticipantOnboardingTask' }
        acceptedSamples:
          { type: array, maxItems: 15, items: { $ref: '#/components/schemas/BatchReportItem' } }
        items: { type: array, items: { $ref: '#/components/schemas/BatchReportItem' } }

    BatchReportDownloadResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/BatchReportDownload' }

    BatchReferenceMappingRequest:
      type: object
      additionalProperties: false
      required: [itemOrdinal, referencePath, candidateReference, decisionKey]
      properties:
        itemOrdinal: { type: integer, minimum: 0 }
        referencePath: { type: string, minLength: 1, maxLength: 1000 }
        candidateReference: { type: string, format: uuid }
        decisionKey: { type: string, minLength: 1, maxLength: 255 }

    BatchCanonicalFixtureRequest:
      type: object
      additionalProperties: false
      required: [itemOrdinal, referencePath, decisionKey]
      properties:
        itemOrdinal: { type: integer, minimum: 0 }
        referencePath: { type: string, minLength: 1, maxLength: 1000 }
        decisionKey: { type: string, minLength: 1, maxLength: 255 }

    BatchParticipantOnboardingDecision:
      type: object
      additionalProperties: false
      required: [taskReference]
      oneOf:
        - required: [personId]
        - required: [sourceId]
      description: >-
        One decision settling one outstanding task. It answers two separate questions, and they
        are not interchangeable. Who the participant is comes from exactly one of `personId` or
        `sourceId`; two identities for one participant have no meaning, so both together are
        refused. Which of the fixture's two teams they belong to comes from the optional
        `teamName`, needed when the submission carried a team the fixture does not recognise.
        A `team_not_recognised` task needs both at once, because the task holds no identity of
        its own and neither half can be inferred from the other; `teamName` alone is therefore
        refused. The task is addressed by `taskReference` and never by anything derivable from
        the answer: supplying an identifier or a team changes what the task's participant key
        would derive to, so a re-derived handle would match no task.
      properties:
        taskReference: { type: string, format: uuid }
        personId:
          allOf: [{ $ref: '#/components/schemas/DatabaseIdentifier' }]
          description: One of the candidates the task offered. Any other person is refused.
        sourceId:
          type: string
          description: >-
            A durable registry identifier, `namespace:entityType:value`. A `cricsheet`
            participant is created or reused by source reference; an `app` participant must
            already exist.
        teamName:
          type: string
          minLength: 1
          maxLength: 255
          description: One of the fixture's two teams. Any other team is refused.

    BatchParticipantOnboardingRequest:
      type: object
      additionalProperties: false
      required: [decisionKey, decisions]
      properties:
        decisionKey: { type: string, minLength: 1, maxLength: 255 }
        decisions:
          type: array
          minItems: 1
          maxItems: 200
          items: { $ref: '#/components/schemas/BatchParticipantOnboardingDecision' }

    BatchParticipantOnboardingReceipt:
      type: object
      additionalProperties: false
      required:
        [
          batchReference,
          decisionReference,
          status,
          statusUrl,
          submittedAt,
          onboarded,
          alreadyOnboarded,
          revalidationQueued,
        ]
      properties:
        batchReference: { type: string, format: uuid }
        decisionReference: { type: string, format: uuid }
        status: { type: string, enum: [queued, applied] }
        statusUrl: { type: string, pattern: '^/api/v1/batches/' }
        submittedAt: { type: string, format: date-time }
        onboarded:
          type: integer
          minimum: 0
          description: Tasks this request settled.
        alreadyOnboarded:
          type: integer
          minimum: 0
          description: Tasks already settled when it arrived. A replay reports its whole result here.
        revalidationQueued:
          type: boolean
          description: False when nothing changed, because then there is nothing to revalidate.

    BatchParticipantOnboardingResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/BatchParticipantOnboardingReceipt' }

    BatchFixtureOnboardingUnresolvedParticipant:
      type: object
      additionalProperties: false
      required: [name, reason, candidates]
      description: >-
        A participant named by the batch that could not be added to the new fixture's squad.
        A participant is never matched on a name alone, so each of these is a decision for a
        reviewer rather than a match the platform declined to guess at.
      properties:
        name: { type: string, minLength: 1 }
        teamName: { type: string, minLength: 1 }
        reason:
          type: string
          description: >-
            Which decision the reviewer has to make. `team_not_recognised`: the submitted team is
            missing or is not one of the fixture's two teams, so the team must be chosen.
            `no_durable_identifier`: no source identifier was submitted and at most one existing
            person carries the name, so a registry identifier must be supplied or the single
            candidate confirmed. `ambiguous_name`: the name belongs to more than one existing
            person, so a candidate must be chosen. `identifier_not_found`: an application
            identifier was supplied but names no existing person.
          enum: [team_not_recognised, no_durable_identifier, ambiguous_name, identifier_not_found]
        candidates:
          type: array
          description: Existing people sharing the submitted name, by display name or alias.
          items:
            type: object
            additionalProperties: false
            required: [personId, displayName]
            properties:
              personId: { $ref: '#/components/schemas/DatabaseIdentifier' }
              displayName: { type: string, minLength: 1 }

    BatchFixtureOnboardingSummary:
      type: object
      additionalProperties: false
      required: [inningsCreated, squadCreated, unresolvedParticipants]
      description: >-
        What creating the canonical fixture onboarded deterministically, and what it could not.
        A reviewer-approved proposal carries fixture-level facts only, so the innings and squad
        the fixture needs are recovered from the deliveries that named them.
      properties:
        inningsCreated: { type: integer, minimum: 0 }
        squadCreated: { type: integer, minimum: 0 }
        unresolvedParticipants:
          type: array
          items: { $ref: '#/components/schemas/BatchFixtureOnboardingUnresolvedParticipant' }

    BatchReferenceMappingReceipt:
      type: object
      additionalProperties: false
      required: [batchReference, decisionReference, status, statusUrl, submittedAt]
      properties:
        batchReference: { type: string, format: uuid }
        decisionReference: { type: string, format: uuid }
        status: { type: string, enum: [queued, applied] }
        statusUrl: { type: string, pattern: '^/api/v1/batches/' }
        submittedAt: { type: string, format: date-time }
        onboarding:
          allOf: [{ $ref: '#/components/schemas/BatchFixtureOnboardingSummary' }]
          description: >-
            Present on a canonical fixture decision. A repeat decision for a fixture that already
            exists reports what it was able to add this time and what still needs a reviewer, so
            the counts describe that decision rather than the fixture's whole history. Absent on
            an ordinary reference mapping, which onboards nothing.

    BatchReferenceMappingResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/BatchReferenceMappingReceipt' }

    SubmissionFielder:
      type: object
      additionalProperties: false
      properties:
        participantId:
          $ref: '#/components/schemas/DatabaseIdentifier'
        substitute:
          type: boolean
          default: false

    SubmissionWicket:
      type: object
      additionalProperties: false
      required:
        - kind
        - playerOutId
      properties:
        kind:
          type: string
          enum:
            - caught
            - bowled
            - lbw
            - stumped
            - caught and bowled
            - hit wicket
            - run out
            - obstructing the field
            - timed out
            - hit the ball twice
            - handled the ball
            - retired hurt
            - retired out
            - retired not out
        playerOutId:
          $ref: '#/components/schemas/DatabaseIdentifier'
        fielders:
          type: array
          maxItems: 11
          default: []
          items:
            $ref: '#/components/schemas/SubmissionFielder'

    SubmissionEvent:
      type: object
      description: A single ordered cricket delivery event, not a statistic total.
      additionalProperties: false
      required:
        - eventId
        - inningsId
        - sequenceNumber
        - overNumber
        - positionInOver
        - strikerId
        - nonStrikerId
        - bowlerId
        - runs
      properties:
        eventId:
          type: string
          format: uuid
          description: Globally unique client-generated identifier used to detect retries.
        inningsId:
          $ref: '#/components/schemas/DatabaseIdentifier'
        sequenceNumber:
          type: integer
          minimum: 1
          maximum: 2147483647
        overNumber:
          type: integer
          minimum: 0
          maximum: 32767
        positionInOver:
          type: integer
          minimum: 0
          maximum: 32767
          description: Zero-based position in the over; unlike the display ball number, this cannot repeat.
        ballNumber:
          type: string
          maxLength: 32
          pattern: '^\d{1,5}\.\d{1,2}$'
          description: Optional display label; its over component must match overNumber.
        strikerId:
          $ref: '#/components/schemas/DatabaseIdentifier'
        nonStrikerId:
          $ref: '#/components/schemas/DatabaseIdentifier'
        bowlerId:
          $ref: '#/components/schemas/DatabaseIdentifier'
        runs:
          type: object
          additionalProperties: false
          required:
            - offBat
            - extras
            - total
          properties:
            offBat:
              type: integer
              minimum: 0
              maximum: 32767
            extras:
              type: integer
              minimum: 0
              maximum: 32767
            total:
              type: integer
              minimum: 0
              maximum: 32767
            nonBoundary:
              type: boolean
              default: false
        extras:
          type: object
          additionalProperties: false
          default: {}
          properties:
            wides:
              type: integer
              minimum: 0
              maximum: 32767
            noBalls:
              type: integer
              minimum: 0
              maximum: 32767
            byes:
              type: integer
              minimum: 0
              maximum: 32767
            legByes:
              type: integer
              minimum: 0
              maximum: 32767
            penalty:
              type: integer
              minimum: 0
              maximum: 32767
        wickets:
          type: array
          maxItems: 2
          default: []
          items:
            $ref: '#/components/schemas/SubmissionWicket'

    SubmissionRequest:
      type: object
      additionalProperties: false
      required:
        - fixtureId
        - schemaVersion
        - events
      properties:
        fixtureId:
          $ref: '#/components/schemas/DatabaseIdentifier'

        schemaVersion:
          type: string
          const: '1.0'

        events:
          type: array
          description: Events for each innings must appear in ascending sequenceNumber order.
          minItems: 1
          maxItems: 1000
          items:
            $ref: '#/components/schemas/SubmissionEvent'

    Submission:
      type: object
      additionalProperties: false
      required:
        - submissionId
        - fixtureId
        - submitterId
        - status
        - receivedAt
        - schemaVersion
        - eventCount
      properties:
        submissionId:
          $ref: '#/components/schemas/ApiIdentifier'
        fixtureId:
          $ref: '#/components/schemas/ApiIdentifier'
        submitterId:
          $ref: '#/components/schemas/ApiIdentifier'
        status:
          type: string
          const: accepted
        receivedAt:
          type: string
          format: date-time
        schemaVersion:
          type: string
          const: '1.0'
        eventCount:
          type: integer
          minimum: 1
        checksum:
          type: string
          pattern: '^[0-9a-f]{64}$'
          description: SHA-256 of the validated direct payload or original uploaded file bytes.
        sourceFile:
          $ref: '#/components/schemas/SubmissionSourceFile'

    SubmissionSourceFile:
      type: object
      additionalProperties: false
      required:
        - fileName
        - mediaType
        - sizeBytes
      properties:
        fileName:
          type: string
          minLength: 1
          maxLength: 255
        mediaType:
          type: string
          enum: [application/json, text/csv]
        sizeBytes:
          type: integer
          minimum: 1
          maximum: 1000000

    SubmissionUploadRequest:
      type: object
      additionalProperties: false
      required:
        - file
      properties:
        file:
          type: string
          format: binary
          description: A .json application/json file or .csv text/csv file, at most 1 MB.

    SubmissionResponse:
      type: object
      additionalProperties: false
      required:
        - data
      properties:
        data:
          $ref: '#/components/schemas/Submission'

    CorrectionEvent:
      type: object
      description: Corrected delivery content. The source event ID and occurrence sequence are inherited from the target event.
      additionalProperties: false
      required:
        - inningsId
        - overNumber
        - positionInOver
        - strikerId
        - nonStrikerId
        - bowlerId
        - runs
      properties:
        inningsId:
          $ref: '#/components/schemas/DatabaseIdentifier'
        overNumber:
          type: integer
          minimum: 0
          maximum: 32767
        positionInOver:
          type: integer
          minimum: 0
          maximum: 32767
        ballNumber:
          type: string
          maxLength: 32
          pattern: '^\d{1,5}\.\d{1,2}$'
          description: Optional display label; its over component must match overNumber.
        strikerId:
          $ref: '#/components/schemas/DatabaseIdentifier'
        nonStrikerId:
          $ref: '#/components/schemas/DatabaseIdentifier'
        bowlerId:
          $ref: '#/components/schemas/DatabaseIdentifier'
        runs:
          type: object
          additionalProperties: false
          required: [offBat, extras, total]
          properties:
            offBat: { type: integer, minimum: 0, maximum: 32767 }
            extras: { type: integer, minimum: 0, maximum: 32767 }
            total: { type: integer, minimum: 0, maximum: 32767 }
            nonBoundary: { type: boolean, default: false }
        extras:
          type: object
          additionalProperties: false
          default: {}
          properties:
            wides: { type: integer, minimum: 0, maximum: 32767 }
            noBalls: { type: integer, minimum: 0, maximum: 32767 }
            byes: { type: integer, minimum: 0, maximum: 32767 }
            legByes: { type: integer, minimum: 0, maximum: 32767 }
            penalty: { type: integer, minimum: 0, maximum: 32767 }
        wickets:
          type: array
          maxItems: 2
          default: []
          items:
            $ref: '#/components/schemas/SubmissionWicket'

    CorrectionRequest:
      type: object
      additionalProperties: false
      required: [fixtureId, schemaVersion, reason, event]
      properties:
        fixtureId:
          $ref: '#/components/schemas/DatabaseIdentifier'
        schemaVersion:
          type: string
          const: '1.0'
        reason:
          type: string
          minLength: 1
          maxLength: 1000
        event:
          $ref: '#/components/schemas/CorrectionEvent'

    Correction:
      type: object
      additionalProperties: false
      required: [eventId, fixtureId, revision, refreshedScopes]
      properties:
        eventId:
          type: string
          format: uuid
        fixtureId:
          $ref: '#/components/schemas/ApiIdentifier'
        revision:
          type: integer
          minimum: 1
        refreshedScopes:
          type: array
          description: Exact derived-statistic scopes whose live inputs changed with this correction.
          items:
            type: object
            additionalProperties: false
            required: [scope, participantId, competitionId, season]
            properties:
              scope:
                type: string
                enum: [fixture, season, competition, career]
              participantId:
                oneOf:
                  - $ref: '#/components/schemas/ApiIdentifier'
                  - type: 'null'
              competitionId:
                oneOf:
                  - $ref: '#/components/schemas/ApiIdentifier'
                  - type: 'null'
              season:
                oneOf:
                  - type: string
                    minLength: 1
                  - type: 'null'

    CorrectionResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/Correction'

    CorrectionHistoryEntry:
      type: object
      additionalProperties: false
      required:
        [
          correctionId,
          previousDeliveryId,
          replacementDeliveryId,
          previousRevision,
          resultingRevision,
          requester,
          correctedAt,
          reason,
          source,
          previousState,
          resultingState,
          review,
        ]
      properties:
        correctionId: { $ref: '#/components/schemas/ApiIdentifier' }
        previousDeliveryId: { $ref: '#/components/schemas/ApiIdentifier' }
        replacementDeliveryId: { $ref: '#/components/schemas/ApiIdentifier' }
        previousRevision: { type: integer, minimum: 1 }
        resultingRevision: { type: integer, minimum: 1 }
        requester:
          type: object
          additionalProperties: false
          required: [accountId, displayName]
          properties:
            accountId: { $ref: '#/components/schemas/ApiIdentifier' }
            displayName: { type: [string, 'null'] }
        correctedAt: { type: string, format: date-time }
        reason: { type: string, minLength: 1 }
        source:
          type: object
          additionalProperties: false
          required: [submissionId, submissionEventOrdinal, batchItemId]
          properties:
            submissionId: { $ref: '#/components/schemas/ApiIdentifier' }
            submissionEventOrdinal: { type: [integer, 'null'], minimum: 0 }
            batchItemId:
              oneOf:
                - { $ref: '#/components/schemas/ApiIdentifier' }
                - { type: 'null' }
        previousState: { $ref: '#/components/schemas/SubmissionEvent' }
        resultingState: { $ref: '#/components/schemas/SubmissionEvent' }
        review:
          oneOf:
            - type: object
              additionalProperties: false
              required: [reviewer, decision, reviewedAt, reason]
              properties:
                reviewer:
                  type: object
                  additionalProperties: false
                  required: [accountId, displayName]
                  properties:
                    accountId: { $ref: '#/components/schemas/ApiIdentifier' }
                    displayName: { type: [string, 'null'] }
                decision: { type: string, enum: [approved, rejected] }
                reviewedAt: { type: string, format: date-time }
                reason: { type: string, minLength: 1 }
            - { type: 'null' }

    CorrectionHistoryResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [eventId, fixtureId, corrections]
          properties:
            eventId: { type: string, format: uuid }
            fixtureId: { $ref: '#/components/schemas/ApiIdentifier' }
            corrections:
              type: array
              items: { $ref: '#/components/schemas/CorrectionHistoryEntry' }
