openapi: 3.1.0
info:
  title: Gaffer API
  version: 1.0.0
  description: |
    Gaffer API for programmatic access to test data, analytics, and coverage metrics.

    ## Authentication

    Gaffer supports two types of API keys, both passed via the `X-API-Key` header.

    ### Project Tokens (project-scoped)

    Format: `gfr_<64 hex chars>`

    Each project token is scoped to a single project. Use these for uploading test reports and accessing all project-scoped read endpoints. The project ID is resolved automatically from the token.

    ```bash
    curl -H "X-API-Key: gfr_your_project_token_here" \
      https://app.gaffer.sh/api/v1/user/projects/{projectId}/health
    ```

    ### User API Keys (user-scoped)

    Format: `gaf_<64 hex chars>`

    User API keys grant access to all projects across your organizations. Use these for cross-project queries and listing projects.

    ```bash
    curl -H "X-API-Key: gaf_your_user_api_key_here" \
      https://app.gaffer.sh/api/v1/user/projects
    ```

    Generate user API keys from your [account settings](https://app.gaffer.sh/account/api-keys).

    ### Which key type should I use?

    - **CI/CD pipelines & single-project agents** — use project tokens (`gfr_`), scoped to one project
    - **Cross-project queries** — use user API keys (`gaf_`), access all projects
    - **AI agents / MCP servers** — either works; project tokens are simpler for single-project setups

    ## Plan-Based Limits

    Analytics endpoints accept a `days` parameter to control the analysis window. This value is clamped by your organization's plan tier:

    | Plan | Max Days | Max Items (test history) |
    |------|----------|--------------------------|
    | Free | 30 | 50 |
    | Pro | 90 | 100 |
    | Team | 180 | 100 |

    Max Items is the per-plan clamp on the test-history endpoint. Paginated
    endpoints declare their own `limit` maximum on the parameter itself.

    When clamping occurs, the response includes a `meta` object:

    ```json
    {
      "meta": {
        "appliedDays": 30,
        "requestedDays": 90,
        "clampedByPlan": true
      }
    }
    ```

    The `meta` field is only present when values were clamped. Always use the values in the response (e.g., `summary.period`) rather than assuming your requested value was applied.

    ## Common Workflows

    ### Diagnose CI failures

    1. `GET /api/v1/user/projects` — find the project ID
    2. `GET /api/v1/user/projects/{id}/test-runs?status=failed` — list failed runs
    3. `GET /api/v1/user/projects/{id}/test-runs/{runId}/details?status=failed` — get failure details with stack traces
    4. `GET /api/v1/user/projects/{id}/test-runs/{runId}/failure-clusters` — group failures by root cause
    5. `GET /api/v1/user/projects/{id}/test-history?testName=...` — check if the failure is new or recurring

    ### Check upload status

    1. `GET /api/v1/user/projects/{id}/upload-sessions?commitSha=...` — find upload session for a commit
    2. Check `processingStatus` field — `completed` means results are ready
    3. `GET /api/v1/user/projects/{id}/upload-sessions/{sessionId}` — get linked test runs and coverage reports

    ### Find flaky tests

    1. `GET /api/v1/user/projects/{id}/flaky-tests?days=30` — list tests with high flip rates
    2. `GET /api/v1/user/projects/{id}/test-history?testName=...` — inspect individual test stability

    ### Assess coverage gaps

    1. `GET /api/v1/user/projects/{id}/coverage-summary` — get overall coverage metrics
    2. `GET /api/v1/user/projects/{id}/coverage/risk-areas` — find files with low coverage AND test failures
    3. `GET /api/v1/user/projects/{id}/coverage/files?maxCoverage=50&sortBy=coverage` — list poorly-covered files

    ### Compare test performance

    1. `GET /api/v1/user/projects/{id}/compare-test?testName=...&beforeCommit=abc&afterCommit=def` — compare a test across commits

    ## For AI Agents

    Gaffer provides an [MCP server](https://www.npmjs.com/package/@gaffer-sh/mcp) that wraps this API with tool definitions optimized for AI agent consumption. If you're building an AI integration, consider using the MCP server instead of calling the API directly.

  contact:
    name: Gaffer Support
    email: support@gaffer.sh
    url: https://gaffer.sh
  license:
    name: Proprietary
    url: https://gaffer.sh/terms

servers:
  - url: https://app.gaffer.sh
    description: Production

security:
  - ApiKeyAuth: []

paths:
  # ==========================================================================
  # Project-scoped endpoints (project token auth: gfr_)
  # ==========================================================================

  /api/v1/project:
    get:
      summary: Get project
      description: Returns the project associated with the authenticated project token.
      operationId: getProject
      tags:
        - Project
      responses:
        '200':
          description: Project details
          content:
            application/json:
              schema:
                type: object
                properties:
                  project:
                    $ref: '#/components/schemas/Project'
              example:
                project:
                  id: "proj_abc123def456"
                  name: "My Test Suite"
                  description: "E2E tests for the main application"
                  retentionDays: 30
                  createdAt: "2024-01-15T10:30:00Z"
                  updatedAt: "2024-03-20T14:22:00Z"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/project/test-runs:
    get:
      summary: List test runs (project-scoped)
      description: Returns a paginated list of test runs for the project.
      operationId: listTestRuns
      tags:
        - Project
      parameters:
        - name: limit
          in: query
          description: Number of items per page (1-100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: offset
          in: query
          description: Number of items to skip
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: branch
          in: query
          description: Filter by branch name
          schema:
            type: string
        - name: framework
          in: query
          description: Filter by test framework
          schema:
            type: string
        - name: commitSha
          in: query
          description: Filter by commit SHA (exact or prefix match)
          schema:
            type: string
        - name: status
          in: query
          description: "Filter by status: 'passed' (no failures) or 'failed' (has failures)"
          schema:
            type: string
            enum: [passed, failed]
      responses:
        '200':
          description: Paginated list of test runs
          content:
            application/json:
              schema:
                type: object
                properties:
                  testRuns:
                    type: array
                    items:
                      $ref: '#/components/schemas/TestRunSummary'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/test-runs/{id}:
    get:
      summary: Get test run
      description: Returns detailed information about a specific test run.
      operationId: getTestRun
      tags:
        - Project
      parameters:
        - name: id
          in: path
          required: true
          description: Test run ID
          schema:
            type: string
      responses:
        '200':
          description: Test run details
          content:
            application/json:
              schema:
                type: object
                properties:
                  testRun:
                    $ref: '#/components/schemas/TestRun'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/project/analytics:
    get:
      summary: Get analytics (project-scoped)
      description: |
        Returns analytics summary including health score, pass rate, and flaky test count.
        The analysis period is clamped by your plan tier.
      operationId: getAnalytics
      tags:
        - Project
      parameters:
        - $ref: '#/components/parameters/DaysParam'
      responses:
        '200':
          description: Analytics summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  analytics:
                    $ref: '#/components/schemas/HealthSummary'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/project/flaky-tests:
    get:
      summary: List flaky tests (project-scoped)
      description: |
        Returns tests identified as flaky based on flip rate analysis.
        A test is flaky if it frequently switches between pass and fail states.
      operationId: listFlakyTests
      tags:
        - Project
      parameters:
        - $ref: '#/components/parameters/ThresholdParam'
        - $ref: '#/components/parameters/LimitParam'
        - $ref: '#/components/parameters/DaysParam'
      responses:
        '200':
          description: List of flaky tests
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlakyTestsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ==========================================================================
  # Project-scoped read endpoints (user API key or project token)
  # ==========================================================================

  /api/v1/user/projects:
    get:
      summary: List projects
      description: |
        Lists all projects the authenticated user has access to across their organizations.
        Results include organization context for each project.
        **Requires a user API key (gaf_).** Project tokens cannot list projects.
      operationId: listUserProjects
      tags:
        - User Projects
      security:
        - UserApiKeyAuth: []
      parameters:
        - name: limit
          in: query
          description: Number of items per page (1-100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: offset
          in: query
          description: Number of items to skip
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: organizationId
          in: query
          description: Filter to a specific organization
          schema:
            type: string
      responses:
        '200':
          description: Paginated list of projects with organization info
          content:
            application/json:
              schema:
                type: object
                properties:
                  projects:
                    type: array
                    items:
                      $ref: '#/components/schemas/UserProject'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
              example:
                projects:
                  - id: "proj_abc123"
                    name: "Frontend Tests"
                    description: "Playwright E2E tests"
                    retentionDays: 30
                    organization:
                      id: "org_xyz789"
                      name: "Acme Corp"
                      slug: "acme-corp"
                    createdAt: "2024-01-15T10:30:00Z"
                    updatedAt: "2024-03-20T14:22:00Z"
                pagination:
                  limit: 50
                  offset: 0
                  total: 3
                  hasMore: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /api/v1/user/projects/{projectId}/health:
    get:
      summary: Get project health
      description: |
        Returns analytics overview for a project including health score,
        pass rate, test run count, trend direction, and flaky test count.
        The analysis period is clamped by your plan tier.
      operationId: getProjectHealth
      tags:
        - User Projects
      security:
        - UserApiKeyAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - $ref: '#/components/parameters/DaysParam'
      responses:
        '200':
          description: Health analytics for the project
          content:
            application/json:
              schema:
                type: object
                properties:
                  analytics:
                    $ref: '#/components/schemas/HealthSummary'
                  meta:
                    $ref: '#/components/schemas/Meta'
              example:
                analytics:
                  projectId: "proj_abc123"
                  projectName: "Frontend Tests"
                  period:
                    days: 30
                    start: "2024-02-19T00:00:00Z"
                    end: "2024-03-20T14:30:00Z"
                  healthScore: 87
                  passRate: 94.5
                  testRunCount: 42
                  totalTests: 6300
                  flakyTestCount: 3
                  trend: "up"
                  computedAt: "2024-03-20T14:30:00Z"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/user/projects/{projectId}/test-runs:
    get:
      summary: List test runs
      description: |
        Lists test runs for a specific project with pagination and filtering.
        Returns summary data (excludes full test results for performance).
      operationId: listUserTestRuns
      tags:
        - User Projects
      security:
        - UserApiKeyAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - name: limit
          in: query
          description: Number of items per page (1-100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: offset
          in: query
          description: Number of items to skip
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: branch
          in: query
          description: Filter by branch name
          schema:
            type: string
        - name: framework
          in: query
          description: Filter by test framework (e.g., playwright, vitest, jest)
          schema:
            type: string
        - name: commitSha
          in: query
          description: Filter by commit SHA (exact or prefix match)
          schema:
            type: string
        - name: status
          in: query
          description: "Filter by status: 'passed' (no failures) or 'failed' (has failures)"
          schema:
            type: string
            enum: [passed, failed]
      responses:
        '200':
          description: Paginated list of test runs
          content:
            application/json:
              schema:
                type: object
                properties:
                  testRuns:
                    type: array
                    items:
                      $ref: '#/components/schemas/TestRunSummary'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/user/projects/{projectId}/test-runs/{testRunId}/details:
    get:
      summary: Get test run details
      description: |
        Returns parsed test results for a specific test run with pagination and filtering.
        Includes individual test cases with error messages and stack traces.
        Also includes the test run's commit, branch, and framework context.
      operationId: getTestRunDetails
      tags:
        - Test History
      security:
        - UserApiKeyAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - name: testRunId
          in: path
          required: true
          description: Test run ID
          schema:
            type: string
        - name: limit
          in: query
          description: Number of test cases per page (1-500)
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 100
        - name: offset
          in: query
          description: Number of test cases to skip
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: status
          in: query
          description: Filter by test status
          schema:
            type: string
            enum: [passed, failed, skipped]
      responses:
        '200':
          description: Paginated test cases with run context
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestRunDetailsResponse'
              example:
                testRunId: "run_xyz789"
                commitSha: "a1b2c3d4e5f6"
                branch: "feature/login"
                framework: "playwright"
                createdAt: "2024-03-20T14:30:00Z"
                summary:
                  passed: 142
                  failed: 3
                  skipped: 5
                  total: 150
                tests:
                  - name: "should handle session timeout"
                    fullName: "Auth > Login > should handle session timeout"
                    status: "failed"
                    durationMs: 5230
                    filePath: "tests/auth/login.spec.ts"
                    error: "Timeout waiting for selector '.dashboard'"
                    errorStack: "Error: Timeout waiting for selector '.dashboard'\n    at tests/auth/login.spec.ts:42:15"
                  - name: "should display user profile"
                    fullName: "Auth > Login > should display user profile"
                    status: "passed"
                    durationMs: 1200
                    filePath: "tests/auth/login.spec.ts"
                    error: null
                    errorStack: null
                pagination:
                  total: 150
                  limit: 100
                  offset: 0
                  hasMore: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/user/projects/{projectId}/test-history:
    get:
      summary: Get test history
      description: |
        Returns pass/fail history for a specific test by name or file path.
        Useful for understanding test stability over time.
        Either `testName` or `filePath` must be provided.
      operationId: getTestHistory
      tags:
        - Test History
      security:
        - UserApiKeyAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - name: testName
          in: query
          description: Test name to search for (provide this or filePath)
          schema:
            type: string
        - name: filePath
          in: query
          description: File path to search for (provide this or testName)
          schema:
            type: string
        - name: limit
          in: query
          description: Maximum number of results (1-100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        '200':
          description: Test history with summary statistics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestHistoryResponse'
              example:
                history:
                  - testRunId: "run_xyz789"
                    createdAt: "2024-03-20T14:30:00Z"
                    branch: "main"
                    commitSha: "a1b2c3d4"
                    test:
                      name: "should handle login"
                      status: "passed"
                      durationMs: 1250
                      filePath: "tests/auth/login.spec.ts"
                      message: null
                  - testRunId: "run_abc456"
                    createdAt: "2024-03-19T14:30:00Z"
                    branch: "main"
                    commitSha: "f6e5d4c3"
                    test:
                      name: "should handle login"
                      status: "failed"
                      durationMs: 5100
                      filePath: "tests/auth/login.spec.ts"
                      message: null
                summary:
                  totalRuns: 2
                  passedRuns: 1
                  failedRuns: 1
                  passRate: 50.0
                  searchedBy: "testName"
                  searchValue: "should handle login"
                message: null
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/user/projects/{projectId}/compare-test:
    get:
      summary: Compare test metrics
      description: |
        Compare test metrics between two commits or two test runs.
        Provide either (`beforeCommit` + `afterCommit`) or (`beforeRunId` + `afterRunId`).
      operationId: compareTest
      tags:
        - Test History
      security:
        - UserApiKeyAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - name: testName
          in: query
          required: true
          description: The test name to compare
          schema:
            type: string
        - name: beforeCommit
          in: query
          description: Commit SHA for the "before" measurement (use with afterCommit)
          schema:
            type: string
        - name: afterCommit
          in: query
          description: Commit SHA for the "after" measurement (use with beforeCommit)
          schema:
            type: string
        - name: beforeRunId
          in: query
          description: Test run ID for the "before" measurement (use with afterRunId)
          schema:
            type: string
        - name: afterRunId
          in: query
          description: Test run ID for the "after" measurement (use with beforeRunId)
          schema:
            type: string
      responses:
        '200':
          description: Test comparison result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestComparisonResponse'
              example:
                testName: "should process payment"
                before:
                  testRunId: "run_abc123"
                  commit: "a1b2c3d4"
                  branch: "main"
                  status: "passed"
                  durationMs: 2500
                  createdAt: "2024-03-19T14:30:00Z"
                after:
                  testRunId: "run_def456"
                  commit: "e5f6a7b8"
                  branch: "main"
                  status: "passed"
                  durationMs: 4200
                  createdAt: "2024-03-20T14:30:00Z"
                change:
                  durationMs: 1700
                  percentChange: 68.0
                  statusChanged: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/user/projects/{projectId}/flaky-tests:
    get:
      summary: List flaky tests
      description: |
        Returns tests identified as flaky based on flip rate analysis.
        A test is considered flaky if its flip rate exceeds the threshold.
        The analysis period is clamped by your plan tier.
      operationId: listUserFlakyTests
      tags:
        - Analytics
      security:
        - UserApiKeyAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - $ref: '#/components/parameters/ThresholdParam'
        - $ref: '#/components/parameters/LimitParam'
        - $ref: '#/components/parameters/DaysParam'
      responses:
        '200':
          description: List of flaky tests with summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlakyTestsResponse'
              example:
                flakyTests:
                  - name: "tests/auth/login.spec.ts > should handle session timeout"
                    flipRate: 0.35
                    flipCount: 7
                    totalRuns: 20
                    lastSeen: "2024-03-20T10:15:00Z"
                    flakinessScore: 0.54
                summary:
                  threshold: 0.1
                  totalFlaky: 3
                  period: 30
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/user/projects/{projectId}/search-failures:
    get:
      summary: Search test failures
      description: |
        Search across test failures by error message, stack trace, or test name.
        Returns matching failed test cases with context from their test runs.
        Omit `query` to return every failed test case in the window.
        The analysis period is clamped by your plan tier.
      operationId: searchFailures
      tags:
        - Analytics
      security:
        - UserApiKeyAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - name: query
          in: query
          description: Case-insensitive substring to search for. Omit to match every failure.
          schema:
            type: string
            minLength: 1
        - name: searchIn
          in: query
          description: Where to search
          schema:
            type: string
            enum: [errors, names, all]
            default: all
        - $ref: '#/components/parameters/DaysParam'
        - name: branch
          in: query
          description: Filter by branch name
          schema:
            type: string
        - name: limit
          in: query
          description: Maximum matches to return (1-100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: Matching test failures
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchFailuresResponse'
              example:
                matches:
                  - testName: "tests/api/upload.spec.ts > should handle connection timeout"
                    testRunId: "run_abc123"
                    branch: "main"
                    commitSha: "a1b2c3d"
                    errorMessage: "Error: Connection timeout after 30000ms"
                    errorStack: "Error: Connection timeout after 30000ms\n    at TCPSocket.connect..."
                    createdAt: "2026-02-28T14:30:00.000Z"
                total: 1
                truncated: false
                query: "timeout"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/user/projects/{projectId}/slowest-tests:
    get:
      summary: List slowest tests
      description: |
        Returns the slowest tests for a project, sorted by P95 duration.
        Queries duration statistics from the Analytics Engine.
        The analysis period is clamped by your plan tier.
      operationId: listSlowestTests
      tags:
        - Analytics
      security:
        - UserApiKeyAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - $ref: '#/components/parameters/DaysParam'
        - name: limit
          in: query
          description: Maximum results to return (1-100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: framework
          in: query
          description: Filter by test framework (e.g., playwright, vitest)
          schema:
            type: string
        - name: branch
          in: query
          description: Filter by branch name
          schema:
            type: string
      responses:
        '200':
          description: Slowest tests with duration statistics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SlowestTestsResponse'
              example:
                slowestTests:
                  - name: "should process payment"
                    fullName: "Checkout > Payment > should process payment"
                    filePath: "tests/checkout/payment.spec.ts"
                    framework: "playwright"
                    avgDurationMs: 12500
                    p95DurationMs: 18200
                    runCount: 45
                  - name: "should load dashboard"
                    fullName: "Dashboard > should load dashboard"
                    filePath: "tests/dashboard.spec.ts"
                    framework: "playwright"
                    avgDurationMs: 8300
                    p95DurationMs: 14100
                    runCount: 50
                summary:
                  projectId: "proj_abc123"
                  projectName: "Frontend Tests"
                  period: 30
                  totalReturned: 2
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/user/projects/{projectId}/test-runs/{testRunId}/failure-clusters:
    get:
      summary: Get failure clusters
      description: |
        Groups failed tests by error message similarity using Levenshtein distance.
        Helps identify root causes when multiple tests fail — often 15 failures are 2-3 bugs.
      operationId: getFailureClusters
      tags:
        - Test History
      security:
        - UserApiKeyAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - name: testRunId
          in: path
          required: true
          description: Test run ID
          schema:
            type: string
      responses:
        '200':
          description: Failure clusters grouped by error similarity
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FailureClustersResponse'
              example:
                clusters:
                  - representativeError: "Connection refused: localhost:5432"
                    count: 9
                    tests:
                      - name: "should create user"
                        fullName: "Auth > should create user"
                        errorMessage: "Connection refused: localhost:5432"
                        filePath: "tests/auth/create-user.spec.ts"
                      - name: "should update profile"
                        fullName: "Profile > should update profile"
                        errorMessage: "Connection refused: localhost:5432"
                        filePath: "tests/profile/update.spec.ts"
                    similarity: 0.7
                    aiCategory:
                      category: "environment_issue"
                      confidence: "high"
                      reasoning: "Connection refused errors to localhost:5432 indicate the PostgreSQL database service was unavailable during the test run."
                  - representativeError: "Expected 200, received 401"
                    count: 3
                    tests:
                      - name: "should access dashboard"
                        fullName: "Dashboard > should access dashboard"
                        errorMessage: "Expected 200, received 401"
                        filePath: "tests/dashboard.spec.ts"
                    similarity: 0.7
                    aiCategory:
                      category: "product_bug"
                      confidence: "medium"
                      reasoning: "Unexpected 401 responses suggest an authentication regression in the application."
                totalFailures: 12
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/user/projects/{projectId}/upload-sessions:
    get:
      summary: List upload sessions
      description: |
        Lists upload sessions for a project with optional filtering by commit or branch.
        Use this to check if CI results have been uploaded and their processing status.
      operationId: listUploadSessions
      tags:
        - Upload Sessions
      security:
        - UserApiKeyAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - name: commitSha
          in: query
          description: Filter by commit SHA
          schema:
            type: string
        - name: branch
          in: query
          description: Filter by branch name
          schema:
            type: string
        - name: limit
          in: query
          description: Number of items per page (1-50)
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 10
        - name: offset
          in: query
          description: Number of items to skip
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        '200':
          description: Paginated list of upload sessions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadSessionsResponse'
              example:
                sessions:
                  - id: "upl_abc123"
                    projectId: "proj_xyz789"
                    uniqueId: "upl_abc123"
                    tags: null
                    commitSha: "a1b2c3d4e5f6"
                    branch: "main"
                    processingStatus: "completed"
                    pendingFileCount: 0
                    failedFileCount: 0
                    createdAt: "2024-03-20T14:30:00Z"
                    updatedAt: "2024-03-20T14:31:00Z"
                pagination:
                  limit: 10
                  offset: 0
                  total: 1
                  hasMore: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/user/projects/{projectId}/upload-sessions/{sessionId}:
    get:
      summary: Get upload session detail
      description: |
        Returns detailed information about a specific upload session, including
        linked test runs and coverage reports produced from that upload.
      operationId: getUploadSessionDetail
      tags:
        - Upload Sessions
      security:
        - UserApiKeyAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - name: sessionId
          in: path
          required: true
          description: Upload session ID
          schema:
            type: string
      responses:
        '200':
          description: Upload session with linked results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadSessionDetailResponse'
              example:
                session:
                  id: "upl_abc123"
                  projectId: "proj_xyz789"
                  uniqueId: "upl_abc123"
                  tags: null
                  commitSha: "a1b2c3d4e5f6"
                  branch: "main"
                  processingStatus: "completed"
                  pendingFileCount: 0
                  failedFileCount: 0
                  createdAt: "2024-03-20T14:30:00Z"
                  updatedAt: "2024-03-20T14:31:00Z"
                testRuns:
                  - id: "run_xyz789"
                    framework: "playwright"
                    summary:
                      passed: 142
                      failed: 3
                      skipped: 5
                      total: 150
                    createdAt: "2024-03-20T14:30:30Z"
                coverageReports:
                  - id: "cov_abc456"
                    format: "lcov"
                    createdAt: "2024-03-20T14:30:45Z"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/user/projects/{projectId}/coverage-summary:
    get:
      summary: Get coverage summary
      description: |
        Returns coverage summary for a project including current line/branch/function
        coverage percentages, trend direction, and the lowest-covered files.
        The analysis period is clamped by your plan tier.
      operationId: getCoverageSummary
      tags:
        - Coverage
      security:
        - UserApiKeyAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - $ref: '#/components/parameters/DaysParam'
      responses:
        '200':
          description: Coverage summary with trend data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CoverageSummaryResponse'
              example:
                hasCoverage: true
                current:
                  lines: 78.5
                  branches: 65.2
                  functions: 82.1
                trend:
                  direction: "up"
                  change: 2.3
                totalReports: 15
                latestReportDate: "2024-03-20T14:30:00Z"
                lowestCoverageFiles:
                  - path: "src/utils/parser.ts"
                    coverage: 12
                  - path: "src/services/billing.ts"
                    coverage: 25
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/user/projects/{projectId}/coverage/files:
    get:
      summary: List coverage files
      description: |
        Returns per-file coverage data for the project's latest coverage report.
        Supports filtering by path, coverage thresholds, sorting, and pagination.
      operationId: listCoverageFiles
      tags:
        - Coverage
      security:
        - UserApiKeyAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - name: filePath
          in: query
          description: Filter to files matching this path (contains match)
          schema:
            type: string
        - name: minCoverage
          in: query
          description: Minimum coverage percentage (0-100)
          schema:
            type: number
            minimum: 0
            maximum: 100
        - name: maxCoverage
          in: query
          description: Maximum coverage percentage (0-100)
          schema:
            type: number
            minimum: 0
            maximum: 100
        - name: limit
          in: query
          description: Maximum number of results (1-500)
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 100
        - name: offset
          in: query
          description: Pagination offset
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: sortBy
          in: query
          description: Sort field
          schema:
            type: string
            enum: [path, coverage]
            default: coverage
        - name: sortOrder
          in: query
          description: Sort direction
          schema:
            type: string
            enum: [asc, desc]
            default: asc
      responses:
        '200':
          description: Per-file coverage data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CoverageFilesResponse'
              example:
                hasCoverage: true
                files:
                  - path: "src/utils/parser.ts"
                    lines:
                      covered: 12
                      total: 100
                      percentage: 12.0
                    branches:
                      covered: 3
                      total: 20
                      percentage: 15.0
                    functions:
                      covered: 2
                      total: 8
                      percentage: 25.0
                pagination:
                  total: 156
                  limit: 100
                  offset: 0
                  hasMore: true
                overallCoverage: 78.5
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/user/projects/{projectId}/coverage/risk-areas:
    get:
      summary: Find coverage risk areas
      description: |
        Returns areas of code that have both low coverage AND test failures.
        Cross-references test results with coverage data to identify high-risk
        areas that need attention. Results are sorted by risk score (highest first).
        The analysis period is clamped by your plan tier.
      operationId: getCoverageRiskAreas
      tags:
        - Coverage
      security:
        - UserApiKeyAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - $ref: '#/components/parameters/DaysParam'
        - name: coverageThreshold
          in: query
          description: Include files below this coverage percentage (0-100)
          schema:
            type: number
            minimum: 0
            maximum: 100
            default: 80
      responses:
        '200':
          description: Risk areas sorted by risk score
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CoverageRiskAreasResponse'
              example:
                hasCoverage: true
                hasTestResults: true
                riskAreas:
                  - filePath: "src/services/billing.ts"
                    coverage: 25
                    failureCount: 8
                    riskScore: 92
                    testNames:
                      - "should process refund"
                      - "should handle expired card"
                message: null
                analysisParams:
                  days: 30
                  coverageThreshold: 80
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/user/projects/{projectId}/reports/{testRunId}/browser-url:
    get:
      summary: Get report browser URL
      description: |
        Returns a browser-navigable URL with a signed token for viewing HTML test reports.
        The token is short-lived (30 minutes) and grants read-only access to the report.
        Useful for AI agents providing clickable links to Playwright, Vitest, or other HTML reports.
      operationId: getReportBrowserUrl
      tags:
        - Reports
      security:
        - UserApiKeyAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ProjectIdParam'
        - name: testRunId
          in: path
          required: true
          description: Test run ID
          schema:
            type: string
        - name: filename
          in: query
          description: Specific filename to link to (default picks index.html or first HTML file)
          schema:
            type: string
      responses:
        '200':
          description: Signed browser URL for report viewing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BrowserUrlResponse'
              example:
                url: "https://app.gaffer.sh/reports/run_xyz789/index.html?token=eyJ..."
                filename: "index.html"
                testRunId: "run_xyz789"
                expiresAt: "2024-03-20T15:00:00Z"
                expiresInSeconds: 1800
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/ingest:
    post:
      summary: Ingest structured test data
      description: |
        Receives structured test data from the Gaffer CLI, the GitHub Action, or
        the MCP server's `upload_test_results` function.
        The payload is stored in R2 and processed asynchronously via a queue consumer.

        Returns 202 immediately with an `uploadSessionId`. Poll the upload-sessions
        endpoint to check when processing completes.

        **This endpoint is not idempotent.** Each request creates a new upload and,
        once processed, a new test run. `runId` is stored as the report's source key
        and does not deduplicate across requests, so retrying a call that may have
        already succeeded produces a duplicate run. Poll the upload-sessions endpoint
        instead of retrying blind.

        **Rate limited per project** across all callers. Exceeding the limit returns
        429 with a `Retry-After` header. `X-RateLimit-*` headers are set on every
        response.

        Accepts either credential type. A project token (`gfr_`) is already scoped to
        one project and ignores `projectId`; a user API key (`gaf_`) is not, and must
        pass `projectId`.
      operationId: ingestTestData
      tags:
        - Ingest
      security:
        - ApiKeyAuth: []
        - UserApiKeyAuth: []
      parameters:
        - name: projectId
          in: query
          required: false
          description: |
            Target project. Required when authenticating with a user API key (`gaf_`);
            omitted for project tokens, which resolve their own project.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IngestPayload'
            example:
              runId: "550e8400-e29b-41d4-a716-446655440000"
              framework: "vitest"
              branch: "main"
              commitSha: "abc123def456"
              startedAt: "2026-02-22T10:00:00Z"
              finishedAt: "2026-02-22T10:01:30Z"
              summary:
                total: 42
                passed: 40
                failed: 1
                skipped: 1
                durationMs: 90000
              tests:
                - name: "src/auth.test.ts > login > should return 200"
                  status: "passed"
                  durationMs: 150
                  filePath: "src/auth.test.ts"
                - name: "src/auth.test.ts > login > should reject bad credentials"
                  status: "failed"
                  durationMs: 200
                  filePath: "src/auth.test.ts"
                  error: "Expected status 401, got 500"
              coverage:
                format: "lcov"
                lines:
                  covered: 450
                  total: 600
                branches:
                  covered: 120
                  total: 200
                functions:
                  covered: 80
                  total: 100
                files:
                  - path: "src/auth.ts"
                    lines: { covered: 30, total: 40 }
                    branches: { covered: 8, total: 12 }
                    functions: { covered: 5, total: 6 }
      responses:
        '202':
          description: Payload accepted for async processing
          content:
            application/json:
              schema:
                type: object
                properties:
                  uploadSessionId:
                    type: string
                    description: ID of the upload session — use the upload-sessions endpoint to poll for completion
              example:
                uploadSessionId: "upl_v1a2b3c4d5e6f7g8h9i0j"
        '400':
          description: Invalid payload (validation failed), or a user API key was used without `projectId`
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          description: Project upload rate limit exceeded. Retry after the interval in the `Retry-After` header.

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: "Project token (format: gfr_<64 hex chars>). Scoped to a single project."

    UserApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: "User API key (format: gaf_<64 hex chars>). Access all projects in your organizations."

  parameters:
    ProjectIdParam:
      name: projectId
      in: path
      required: true
      description: Project ID. Use the list projects endpoint to find project IDs.
      schema:
        type: string

    DaysParam:
      name: days
      in: query
      description: |
        Analysis period in days. Clamped by plan tier (Free: 30, Pro: 90, Team: 180).
        Check the `meta` field in the response to see if clamping was applied.
      schema:
        type: integer
        minimum: 1
        maximum: 365
        default: 30

    LimitParam:
      name: limit
      in: query
      description: Maximum number of results to return
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50

    ThresholdParam:
      name: threshold
      in: query
      description: Minimum flip rate to be considered flaky (0-1)
      schema:
        type: number
        minimum: 0
        maximum: 1
        default: 0.1

  schemas:
    IngestPayload:
      type: object
      required: [runId, framework, startedAt, finishedAt, summary, tests]
      properties:
        runId:
          type: string
          format: uuid
          description: Local UUID used for deduplication
        framework:
          type: string
          description: Test framework identifier (e.g. "vitest", "playwright", "jest")
        branch:
          type: string
          description: Git branch name
        commitSha:
          type: string
          description: Git commit SHA
        ciProvider:
          type: string
          description: CI provider (e.g. "github-actions")
        startedAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the run started
        finishedAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the run finished
        summary:
          type: object
          required: [total, passed, failed, skipped, durationMs]
          properties:
            total:
              type: integer
            passed:
              type: integer
            failed:
              type: integer
            skipped:
              type: integer
            durationMs:
              type: number
        tests:
          type: array
          minItems: 1
          maxItems: 50000
          items:
            type: object
            required: [name, status, durationMs]
            properties:
              name:
                type: string
              status:
                type: string
                enum: [passed, failed, skipped, todo]
              durationMs:
                type: number
              filePath:
                type: string
              error:
                type: string
              retryCount:
                type: integer
              flaky:
                type: boolean
        coverage:
          type: object
          description: Optional coverage data collected during the test run
          required: [format, lines, branches, functions, files]
          properties:
            format:
              type: string
              description: Coverage format (e.g. "lcov")
            lines:
              $ref: '#/components/schemas/IngestCoverageMetrics'
            branches:
              $ref: '#/components/schemas/IngestCoverageMetrics'
            functions:
              $ref: '#/components/schemas/IngestCoverageMetrics'
            files:
              type: array
              maxItems: 50000
              items:
                type: object
                required: [path, lines, branches, functions]
                properties:
                  path:
                    type: string
                  lines:
                    $ref: '#/components/schemas/IngestCoverageMetrics'
                  branches:
                    $ref: '#/components/schemas/IngestCoverageMetrics'
                  functions:
                    $ref: '#/components/schemas/IngestCoverageMetrics'

    IngestCoverageMetrics:
      type: object
      properties:
        covered:
          type: integer
        total:
          type: integer
      required:
        - covered
        - total

    Project:
      type: object
      properties:
        id:
          type: string
          description: Unique project identifier
        name:
          type: string
          description: Project name
        description:
          type: string
          nullable: true
          description: Optional project description
        retentionDays:
          type: integer
          description: Report retention period in days
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - name
        - retentionDays
        - createdAt
        - updatedAt

    UserProject:
      type: object
      description: A project with organization context, returned by user-scoped endpoints.
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        retentionDays:
          type: integer
          nullable: true
        organization:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
            slug:
              type: string
          required:
            - id
            - name
            - slug
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - name
        - organization
        - createdAt
        - updatedAt

    TestRunSummary:
      type: object
      description: Summary of a test run (excludes full test results for performance).
      properties:
        id:
          type: string
        uniqueId:
          type: string
          description: "Deduplication key for idempotent uploads (format: sessionId:filename)"
        sourceFile:
          type: string
          nullable: true
          description: Original filename from the uploaded report
        branch:
          type: string
          nullable: true
        commitSha:
          type: string
          nullable: true
        framework:
          type: string
          nullable: true
          description: "Detected test framework (e.g., playwright, vitest, jest, junit, pytest, rspec, ctrf, trx)"
        tags:
          type: object
          nullable: true
          additionalProperties:
            type: string
        summary:
          $ref: '#/components/schemas/TestSummary'
        createdAt:
          type: string
          format: date-time
      required:
        - id
        - uniqueId
        - summary
        - createdAt

    TestRun:
      allOf:
        - $ref: '#/components/schemas/TestRunSummary'
        - type: object
          properties:
            projectId:
              type: string
            updatedAt:
              type: string
              format: date-time
          required:
            - projectId
            - updatedAt

    TestSummary:
      type: object
      properties:
        passed:
          type: integer
        failed:
          type: integer
        skipped:
          type: integer
        total:
          type: integer
        durationMs:
          type: integer
          nullable: true
          description: Total duration in milliseconds
      required:
        - passed
        - failed
        - skipped
        - total

    TestRunDetailsResponse:
      type: object
      description: Detailed test results for a specific test run, including run context.
      properties:
        testRunId:
          type: string
        commitSha:
          type: string
          nullable: true
          description: Git commit SHA
        branch:
          type: string
          nullable: true
          description: Git branch name
        framework:
          type: string
          nullable: true
          description: Test framework (e.g., playwright, vitest)
        createdAt:
          type: string
          format: date-time
          description: When the test run was created
        summary:
          $ref: '#/components/schemas/TestSummary'
        tests:
          type: array
          items:
            $ref: '#/components/schemas/TestCaseDetail'
        pagination:
          $ref: '#/components/schemas/Pagination'
      required:
        - testRunId
        - createdAt
        - summary
        - tests
        - pagination

    TestCaseDetail:
      type: object
      description: Individual test case result with error details.
      properties:
        name:
          type: string
          description: Short test name
        fullName:
          type: string
          description: Full test name including describe blocks
        status:
          type: string
          enum: [passed, failed, skipped]
        durationMs:
          type: number
          nullable: true
          description: Test duration in milliseconds
        filePath:
          type: string
          nullable: true
          description: Test file path
        error:
          type: string
          nullable: true
          description: Error message for failed tests
        errorStack:
          type: string
          nullable: true
          description: Full stack trace for failed tests
      required:
        - name
        - fullName
        - status

    TestHistoryResponse:
      type: object
      properties:
        history:
          type: array
          items:
            $ref: '#/components/schemas/TestHistoryEntry'
        summary:
          type: object
          properties:
            totalRuns:
              type: integer
            passedRuns:
              type: integer
            failedRuns:
              type: integer
            passRate:
              type: number
              nullable: true
              description: Pass rate as percentage (0-100)
            searchedBy:
              type: string
              enum: [testName, filePath]
            searchValue:
              type: string
          required:
            - totalRuns
            - passedRuns
            - failedRuns
            - searchedBy
            - searchValue
        message:
          type: string
          nullable: true
          description: Human-readable message when no results are found
        meta:
          $ref: '#/components/schemas/Meta'
      required:
        - history
        - summary

    TestHistoryEntry:
      type: object
      properties:
        testRunId:
          type: string
        createdAt:
          type: string
          format: date-time
        branch:
          type: string
          nullable: true
        commitSha:
          type: string
          nullable: true
        test:
          type: object
          properties:
            name:
              type: string
            status:
              type: string
              enum: [passed, failed, skipped, pending]
            durationMs:
              type: number
            filePath:
              type: string
              nullable: true
            message:
              type: string
              nullable: true
          required:
            - name
            - status
            - durationMs
      required:
        - testRunId
        - createdAt
        - test

    TestComparisonResponse:
      type: object
      description: Before/after comparison of a specific test across commits or test runs.
      properties:
        testName:
          type: string
        before:
          $ref: '#/components/schemas/TestMetric'
        after:
          $ref: '#/components/schemas/TestMetric'
        change:
          type: object
          properties:
            durationMs:
              type: number
              nullable: true
              description: Duration change in milliseconds (positive = slower)
            percentChange:
              type: number
              nullable: true
              description: Percentage change in duration
            statusChanged:
              type: boolean
          required:
            - statusChanged
      required:
        - testName
        - before
        - after
        - change

    TestMetric:
      type: object
      properties:
        testRunId:
          type: string
        commit:
          type: string
          nullable: true
        branch:
          type: string
          nullable: true
        status:
          type: string
          enum: [passed, failed, skipped]
        durationMs:
          type: number
          nullable: true
        createdAt:
          type: string
          format: date-time
      required:
        - testRunId
        - status
        - createdAt

    HealthSummary:
      type: object
      description: Project health analytics summary.
      properties:
        projectId:
          type: string
        projectName:
          type: string
        period:
          type: object
          properties:
            days:
              type: integer
            start:
              type: string
              format: date-time
            end:
              type: string
              format: date-time
          required:
            - days
            - start
            - end
        healthScore:
          type: integer
          minimum: 0
          maximum: 100
          description: Overall health score (0-100)
        passRate:
          type: number
          nullable: true
          description: Average pass rate as percentage
        testRunCount:
          type: integer
          description: Number of test runs in the period
        totalTests:
          type: integer
          description: Total test executions
        flakyTestCount:
          type: integer
          description: Number of flaky tests detected
        trend:
          type: string
          enum: [up, down, stable]
          description: Pass rate trend direction
        computedAt:
          type: string
          format: date-time
      required:
        - projectId
        - projectName
        - period
        - healthScore
        - testRunCount
        - totalTests
        - flakyTestCount
        - trend
        - computedAt

    FlakyTest:
      type: object
      properties:
        name:
          type: string
          description: Full test name
        flipRate:
          type: number
          description: Rate of pass/fail transitions (0-1)
        flipCount:
          type: integer
          description: Number of state transitions
        totalRuns:
          type: integer
          description: Total test executions
        lastSeen:
          type: string
          format: date-time
          description: When the test was last executed
        flakinessScore:
          type: number
          description: Composite flakiness score (0-1) combining flip proximity, failure rate, and duration variability. Higher values indicate more problematic flaky tests.
      required:
        - name
        - flipRate
        - flipCount
        - totalRuns
        - lastSeen
        - flakinessScore

    FlakyTestsResponse:
      type: object
      properties:
        flakyTests:
          type: array
          items:
            $ref: '#/components/schemas/FlakyTest'
        summary:
          type: object
          properties:
            threshold:
              type: number
              description: Flip rate threshold used
            totalFlaky:
              type: integer
              description: Total number of flaky tests found
            period:
              type: integer
              description: Analysis period in days
          required:
            - threshold
            - totalFlaky
            - period
        meta:
          $ref: '#/components/schemas/Meta'
      required:
        - flakyTests
        - summary

    SearchFailuresResponse:
      type: object
      properties:
        matches:
          type: array
          items:
            $ref: '#/components/schemas/SearchFailureMatch'
        total:
          type: integer
          description: Number of matches returned
        truncated:
          type: boolean
          description: >-
            True when the scan caps (at most 100 test runs, plus `limit`) stopped
            the scan short of the full window, so the list is incomplete
        query:
          type: string
          nullable: true
          description: The search query used, or null when every failure was returned
      required:
        - matches
        - total
        - truncated
        - query

    SearchFailureMatch:
      type: object
      properties:
        testName:
          type: string
          description: Full test name
        testRunId:
          type: string
          description: ID of the test run containing the failure
        branch:
          type: string
          nullable: true
          description: Git branch name
        commitSha:
          type: string
          nullable: true
          description: Git commit SHA
        errorMessage:
          type: string
          nullable: true
          description: Error message from the test failure
        errorStack:
          type: string
          nullable: true
          description: Error stack trace (truncated to 500 chars)
        createdAt:
          type: string
          format: date-time
          description: When the test run was created
      required:
        - testName
        - testRunId
        - branch
        - commitSha
        - errorMessage
        - errorStack
        - createdAt

    SlowestTest:
      type: object
      properties:
        name:
          type: string
          description: Short test name
        fullName:
          type: string
          description: Full test name including describe blocks
        filePath:
          type: string
          nullable: true
        framework:
          type: string
          nullable: true
        avgDurationMs:
          type: integer
          description: Average duration in milliseconds
        p95DurationMs:
          type: integer
          description: 95th percentile duration in milliseconds
        runCount:
          type: integer
          description: Number of times this test has run
      required:
        - name
        - fullName
        - avgDurationMs
        - p95DurationMs
        - runCount

    SlowestTestsResponse:
      type: object
      properties:
        slowestTests:
          type: array
          items:
            $ref: '#/components/schemas/SlowestTest'
        summary:
          type: object
          properties:
            projectId:
              type: string
            projectName:
              type: string
            period:
              type: integer
              description: Analysis period in days
            totalReturned:
              type: integer
          required:
            - projectId
            - projectName
            - period
            - totalReturned
        meta:
          $ref: '#/components/schemas/Meta'
      required:
        - slowestTests
        - summary

    CoverageSummaryResponse:
      type: object
      description: Coverage summary with trend data. When `hasCoverage` is false, only `message` and `totalReports` are present.
      properties:
        hasCoverage:
          type: boolean
          description: Whether the project has any coverage data
        current:
          type: object
          description: Current coverage percentages (present when hasCoverage is true)
          properties:
            lines:
              type: number
            branches:
              type: number
            functions:
              type: number
          required:
            - lines
            - branches
            - functions
        trend:
          type: object
          description: Coverage trend over the analysis period
          properties:
            direction:
              type: string
              enum: [up, down, stable]
            change:
              type: number
              description: Percentage point change
          required:
            - direction
            - change
        totalReports:
          type: integer
        latestReportDate:
          type: string
          format: date-time
          nullable: true
        lowestCoverageFiles:
          type: array
          description: Up to 5 files with lowest coverage
          items:
            type: object
            properties:
              path:
                type: string
              coverage:
                type: integer
                description: Line coverage percentage (0-100)
            required:
              - path
              - coverage
        message:
          type: string
          nullable: true
          description: Human-readable message (present when no coverage data exists)
        meta:
          $ref: '#/components/schemas/Meta'
      required:
        - hasCoverage
        - totalReports

    CoverageFilesResponse:
      type: object
      description: Per-file coverage data. When `hasCoverage` is false, files array is empty.
      properties:
        hasCoverage:
          type: boolean
        files:
          type: array
          items:
            $ref: '#/components/schemas/CoverageFile'
        pagination:
          $ref: '#/components/schemas/Pagination'
        overallCoverage:
          type: number
          nullable: true
          description: Overall project coverage percentage
        message:
          type: string
          nullable: true
      required:
        - hasCoverage
        - files
        - pagination

    CoverageFile:
      type: object
      description: Per-file coverage metrics.
      properties:
        path:
          type: string
        lines:
          $ref: '#/components/schemas/CoverageMetric'
        branches:
          $ref: '#/components/schemas/CoverageMetric'
        functions:
          $ref: '#/components/schemas/CoverageMetric'
      required:
        - path
        - lines
        - branches
        - functions

    CoverageMetric:
      type: object
      properties:
        covered:
          type: integer
        total:
          type: integer
        percentage:
          type: number
      required:
        - covered
        - total
        - percentage

    CoverageRiskAreasResponse:
      type: object
      description: Files with both low coverage and test failures, sorted by risk score.
      properties:
        hasCoverage:
          type: boolean
        hasTestResults:
          type: boolean
        riskAreas:
          type: array
          items:
            $ref: '#/components/schemas/RiskArea'
        message:
          type: string
          nullable: true
        analysisParams:
          type: object
          properties:
            days:
              type: integer
            coverageThreshold:
              type: number
          required:
            - days
            - coverageThreshold
        meta:
          $ref: '#/components/schemas/Meta'
      required:
        - hasCoverage
        - hasTestResults
        - riskAreas
        - analysisParams

    RiskArea:
      type: object
      properties:
        filePath:
          type: string
        coverage:
          type: number
          description: Line coverage percentage (0-100)
        failureCount:
          type: integer
          description: Number of test failures touching this file
        riskScore:
          type: number
          description: Composite risk score (0-100, higher = more risky)
        testNames:
          type: array
          items:
            type: string
          description: Names of failing tests associated with this file
      required:
        - filePath
        - coverage
        - failureCount
        - riskScore
        - testNames

    BrowserUrlResponse:
      type: object
      description: Signed URL for viewing an HTML test report in the browser.
      properties:
        url:
          type: string
          format: uri
          description: Browser-navigable URL with signed token
        filename:
          type: string
          description: The report filename being linked to
        testRunId:
          type: string
        expiresAt:
          type: string
          format: date-time
          description: When the signed URL expires
        expiresInSeconds:
          type: integer
          description: Seconds until the URL expires
      required:
        - url
        - filename
        - testRunId
        - expiresAt
        - expiresInSeconds

    FailedTest:
      type: object
      description: A failed test within a failure cluster.
      properties:
        name:
          type: string
          description: Short test name
        fullName:
          type: string
          description: Full test name including describe blocks
        errorMessage:
          type: string
          description: The error message for this test
        filePath:
          type: string
          nullable: true
          description: Test file path
      required:
        - name
        - fullName
        - errorMessage

    FailureCategory:
      type: object
      nullable: true
      description: AI-generated categorization of a failure cluster's root cause.
      properties:
        category:
          type: string
          enum: [product_bug, automation_bug, environment_issue]
          description: The classified root cause category
        confidence:
          type: string
          enum: [high, medium]
          description: Confidence level of the classification
        reasoning:
          type: string
          description: Brief explanation of why this category was chosen
      required:
        - category
        - confidence
        - reasoning

    FailureCluster:
      type: object
      description: A group of failed tests sharing a similar error message.
      properties:
        representativeError:
          type: string
          description: The error message representing this cluster
        count:
          type: integer
          description: Number of tests in this cluster
        tests:
          type: array
          items:
            $ref: '#/components/schemas/FailedTest'
        similarity:
          type: number
          description: Similarity threshold used for clustering (0-1)
        aiCategory:
          $ref: '#/components/schemas/FailureCategory'
      required:
        - representativeError
        - count
        - tests
        - similarity

    FailureClustersResponse:
      type: object
      description: Failed tests grouped by error message similarity.
      properties:
        clusters:
          type: array
          items:
            $ref: '#/components/schemas/FailureCluster'
        totalFailures:
          type: integer
          description: Total number of failed tests across all clusters
      required:
        - clusters
        - totalFailures

    UploadSessionSummary:
      type: object
      description: Upload session summary with processing status.
      properties:
        id:
          type: string
        projectId:
          type: string
        uniqueId:
          type: string
        tags:
          type: object
          nullable: true
          additionalProperties:
            type: string
        commitSha:
          type: string
          nullable: true
        branch:
          type: string
          nullable: true
        processingStatus:
          type: string
          enum: [pending, processing, completed, error]
          description: "Current processing status"
        pendingFileCount:
          type: integer
          description: Number of files still being processed
        failedFileCount:
          type: integer
          description: Number of files that failed processing
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - projectId
        - uniqueId
        - processingStatus
        - pendingFileCount
        - failedFileCount
        - createdAt
        - updatedAt

    UploadSessionsResponse:
      type: object
      properties:
        sessions:
          type: array
          items:
            $ref: '#/components/schemas/UploadSessionSummary'
        pagination:
          $ref: '#/components/schemas/Pagination'
      required:
        - sessions
        - pagination

    UploadSessionDetailResponse:
      type: object
      description: Upload session with linked test runs and coverage reports.
      properties:
        session:
          $ref: '#/components/schemas/UploadSessionSummary'
        testRuns:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              framework:
                type: string
                nullable: true
              summary:
                $ref: '#/components/schemas/TestSummary'
              createdAt:
                type: string
                format: date-time
            required:
              - id
              - summary
              - createdAt
        coverageReports:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              format:
                type: string
              createdAt:
                type: string
                format: date-time
            required:
              - id
              - format
              - createdAt
      required:
        - session
        - testRuns
        - coverageReports

    Meta:
      type: object
      nullable: true
      description: |
        Present when query parameters were clamped by plan tier limits.
        Only included in the response when clamping actually occurred.
      properties:
        appliedDays:
          type: integer
          description: The actual days value used after clamping
        requestedDays:
          type: integer
          description: The days value originally requested
        appliedLimit:
          type: integer
          description: The actual limit value used after clamping
        requestedLimit:
          type: integer
          description: The limit value originally requested
        clampedByPlan:
          type: boolean
          description: Always true when this object is present

    Pagination:
      type: object
      properties:
        limit:
          type: integer
        offset:
          type: integer
        total:
          type: integer
        hasMore:
          type: boolean
      required:
        - limit
        - offset
        - total
        - hasMore

    Error:
      type: object
      description: >
        The error envelope emitted by the h3/Nitro runtime. Note that `error` is a
        boolean flag, not a nested object: the human-readable reason is in
        `statusMessage`, and `message` is usually an empty string.
      properties:
        error:
          type: boolean
          description: Always `true` on an error response.
          example: true
        url:
          type: string
          description: Absolute URL of the request that failed.
          example: https://app.gaffer.sh/api/v1/user/projects
        statusCode:
          type: integer
          description: HTTP status code, repeated in the body.
          example: 401
        statusMessage:
          type: string
          description: The reason to surface to a human or log. Branch on `statusCode`, not on this string.
          example: Invalid API key format. Expected user API key (gaf_...).
        message:
          type: string
          description: Reserved for detail most errors do not carry. Frequently an empty string.
          example: ""
      required:
        - error
        - statusCode

  responses:
    BadRequest:
      description: Invalid request parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: true
            url: https://app.gaffer.sh/api/v1/user/projects/abc123/health
            statusCode: 400
            statusMessage: Invalid query parameters
            message: ""

    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: true
            url: https://app.gaffer.sh/api/v1/user/projects
            statusCode: 401
            statusMessage: Invalid API key format. Expected user API key (gaf_...).
            message: ""

    Forbidden:
      description: Authenticated but insufficient permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: true
            url: https://app.gaffer.sh/api/v1/user/projects/abc123/health
            statusCode: 403
            statusMessage: You do not have access to this organization
            message: ""

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: true
            url: https://app.gaffer.sh/api/v1/user/projects/abc123/health
            statusCode: 404
            statusMessage: Resource not found
            message: ""

    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds until rate limit resets
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: RATE_LIMIT_EXCEEDED
              message: Rate limit exceeded. Try again later.

tags:
  - name: Project
    description: Project-scoped endpoints (project token auth)
  - name: User Projects
    description: User-scoped project and test run endpoints
  - name: Test History
    description: Test run details, per-test history, and comparisons
  - name: Analytics
    description: Flaky test detection and performance analysis
  - name: Coverage
    description: Code coverage metrics and risk analysis
  - name: Reports
    description: HTML test report viewing
  - name: Upload Sessions
    description: Upload session status and linked results
