openapi: 3.1.0
info:
  title: CodeVault REST API
  version: "2.6.0"
  summary: Programmatic access to a CodeVault instance.
  description: |
    The `/api/v1/*` REST surface for managing repositories, branches, issues,
    pull requests, commit statuses, reactions, the roadmap (milestones,
    releases), community collaboration (feature requests, polls), notifications,
    and the user's contribution heatmap. Authentication is by Personal Access Token issued at
    `/settings/tokens`, presented either as `Authorization: Bearer <token>`
    or as the password half of HTTP Basic auth (`username:token`).

    This document covers the JSON REST endpoints under `/api/v1/`. Other
    CodeVault interfaces follow standard external specs and are not described
    here:

    * **Git Smart HTTP** (`/info/refs`, `/git-upload-pack`, `/git-receive-pack`)
      — see the Git protocol documentation.
    * **Git LFS** (`/info/lfs/objects/*`) — see the Git LFS Batch API spec.
    * **Docker registry v2** (`/v2/*`) — see the OCI Distribution spec.
    * **npm / PyPI registries** — provider-specific conventions.

    Tokens with the legacy scope `repo` (or new `repo:write`) can perform
    write operations. Tokens scoped `repo:read` are denied for any state-
    changing endpoint with HTTP `403`.
  contact:
    name: CodeVault
    url: https://codevault.example/
  license:
    name: MPL-2.0

servers:
  - url: "{baseUrl}"
    description: Your CodeVault instance
    variables:
      baseUrl:
        default: https://codevault.example
        description: Root URL of your CodeVault installation, with no trailing slash.

security:
  - BearerToken: []
  - BasicAuth: []

tags:
  - name: User
    description: The authenticated user and other user profiles.
  - name: Repositories
    description: Repository metadata and branches.
  - name: Issues
    description: Issue tracker.
  - name: Pull Requests
    description: Pull request lifecycle.
  - name: Commit Status
    description: GitHub-compatible commit-status checks (used by external CI).
  - name: Reactions
    description: Emoji reactions on issues, PRs, and comments.
  - name: Projects
    description: Project board card management.
  - name: Activity
    description: The authenticated user's contribution heatmap and per-day activity.
  - name: Roadmap
    description: Milestones as roadmap items, releases, and org-wide roadmaps.
  - name: Community
    description: Feature requests and polls, gated by per-repo community capabilities.
  - name: Notifications
    description: The authenticated user's notifications.
  - name: Search
    description: Global command-palette search.
  - name: Security
    description: Dependency advisories, autofix, provenance, and the notary key.
  - name: Deployments
    description: Deployment pipeline configuration and rollout runs.
  - name: Encryption
    description: Per-repository encryption configuration and keys.
  - name: Packages
    description: Package registry upload/download (Generic, npm, PyPI, Go).

