openapi: 3.1.0
info:
  title: CodeVault REST API
  version: "2.4.1"
  summary: Programmatic access to a CodeVault instance.
  description: |
    The `/api/v1/*` REST surface for managing repositories, branches, issues,
    pull requests, commit statuses, reactions, 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.

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" }

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:
    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 }