paths:
  /api/v1/user:
    get:
      summary: Get the authenticated user
      operationId: getCurrentUser
      tags: [User]
      responses:
        "200":
          description: The user the supplied token belongs to.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CurrentUser" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /api/v1/users/{username}:
    parameters:
      - $ref: "#/components/parameters/Username"
    get:
      summary: Get a user profile
      operationId: getUser
      tags: [User]
      security: []  # public, no auth required
      responses:
        "200":
          description: The user's public profile.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserProfile" }
        "404": { $ref: "#/components/responses/NotFound" }

  /api/v1/repos/{owner}/{repo}:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      summary: Get repository details
      operationId: getRepo
      tags: [Repositories]
      responses:
        "200":
          description: Repository metadata.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Repository" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /api/v1/repos/{owner}/{repo}/branches:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      summary: List branches
      operationId: listBranches
      tags: [Repositories]
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: Page of branches. Pagination via `X-Total-Count` and `Link`.
          headers:
            X-Total-Count: { $ref: "#/components/headers/XTotalCount" }
            Link: { $ref: "#/components/headers/Link" }
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Branch" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      summary: Create a branch
      operationId: createBranch
      tags: [Repositories]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateBranch" }
      responses:
        "201":
          description: Branch created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Branch" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409":
          description: Branch already exists or source ref invalid.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /api/v1/repos/{owner}/{repo}/branches/{branch}:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - name: branch
        in: path
        required: true
        schema: { type: string }
        description: Branch name.
    delete:
      summary: Delete a branch
      operationId: deleteBranch
      tags: [Repositories]
      responses:
        "204":
          description: Branch deleted.
        "400":
          description: Cannot delete (e.g., default branch, branch has open PRs).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /api/v1/repos/{owner}/{repo}/issues:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      summary: List issues
      operationId: listIssues
      tags: [Issues]
      parameters:
        - name: state
          in: query
          schema:
            type: string
            enum: [open, closed]
            default: open
          description: Filter by state. Defaults to `open`.
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: Page of issues.
          headers:
            X-Total-Count: { $ref: "#/components/headers/XTotalCount" }
            Link: { $ref: "#/components/headers/Link" }
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/IssueSummary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      summary: Create an issue
      operationId: createIssue
      tags: [Issues]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateIssue" }
      responses:
        "201":
          description: Issue created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/IssueCreated" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/v1/repos/{owner}/{repo}/issues/{number}:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - $ref: "#/components/parameters/Number"
    get:
      summary: Get an issue with comments
      operationId: getIssue
      tags: [Issues]
      responses:
        "200":
          description: Issue including comments.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Issue" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /api/v1/repos/{owner}/{repo}/issues/{number}/comments:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - $ref: "#/components/parameters/Number"
    post:
      summary: Add a comment to an issue
      operationId: commentOnIssue
      tags: [Issues]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateComment" }
      responses:
        "201":
          description: Comment created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CommentCreated" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /api/v1/repos/{owner}/{repo}/pulls:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      summary: List pull requests
      operationId: listPullRequests
      tags: [Pull Requests]
      parameters:
        - name: state
          in: query
          schema:
            type: string
            enum: [open, closed, merged]
            default: open
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: Page of pull requests.
          headers:
            X-Total-Count: { $ref: "#/components/headers/XTotalCount" }
            Link: { $ref: "#/components/headers/Link" }
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/PullRequestSummary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      summary: Create a pull request
      operationId: createPullRequest
      tags: [Pull Requests]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreatePullRequest" }
      responses:
        "201":
          description: Pull request created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PullRequestCreated" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/v1/repos/{owner}/{repo}/pulls/{number}:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - $ref: "#/components/parameters/Number"
    get:
      summary: Get a pull request with comments and reviews
      operationId: getPullRequest
      tags: [Pull Requests]
      responses:
        "200":
          description: Pull request including review and comment threads.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PullRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /api/v1/repos/{owner}/{repo}/statuses/{sha}:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - name: sha
        in: path
        required: true
        schema: { type: string, pattern: "^[a-f0-9]{4,64}$" }
        description: Commit SHA (full or abbreviated).
    post:
      summary: Create or update a commit status
      operationId: createCommitStatus
      tags: [Commit Status]
      description: |
        GitHub-compatible commit-status writer. External CI systems POST a
        status here for each commit they evaluate; the most-recent status
        per `(sha, context)` pair wins. Branch protection's "require
        status checks" gate reads the combined state from the matching
        GET endpoint.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateCommitStatus" }
      responses:
        "201":
          description: Status recorded.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CommitStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    get:
      summary: List commit statuses
      operationId: getCommitStatuses
      tags: [Commit Status]
      responses:
        "200":
          description: All statuses for the SHA plus a combined `state` field.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CommitStatusList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /api/v1/reactions:
    post:
      summary: Toggle a reaction
      operationId: toggleReaction
      tags: [Reactions]
      description: |
        Idempotently adds or removes a single emoji reaction on an issue,
        pull request, or comment. Calling twice from the same user with
        the same body removes the reaction.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ToggleReaction" }
      responses:
        "200":
          description: Updated reaction counts for the target.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: { type: integer }
                example:
                  thumbsup: 3
                  heart: 1
                  rocket: 0
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /api/v1/projects/cards/{cardID}/move:
    parameters:
      - name: cardID
        in: path
        required: true
        schema: { type: integer, format: int64 }
        description: Project card identifier.
    post:
      summary: Move a project card
      operationId: moveProjectCard
      tags: [Projects]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/MoveCard" }
      responses:
        "200":
          description: Card moved.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /api/v1/heatmap:
    get:
      summary: Authenticated user's contribution heatmap
      operationId: getHeatmap
      tags: [Activity]
      description: |
        Returns one entry per day for the past year. Authentication is via
        the session cookie (this endpoint is consumed by the dashboard's
        SVG heatmap), not by Bearer token.
      security: []
      responses:
        "200":
          description: Heatmap days (sparse — only days with activity).
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/HeatmapDay" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /api/v1/heatmap/{date}:
    parameters:
      - name: date
        in: path
        required: true
        schema:
          type: string
          format: date
          pattern: "^\\d{4}-\\d{2}-\\d{2}$"
        description: ISO-8601 date (YYYY-MM-DD).
    get:
      summary: Activity for a single day
      operationId: getHeatmapDay
      tags: [Activity]
      security: []
      responses:
        "200":
          description: List of activity entries for that day.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ActivityEntry" }
        "400":
          description: Invalid date format.
        "401": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/user/repos:
    get:
      operationId: listUserRepos
      summary: Repositories the authenticated user can access.
      tags: [Repositories]
      responses:
        "200":
          description: Accessible repositories.
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/Repository" } }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/repos/{owner}/{repo}/roadmap:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      operationId: getRoadmap
      summary: A repository's milestones as roadmap items.
      tags: [Roadmap]
      security: []
      responses:
        "200":
          description: Roadmap milestones.
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/Milestone" } }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/repos/{owner}/{repo}/milestones:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      operationId: listMilestones
      summary: List milestones.
      tags: [Roadmap]
      security: []
      responses:
        "200":
          description: Milestones.
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/Milestone" } }
    post:
      operationId: createMilestone
      summary: Create a milestone (write scope).
      tags: [Roadmap]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title]
              properties:
                title: { type: string }
                description: { type: string }
                due_date: { type: string, description: "RFC3339 or YYYY-MM-DD" }
                state: { type: string, enum: [planned, in_progress, shipped, cancelled] }
      responses:
        "201":
          description: Created milestone.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Milestone" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/v1/repos/{owner}/{repo}/milestones/{id}:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: id, in: path, required: true, schema: { type: integer } }
    patch:
      operationId: updateMilestone
      summary: Update a milestone's roadmap fields (write scope).
      tags: [Roadmap]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                state: { type: string, enum: [planned, in_progress, shipped, cancelled] }
                release_tag: { type: string }
                sort_order: { type: integer }
      responses:
        "200":
          description: Updated milestone.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Milestone" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/repos/{owner}/{repo}/releases:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      operationId: listReleases
      summary: List a repository's releases.
      tags: [Roadmap]
      security: []
      responses:
        "200":
          description: Releases.
          content:
            application/json:
              schema: { type: array, items: { type: object, additionalProperties: true } }
  /api/v1/orgs/{org}/roadmap:
    parameters:
      - { name: org, in: path, required: true, schema: { type: string } }
    get:
      operationId: getOrgRoadmap
      summary: Aggregate roadmap across an organization's repositories.
      tags: [Roadmap]
      security: []
      responses:
        "200":
          description: Milestones across the org (private repos only for members).
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/Milestone" } }
  /api/v1/repos/{owner}/{repo}/feature-requests:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      operationId: listFeatureRequests
      summary: List feature requests, most-voted first.
      tags: [Community]
      security: []
      parameters:
        - { name: state, in: query, schema: { type: string, enum: [open, closed] } }
      responses:
        "200":
          description: Feature requests.
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/FeatureRequest" } }
    post:
      operationId: createFeatureRequest
      summary: Suggest a feature (requires the community 'suggest' capability).
      tags: [Community]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title]
              properties:
                title: { type: string }
                body: { type: string }
      responses:
        "201": { description: Created feature request. }
        "403": { description: Not permitted to suggest on this repository. }
  /api/v1/repos/{owner}/{repo}/feature-requests/{number}/vote:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: number, in: path, required: true, schema: { type: integer } }
    post:
      operationId: voteFeatureRequest
      summary: Toggle your upvote (requires the community 'vote' capability).
      tags: [Community]
      responses:
        "200":
          description: New vote count.
          content:
            application/json:
              schema: { type: object, properties: { number: { type: integer }, votes: { type: integer } } }
        "403": { description: Not permitted to vote on this repository. }
  /api/v1/repos/{owner}/{repo}/polls:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      operationId: listPolls
      summary: List polls with live tallies and the caller's own choices.
      tags: [Community]
      security: []
      parameters:
        - { name: state, in: query, schema: { type: string, enum: [closed] } }
      responses:
        "200":
          description: Polls.
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/Poll" } }
  /api/v1/repos/{owner}/{repo}/polls/{id}/vote:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: id, in: path, required: true, schema: { type: integer } }
    post:
      operationId: votePoll
      summary: Cast a poll vote (requires 'vote'); replaces any prior choice.
      tags: [Community]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                options: { type: array, items: { type: integer } }
      responses:
        "200":
          description: Updated poll.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Poll" }
        "403": { description: Poll closed or not permitted. }
  /api/v1/notifications:
    get:
      operationId: listNotifications
      summary: List the authenticated user's notifications.
      tags: [Notifications]
      parameters:
        - { name: unread, in: query, schema: { type: boolean } }
        - { name: limit, in: query, schema: { type: integer } }
      responses:
        "200": { description: Notifications. }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/notifications/{id}/read:
    parameters:
      - { name: id, in: path, required: true, schema: { type: integer } }
    post:
      operationId: markNotificationRead
      summary: Mark one notification read.
      tags: [Notifications]
      responses:
        "200": { description: Marked read. }
  /api/v1/notifications/read:
    post:
      operationId: markAllNotificationsRead
      summary: Mark all notifications read.
      tags: [Notifications]
      responses:
        "200": { description: All marked read. }
  /api/v1/palette:
    get:
      operationId: quickPalette
      summary: Command-palette search across repos, users, issues, and code.
      tags: [Search]
      parameters:
        - { name: q, in: query, required: true, schema: { type: string } }
      responses:
        "200": { description: Grouped search results. }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/notary-key:
    get:
      operationId: getNotaryKey
      summary: The instance's cosign/notary Ed25519 public key (PKIX PEM).
      tags: [Security]
      security: []
      responses:
        "200": { description: PEM-encoded public key. }
  /api/v1/repos/{owner}/{repo}/symbol:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      operationId: symbolLookup
      summary: Jump-to-definition / find-references for a symbol.
      tags: [Repositories]
      security: []
      parameters:
        - { name: name, in: query, required: true, schema: { type: string } }
        - { name: ref, in: query, schema: { type: string } }
      responses:
        "200": { description: Definition + reference locations. }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/repos/{owner}/{repo}/blame:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      operationId: getBlame
      summary: Line-by-line blame for a file.
      tags: [Repositories]
      security: []
      parameters:
        - { name: path, in: query, required: true, schema: { type: string } }
        - { name: ref, in: query, schema: { type: string } }
      responses:
        "200": { description: Blame lines. }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/repos/{owner}/{repo}/contents:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      operationId: getContents
      summary: Raw file content at a ref.
      tags: [Repositories]
      security: []
      parameters:
        - { name: path, in: query, required: true, schema: { type: string } }
        - { name: ref, in: query, schema: { type: string } }
      responses:
        "200": { description: Raw file bytes. }
        "404": { description: File absent at ref. }
  /api/v1/repos/{owner}/{repo}/index:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    post:
      operationId: reindexRepo
      summary: Rebuild the repository's code/symbol index (write scope).
      tags: [Repositories]
      responses:
        "202": { description: Reindex started. }
        "403": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/repos/{owner}/{repo}/dependencies:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      operationId: listDependencies
      summary: The repository's resolved dependency inventory.
      tags: [Security]
      security: []
      responses:
        "200": { description: Dependencies. }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/repos/{owner}/{repo}/provenance:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      operationId: getProvenance
      summary: Build/commit provenance for the repository.
      tags: [Security]
      security: []
      responses:
        "200": { description: Provenance record. }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/repos/{owner}/{repo}/advisories:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      operationId: listAdvisories
      summary: Dependency security advisories affecting the repository.
      tags: [Security]
      security: []
      responses:
        "200": { description: Advisories with severity + reachability. }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      operationId: createAdvisory
      summary: Record a repository advisory (write scope).
      tags: [Security]
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, additionalProperties: true } } }
      responses:
        "201": { description: Created. }
        "403": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/repos/{owner}/{repo}/advisories/sync:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    post:
      operationId: syncAdvisories
      summary: Re-scan dependencies against advisory feeds (write scope).
      tags: [Security]
      responses:
        "200": { description: Sync complete. }
        "403": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/repos/{owner}/{repo}/advisories/{id}/autofix:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: id, in: path, required: true, schema: { type: integer } }
    post:
      operationId: autofixAdvisory
      summary: Open a pull request that bumps the vulnerable dependency (write scope).
      tags: [Security]
      responses:
        "201": { description: Autofix PR opened. }
        "403": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/repos/{owner}/{repo}/pulls/{number}/files:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: number, in: path, required: true, schema: { type: integer } }
    get:
      operationId: listPullFiles
      summary: Files changed in a pull request.
      tags: [Pull Requests]
      security: []
      responses:
        "200": { description: Changed files. }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/repos/{owner}/{repo}/pulls/{number}/comments:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: number, in: path, required: true, schema: { type: integer } }
    post:
      operationId: commentOnPull
      summary: Add a review comment (inline on a file/line, or general).
      tags: [Pull Requests]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [body]
              properties:
                body: { type: string }
                file_path: { type: string }
                line: { type: integer }
      responses:
        "201": { description: Comment created. }
        "403": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/repos/{owner}/{repo}/pulls/{number}/reviews:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: number, in: path, required: true, schema: { type: integer } }
    post:
      operationId: reviewPull
      summary: Submit a review event (approve / request changes / comment).
      tags: [Pull Requests]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [event]
              properties:
                event: { type: string, enum: [approve, request_changes, comment] }
                body: { type: string }
      responses:
        "201": { description: Review submitted. }
  /api/v1/repos/{owner}/{repo}/pulls/{number}/merge:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: number, in: path, required: true, schema: { type: integer } }
    post:
      operationId: mergePull
      summary: Merge a pull request. Returns 409 if branch protection blocks it.
      tags: [Pull Requests]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                strategy: { type: string, enum: [merge, squash, rebase] }
      responses:
        "200": { description: Merged. }
        "409": { description: Blocked by branch protection. }
  /api/v1/repos/{owner}/{repo}/pulls/{number}/impact:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: number, in: path, required: true, schema: { type: integer } }
    get:
      operationId: pullImpact
      summary: Blast-radius / change-impact analysis for a pull request.
      tags: [Pull Requests]
      security: []
      responses:
        "200": { description: Impacted symbols and callers. }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/repos/{owner}/{repo}/pipelines:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: sha, in: query, required: false, schema: { type: string }, description: Filter to one commit (prefix match). }
      - { name: limit, in: query, required: false, schema: { type: integer, default: 20, maximum: 100 } }
    get:
      operationId: listPipelines
      summary: List CI pipelines, newest first.
      description: >
        Readable with a personal access token, or with a deploy key for its own
        repository (HTTP Basic, username `x-deploy-key`, the key's token as the
        password) — so a CI client can diagnose its own failures.
      tags: [CI]
      responses:
        "200": { description: Recent pipelines. }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/repos/{owner}/{repo}/pipelines/{id}:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: id, in: path, required: true, schema: { type: integer } }
    get:
      operationId: getPipeline
      summary: One pipeline with its jobs.
      tags: [CI]
      responses:
        "200": { description: Pipeline and job list. }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/repos/{owner}/{repo}/pipelines/{id}/jobs/{jobID}/log:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: id, in: path, required: true, schema: { type: integer } }
      - { name: jobID, in: path, required: true, schema: { type: integer } }
      - { name: tail, in: query, required: false, schema: { type: integer }, description: Return only the last N bytes. }
    get:
      operationId: getJobLog
      summary: A job's runner-side log, as plain text.
      description: >
        Includes everything the runner reported before the job's own script ran —
        an unavailable container image, a rejected resource limit, a config that
        did not parse. Capped at 512 KiB (the tail is kept); `X-Job-Status` and
        `X-Job-Exit-Code` headers carry the outcome.
      tags: [CI]
      responses:
        "200": { description: Plain-text log. }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/repos/{owner}/{repo}/pulls/{number}/schema-preview:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: number, in: path, required: true, schema: { type: integer } }
    get:
      operationId: pullSchemaPreview
      summary: Database migration preview for a pull request (Schema Provenance).
      description: >
        Classifies the migration SQL this pull request introduces (destructive /
        warning / info) and cross-checks each statement against the schema the
        last release documented for the repository's tied databases. Reads SQL
        text only — nothing is executed and no database is contacted.
        `has_migrations` is false when the pull request touches no reviewable SQL.
      tags: [Pull Requests]
      responses:
        "200": { description: Statement classification and documented-schema checks. }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/repos/{owner}/{repo}/pulls/{number}/semantic-conflicts:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: number, in: path, required: true, schema: { type: integer } }
    get:
      operationId: pullSemanticConflicts
      summary: Semantic (non-textual) conflicts detected for a pull request.
      tags: [Pull Requests]
      security: []
      responses:
        "200": { description: Semantic conflicts. }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/repos/{owner}/{repo}/pulls/{number}/review-links:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: number, in: path, required: true, schema: { type: integer } }
    get:
      operationId: listReviewLinks
      summary: List shareable review links for a pull request.
      tags: [Pull Requests]
      responses:
        "200": { description: Review links. }
    post:
      operationId: createReviewLink
      summary: Create a shareable review link (write scope).
      tags: [Pull Requests]
      responses:
        "201": { description: Link created. }
        "403": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/repos/{owner}/{repo}/pulls/{number}/review-links/{id}/revoke:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: number, in: path, required: true, schema: { type: integer } }
      - { name: id, in: path, required: true, schema: { type: integer } }
    post:
      operationId: revokeReviewLink
      summary: Revoke a review link (write scope).
      tags: [Pull Requests]
      responses:
        "200": { description: Revoked. }
  /api/v1/repos/{owner}/{repo}/deploy-pipeline:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      operationId: getDeployPipeline
      summary: The repository's deployment pipeline definition (rings/gates).
      tags: [Deployments]
      security: []
      responses:
        "200": { description: Pipeline definition. }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      operationId: setDeployPipeline
      summary: Set the deployment pipeline definition (write scope).
      tags: [Deployments]
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, additionalProperties: true } } }
      responses:
        "200": { description: Pipeline saved. }
        "403": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/repos/{owner}/{repo}/deploy-runs:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      operationId: listDeployRuns
      summary: List deployment runs.
      tags: [Deployments]
      security: []
      responses:
        "200": { description: Deploy runs. }
    post:
      operationId: triggerDeploy
      summary: Trigger a deployment run (write scope).
      tags: [Deployments]
      requestBody:
        content: { application/json: { schema: { type: object, additionalProperties: true } } }
      responses:
        "201": { description: Run started. }
        "403": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/repos/{owner}/{repo}/deploy-runs/{id}/advance:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: id, in: path, required: true, schema: { type: integer } }
    post:
      operationId: advanceDeploy
      summary: Advance a run to the next deployment ring (write scope).
      tags: [Deployments]
      responses:
        "200": { description: Advanced. }
  /api/v1/repos/{owner}/{repo}/deploy-runs/{id}/evaluate:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: id, in: path, required: true, schema: { type: integer } }
    post:
      operationId: evaluateDeploy
      summary: Evaluate the current ring's health gates (write scope).
      tags: [Deployments]
      responses:
        "200": { description: Evaluation result. }
  /api/v1/repos/{owner}/{repo}/deploy-runs/{id}/rollback:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: id, in: path, required: true, schema: { type: integer } }
    post:
      operationId: rollbackDeploy
      summary: Roll back a deployment run (write scope).
      tags: [Deployments]
      responses:
        "200": { description: Rolled back. }
  /api/v1/repos/{owner}/{repo}/encryption:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      operationId: getRepoEncryption
      summary: The repository's encryption configuration.
      tags: [Encryption]
      responses:
        "200": { description: Encryption config. }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      operationId: setRepoEncryption
      summary: Enable/configure repository encryption (admin).
      tags: [Encryption]
      requestBody:
        content: { application/json: { schema: { type: object, additionalProperties: true } } }
      responses:
        "200": { description: Saved. }
        "403": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/repos/{owner}/{repo}/encryption/key:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      operationId: getRepoKey
      summary: The repository's wrapped encryption key.
      tags: [Encryption]
      responses:
        "200": { description: Wrapped key. }
    post:
      operationId: setRepoKey
      summary: Set/rotate the repository's encryption key (admin).
      tags: [Encryption]
      requestBody:
        content: { application/json: { schema: { type: object, additionalProperties: true } } }
      responses:
        "200": { description: Key set. }
        "403": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/repos/{owner}/{repo}/encryption/setup:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
    get:
      operationId: encryptionSetup
      summary: Encryption setup material / instructions for the repository.
      tags: [Encryption]
      responses:
        "200": { description: Setup details. }
  /api/v1/repos/{owner}/{repo}/packages/{type}:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: type, in: path, required: true, schema: { type: string, enum: [generic, npm, pypi, go] } }
    post:
      operationId: uploadPackage
      summary: Publish a package to the registry (write scope).
      tags: [Packages]
      requestBody:
        required: true
        content: { application/octet-stream: { schema: { type: string, format: binary } } }
      responses:
        "201": { description: Published. }
        "403": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/repos/{owner}/{repo}/packages/{type}/{name}/{version}/files/{filename}:
    parameters:
      - $ref: "#/components/parameters/Owner"
      - $ref: "#/components/parameters/Repo"
      - { name: type, in: path, required: true, schema: { type: string } }
      - { name: name, in: path, required: true, schema: { type: string } }
      - { name: version, in: path, required: true, schema: { type: string } }
      - { name: filename, in: path, required: true, schema: { type: string } }
    get:
      operationId: downloadPackageFile
      summary: Download a package file.
      tags: [Packages]
      security: []
      responses:
        "200": { description: File bytes. }
        "404": { $ref: "#/components/responses/NotFound" }

components:
  securitySchemes:
    BearerToken:
      type: http
      scheme: bearer
      bearerFormat: CodeVault Personal Access Token
      description: |
        Pass the raw token in the `Authorization: Bearer <token>` header.
        Tokens are issued at `/settings/tokens`. Tokens with `repo:read`
        scope cannot perform write operations and will receive `403`.
    BasicAuth:
      type: http
      scheme: basic
      description: |
        HTTP Basic auth using your username and a Personal Access Token as
        the password. Pure password auth is **not** supported on the API.

  parameters:
    Owner:
      name: owner
      in: path
      required: true
      schema: { type: string }
      description: Repository owner (username or organization).
    Repo:
      name: repo
      in: path
      required: true
      schema: { type: string }
      description: Repository name.
    Username:
      name: username
      in: path
      required: true
      schema: { type: string }
    Number:
      name: number
      in: path
      required: true
      schema: { type: integer, minimum: 1 }
      description: Issue or pull request number, scoped per repository.
    Page:
      name: page
      in: query
      schema: { type: integer, minimum: 1, default: 1 }
      description: 1-based page index.
    PerPage:
      name: per_page
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
      description: Page size (max 100).

  headers:
    XTotalCount:
      description: Total number of items across all pages.
      schema: { type: integer, minimum: 0 }
    Link:
      description: |
        RFC 5988 `Link` header carrying `prev`, `next`, `first`, `last`
        URLs for the paginated collection.
      schema: { type: string }

  responses:
    BadRequest:
      description: Malformed input.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: Authenticated but not allowed (insufficient scope, not the repo owner, etc.).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Resource does not exist or is not visible to the caller.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    Milestone:
      type: object
      properties:
        id: { type: integer }
        title: { type: string }
        description: { type: string }
        state: { type: string, enum: [planned, in_progress, shipped, cancelled] }
        release_tag: { type: string }
        sort_order: { type: integer }
        due_date: { type: string, nullable: true }
        open_issues: { type: integer }
        closed_issues: { type: integer }
        progress: { type: integer }
    FeatureRequest:
      type: object
      properties:
        number: { type: integer }
        title: { type: string }
        body: { type: string }
        state: { type: string }
        votes: { type: integer }
        author: { type: string }
        milestone: { type: string }
        milestone_state: { type: string }
    PollOption:
      type: object
      properties:
        id: { type: integer }
        text: { type: string }
        votes: { type: integer }
        percent: { type: integer }
        chosen: { type: boolean }
    Poll:
      type: object
      properties:
        id: { type: integer }
        question: { type: string }
        description: { type: string }
        multi: { type: boolean }
        open: { type: boolean }
        closed: { type: boolean }
        milestone_id: { type: integer }
        total_votes: { type: integer }
        deadline: { type: string, nullable: true }
        created_by: { type: string }
        options: { type: array, items: { $ref: "#/components/schemas/PollOption" } }
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string }

    CurrentUser:
      type: object
      required: [id, username, email, is_admin, created_at]
      properties:
        id:         { type: integer, format: int64 }
        username:   { type: string }
        email:      { type: string, format: email }
        full_name:  { type: string }
        bio:        { type: string }
        avatar_url: { type: string }
        is_admin:   { type: boolean }
        created_at: { type: string, format: date-time }

    UserProfile:
      type: object
      required: [id, username, created_at]
      properties:
        id:         { type: integer, format: int64 }
        username:   { type: string }
        full_name:  { type: string }
        bio:        { type: string }
        avatar_url: { type: string }
        created_at: { type: string, format: date-time }

    Repository:
      type: object
      required: [id, owner, name, full_name, private, default_branch, empty]
      properties:
        id:             { type: integer, format: int64 }
        owner:          { type: string }
        name:           { type: string }
        full_name:      { type: string, description: "`owner/name`." }
        description:    { type: string }
        private:        { type: boolean }
        default_branch: { type: string }
        stars_count:    { type: integer }
        forks_count:    { type: integer }
        empty:          { type: boolean, description: "True when the repo has no refs yet." }
        created_at:     { type: string, format: date-time }
        updated_at:     { type: string, format: date-time }

    Branch:
      type: object
      required: [name, active]
      properties:
        name:   { type: string }
        active: { type: boolean, description: "True if this branch is the repo's default." }

    CreateBranch:
      type: object
      required: [name]
      properties:
        name:
          type: string
          description: Name of the branch to create.
        source:
          type: string
          description: Branch or commit-ish to fork from. Defaults to the repo's default branch.

    IssueSummary:
      type: object
      required: [id, number, title, state, author, created_at]
      properties:
        id:         { type: integer, format: int64 }
        number:     { type: integer }
        title:      { type: string }
        body:       { type: string }
        state:      { type: string, enum: [open, closed] }
        label:      { type: string }
        author:     { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    Issue:
      allOf:
        - $ref: "#/components/schemas/IssueSummary"
        - type: object
          properties:
            comments:
              type: array
              items: { $ref: "#/components/schemas/Comment" }

    CreateIssue:
      type: object
      required: [title]
      properties:
        title:  { type: string }
        body:   { type: string }
        labels:
          type: string
          description: Comma-separated label names (legacy single-string format).

    IssueCreated:
      type: object
      required: [id, number, title]
      properties:
        id:     { type: integer, format: int64 }
        number: { type: integer }
        title:  { type: string }

    Comment:
      type: object
      required: [id, author, body, created_at]
      properties:
        id:         { type: integer, format: int64 }
        author:     { type: string }
        body:       { type: string }
        created_at: { type: string, format: date-time }

    CreateComment:
      type: object
      required: [body]
      properties:
        body: { type: string }

    CommentCreated:
      type: object
      required: [body, author]
      properties:
        body:   { type: string }
        author: { type: string }

    PullRequestSummary:
      type: object
      required: [id, number, title, head_branch, base_branch, status, author, created_at]
      properties:
        id:          { type: integer, format: int64 }
        number:      { type: integer }
        title:       { type: string }
        body:        { type: string }
        head_branch: { type: string }
        base_branch: { type: string }
        status:      { type: string, enum: [open, closed, merged] }
        is_draft:    { type: boolean }
        author:      { type: string }
        created_at:  { type: string, format: date-time }
        updated_at:  { type: string, format: date-time }

    PullRequest:
      allOf:
        - $ref: "#/components/schemas/PullRequestSummary"
        - type: object
          properties:
            merge_commit: { type: string }
            review_count: { type: integer }
            merged_at:    { type: string, format: date-time, nullable: true }
            closed_at:    { type: string, format: date-time, nullable: true }
            comments:
              type: array
              items: { $ref: "#/components/schemas/PRComment" }
            reviews:
              type: array
              items: { $ref: "#/components/schemas/PRReview" }

    PRComment:
      type: object
      required: [id, author, body, created_at]
      properties:
        id:         { type: integer, format: int64 }
        author:     { type: string }
        body:       { type: string }
        file_path:  { type: string, description: "File the comment is anchored to (line comments only)." }
        line:       { type: integer, description: "Line number for line comments; 0 otherwise." }
        is_system:  { type: boolean, description: "True for CodeVault-generated comments (merge / close / reopen)." }
        created_at: { type: string, format: date-time }

    PRReview:
      type: object
      required: [id, reviewer, status, created_at]
      properties:
        id:         { type: integer, format: int64 }
        reviewer:   { type: string }
        status:     { type: string, enum: [approved, changes_requested, comment] }
        body:       { type: string }
        created_at: { type: string, format: date-time }

    CreatePullRequest:
      type: object
      required: [title, head_branch, base_branch]
      properties:
        title:       { type: string }
        body:        { type: string }
        head_branch: { type: string }
        base_branch: { type: string }
        is_draft:    { type: boolean }

    PullRequestCreated:
      type: object
      required: [id, number, title, head_branch, base_branch, status]
      properties:
        id:          { type: integer, format: int64 }
        number:      { type: integer }
        title:       { type: string }
        head_branch: { type: string }
        base_branch: { type: string }
        status:      { type: string }

    CreateCommitStatus:
      type: object
      required: [state]
      properties:
        state:
          type: string
          enum: [pending, success, failure, error]
        context:
          type: string
          default: default
          description: Logical name of the check (e.g. "ci/build", "lint"). Per `(sha, context)`, the most recent status wins.
        description: { type: string }
        target_url:
          type: string
          format: uri
          description: Link to the CI build / details page.

    CommitStatus:
      type: object
      required: [sha, state, context]
      properties:
        sha:         { type: string }
        state:       { type: string, enum: [pending, success, failure, error] }
        context:     { type: string }
        description: { type: string }
        target_url:  { type: string, format: uri }

    CommitStatusList:
      type: object
      required: [state, statuses]
      properties:
        state:
          type: string
          enum: [pending, success, failure, error]
          description: Combined state across all `(sha, context)` rows.
        statuses:
          type: array
          items:
            type: object
            properties:
              id:          { type: integer, format: int64 }
              sha:         { type: string }
              state:       { type: string, enum: [pending, success, failure, error] }
              context:     { type: string }
              description: { type: string }
              target_url:  { type: string, format: uri }
              created_at:  { type: string, format: date-time }

    ToggleReaction:
      type: object
      required: [target_type, target_id, emoji]
      properties:
        target_type:
          type: string
          enum: [issue, issue_comment, pr, pr_comment]
        target_id:
          type: integer
          format: int64
          description: ID of the issue / PR / comment row (NOT the public number).
        emoji:
          type: string
          enum: [thumbsup, thumbsdown, heart, hooray, confused, rocket, eyes]

    MoveCard:
      type: object
      required: [column_id, position]
      properties:
        column_id:
          type: integer
          format: int64
          description: Target project column.
        position:
          type: integer
          minimum: 0
          description: Zero-based slot within the target column.

    HeatmapDay:
      type: object
      required: [date, count]
      properties:
        date:  { type: string, format: date }
        count: { type: integer, minimum: 1 }

    ActivityEntry:
      type: object
      required: [action, message, repo_name, owner_name, ref_name, sha]
      properties:
        action:     { type: string, description: "Verb: push, create_pr, merge_pr, comment, ..." }
        message:    { type: string }
        repo_name:  { type: string }
        owner_name: { type: string }
        ref_name:   { type: string }
        sha:        { type: string }
