openapi: 3.1.0
info:
  title: ContextDB Memory CI API
  version: 1.0.0-alpha.1
  description: |
    Public transport contract for running bounded Memory CI evaluation suites
    against the hosted ContextDB service.

    Authenticate with `Authorization: Bearer cbe_...`. Evaluation tokens are
    server credentials and must be kept in a CI secret store. This contract
    exposes only run submission, safe status polling, cancellation, and
    content-free export. It never returns suite names, queries, assertions,
    memory content, raw failures, or case details.

    Hosted Alpha limitations: no availability SLA is offered; limits are
    enforced per run; status and export retention may be bounded; and a run can
    finish with `error` when the hosted service cannot produce a behavioral
    result. Queue operation, lease handling, token issuance, tenant resolution,
    and suite execution are not part of this public contract or repository.
    The hosted implementation remains private.
  contact:
    name: Atoms AI
    email: gaurav@saaslabs.co
  license:
    name: Apache License 2.0
    identifier: Apache-2.0
servers:
  - url: https://api.contextdb.ai
    description: ContextDB hosted alpha
tags:
  - name: memory-ci
    description: Submit and observe content-free Memory CI runs

security:
  - EvaluationToken: []

paths:
  /evals/v1/projects/{project_id}/suites/{suite_id}/runs:
    post:
      tags: [memory-ci]
      summary: Start a Memory CI run
      operationId: createMemoryCiRun
      description: |
        Starts one bounded evaluation run. Reusing an `Idempotency-Key` with
        the same request safely returns the same run. Omit `baseline_run_id`
        to use the suite's pinned baseline, if one exists.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/SuiteId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateRunRequest"
      responses:
        "202":
          description: Run accepted; the body contains safe status only
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RunStatusResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotEntitled" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }
        "502": { $ref: "#/components/responses/BadGateway" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        "504": { $ref: "#/components/responses/GatewayTimeout" }

  /evals/v1/projects/{project_id}/runs/{run_id}:
    get:
      tags: [memory-ci]
      summary: Get safe run status
      operationId: getMemoryCiRun
      description: |
        Returns aggregate status only. The response cannot contain case names,
        prompts, assertions, memory content, or raw failure text.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/RunId"
      responses:
        "200":
          description: Current safe run status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RunStatusResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotEntitled" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }
        "502": { $ref: "#/components/responses/BadGateway" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        "504": { $ref: "#/components/responses/GatewayTimeout" }

  /evals/v1/projects/{project_id}/runs/{run_id}/cancel:
    post:
      tags: [memory-ci]
      summary: Request run cancellation
      operationId: cancelMemoryCiRun
      description: |
        Requests best-effort cancellation. A run may remain `cancelling` until
        in-flight work reaches a safe stopping point.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/RunId"
      responses:
        "202":
          description: Cancellation accepted; the body contains safe status only
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RunStatusResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotEntitled" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }
        "502": { $ref: "#/components/responses/BadGateway" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        "504": { $ref: "#/components/responses/GatewayTimeout" }

  /evals/v1/projects/{project_id}/runs/{run_id}/export/{format}:
    get:
      tags: [memory-ci]
      summary: Export a terminal run
      operationId: exportMemoryCiRun
      description: |
        Exports a terminal run as schema-versioned JSON or content-free JUnit
        XML. Case references are opaque IDs or hashes. Failure values are
        bounded machine codes; raw failure text and evaluated content are
        never exported.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - $ref: "#/components/parameters/RunId"
        - $ref: "#/components/parameters/ExportFormat"
      responses:
        "200":
          description: Content-free run export in the requested format
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JsonExport"
            application/xml:
              schema:
                type: string
                maxLength: 2097152
                description: |
                  Well-formed, content-free JUnit XML. Test identifiers are
                  opaque case IDs. Failure elements contain only bounded
                  machine codes and fixed protocol phrases.
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthenticated" }
        "403": { $ref: "#/components/responses/NotEntitled" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }
        "502": { $ref: "#/components/responses/BadGateway" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
        "504": { $ref: "#/components/responses/GatewayTimeout" }

components:
  securitySchemes:
    EvaluationToken:
      type: http
      scheme: bearer
      bearerFormat: cbe_
      description: |
        ContextDB evaluation token beginning with `cbe_`. Keep this server
        credential in a CI secret store and send it only in Authorization.
      x-token-pattern: "^cbe_[A-Za-z0-9._~-]{1,508}$"

  parameters:
    ProjectId:
      name: project_id
      in: path
      required: true
      description: Opaque ContextDB project identifier
      schema:
        $ref: "#/components/schemas/SafeId"
    SuiteId:
      name: suite_id
      in: path
      required: true
      description: Opaque evaluation suite identifier
      schema:
        $ref: "#/components/schemas/SafeId"
    RunId:
      name: run_id
      in: path
      required: true
      description: Opaque evaluation run identifier
      schema:
        $ref: "#/components/schemas/SafeId"
    ExportFormat:
      name: format
      in: path
      required: true
      schema:
        type: string
        enum: [json, junit]
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        Stable caller-generated key. Reuse it only for the same project,
        suite, and optional baseline.
      schema:
        type: string
        minLength: 8
        maxLength: 128
        pattern: "^(?!cbe_)[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$"

  responses:
    BadRequest:
      description: Malformed request or failed bounded validation
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthenticated:
      description: Missing, malformed, unknown, expired, or revoked evaluation token
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotEntitled:
      description: Evaluation token is valid but the project lacks hosted access
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Project, suite, run, or retained export was not found
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Conflict:
      description: |
        Idempotency key reuse conflicts with the original request, the run is
        not in a valid state for this operation, or an export is not terminal.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    PayloadTooLarge:
      description: Request or generated safe export exceeds a configured bound
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Request rate or allowed run concurrency was exceeded
      headers:
        Retry-After:
          description: Whole seconds before another request should be attempted
          schema:
            type: integer
            minimum: 1
            maximum: 86400
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    InternalError:
      description: Hosted service failed without producing a behavioral result
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    BadGateway:
      description: A hosted dependency returned an invalid response
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    ServiceUnavailable:
      description: Hosted service is temporarily unavailable
      headers:
        Retry-After:
          description: Whole seconds before another request should be attempted
          schema:
            type: integer
            minimum: 1
            maximum: 86400
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    GatewayTimeout:
      description: Hosted service timed out without producing a behavioral result
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    SafeId:
      type: string
      minLength: 1
      maxLength: 128
      pattern: "^(?!cbe_)[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"

    SafeCode:
      type: string
      minLength: 1
      maxLength: 64
      pattern: "^(?!cbe_)[a-z][a-z0-9_]{0,63}$"

    ContentHash:
      type: string
      minLength: 64
      maxLength: 71
      pattern: "^(sha256:)?[a-f0-9]{64}$"

    CreateRunRequest:
      type: object
      additionalProperties: false
      properties:
        baseline_run_id:
          anyOf:
            - $ref: "#/components/schemas/SafeId"
            - type: "null"
          description: Explicit baseline. Omit to use the suite's pinned baseline.

    RunState:
      type: string
      enum:
        - queued
        - running
        - retry_wait
        - cancelling
        - passed
        - failed
        - error
        - cancelled

    RegressionState:
      type: string
      enum: [no_baseline, unchanged, regressed]

    TerminalReason:
      type: string
      enum:
        - completed
        - assertion_failed
        - case_error
        - deadline_exceeded
        - max_attempts
        - worker_lost
        - cancelled
        - comparison_result_budget_exceeded
        - suite_result_budget_exceeded
        - worker_terminal_record_missing
        - internal_error
        - process_interrupted

    Counts:
      type: object
      additionalProperties: false
      required: [total, queued, running, passed, failed, error, cancelled]
      properties:
        total: { type: integer, minimum: 0, maximum: 50 }
        queued: { type: integer, minimum: 0, maximum: 50 }
        running: { type: integer, minimum: 0, maximum: 50 }
        passed: { type: integer, minimum: 0, maximum: 50 }
        failed: { type: integer, minimum: 0, maximum: 50 }
        error: { type: integer, minimum: 0, maximum: 50 }
        cancelled: { type: integer, minimum: 0, maximum: 50 }

    Progress:
      type: object
      additionalProperties: false
      required: [completed, total]
      properties:
        completed: { type: integer, minimum: 0, maximum: 50 }
        total: { type: integer, minimum: 0, maximum: 50 }

    Limits:
      type: object
      additionalProperties: false
      required:
        - max_cases
        - max_concurrency
        - wall_clock_seconds
        - case_timeout_seconds
        - max_case_result_bytes
        - max_result_bytes
      properties:
        max_cases: { type: integer, minimum: 1, maximum: 50 }
        max_concurrency: { type: integer, minimum: 1, maximum: 4 }
        wall_clock_seconds:
          type: number
          exclusiveMinimum: 0
          maximum: 60
        case_timeout_seconds:
          type: number
          exclusiveMinimum: 0
          maximum: 15
        max_case_result_bytes:
          type: integer
          minimum: 1
          maximum: 32768
        max_result_bytes:
          type: integer
          minimum: 1
          maximum: 2097152

    NullableTimestamp:
      anyOf:
        - type: string
          format: date-time
          maxLength: 40
        - type: "null"

    RunStatus:
      type: object
      additionalProperties: false
      required:
        - id
        - suite_id
        - baseline_run_id
        - project_id
        - status
        - regression_status
        - counts
        - progress
        - terminal_reason
        - limits
        - created_at
        - started_at
        - completed_at
        - available_at
        - deadline_at
      properties:
        id: { $ref: "#/components/schemas/SafeId" }
        suite_id: { $ref: "#/components/schemas/SafeId" }
        baseline_run_id:
          anyOf:
            - $ref: "#/components/schemas/SafeId"
            - type: "null"
        project_id: { $ref: "#/components/schemas/SafeId" }
        status: { $ref: "#/components/schemas/RunState" }
        regression_status: { $ref: "#/components/schemas/RegressionState" }
        counts: { $ref: "#/components/schemas/Counts" }
        progress: { $ref: "#/components/schemas/Progress" }
        terminal_reason:
          anyOf:
            - $ref: "#/components/schemas/TerminalReason"
            - type: "null"
        limits: { $ref: "#/components/schemas/Limits" }
        created_at:
          type: string
          format: date-time
          maxLength: 40
        started_at: { $ref: "#/components/schemas/NullableTimestamp" }
        completed_at: { $ref: "#/components/schemas/NullableTimestamp" }
        available_at: { $ref: "#/components/schemas/NullableTimestamp" }
        deadline_at: { $ref: "#/components/schemas/NullableTimestamp" }

    RunStatusResponse:
      type: object
      additionalProperties: false
      required: [suite_run]
      properties:
        suite_run:
          $ref: "#/components/schemas/RunStatus"

    TerminalRunState:
      type: string
      enum: [passed, failed, error, cancelled]

    ActionOutcome:
      type: string
      enum: [act, ask, abstain]

    NullableActionOutcome:
      anyOf:
        - $ref: "#/components/schemas/ActionOutcome"
        - type: "null"

    ChangeState:
      type: string
      enum: [changed, unchanged]

    CaseStatusComparison:
      type: object
      additionalProperties: false
      required: [change, baseline, current]
      properties:
        change: { $ref: "#/components/schemas/ChangeState" }
        baseline: { $ref: "#/components/schemas/TerminalRunState" }
        current: { $ref: "#/components/schemas/TerminalRunState" }

    EvidenceIdComparison:
      type: object
      additionalProperties: false
      required: [change, added, removed]
      properties:
        change: { $ref: "#/components/schemas/ChangeState" }
        added:
          type: array
          maxItems: 256
          items: { $ref: "#/components/schemas/SafeId" }
        removed:
          type: array
          maxItems: 256
          items: { $ref: "#/components/schemas/SafeId" }

    ActionOutcomeComparison:
      type: object
      additionalProperties: false
      required: [change, baseline, current]
      properties:
        change: { $ref: "#/components/schemas/ChangeState" }
        baseline: { $ref: "#/components/schemas/NullableActionOutcome" }
        current: { $ref: "#/components/schemas/NullableActionOutcome" }

    CaseComparison:
      type: object
      additionalProperties: false
      required: [status, case_status, evidence_ids, action_outcome]
      properties:
        status:
          type: string
          enum: [unchanged, regressed]
        case_status:
          $ref: "#/components/schemas/CaseStatusComparison"
        evidence_ids:
          $ref: "#/components/schemas/EvidenceIdComparison"
        action_outcome:
          $ref: "#/components/schemas/ActionOutcomeComparison"

    ExportSummary:
      type: object
      additionalProperties: false
      required: [total, passed, failed, error, queued, running]
      properties:
        total: { type: integer, minimum: 0, maximum: 50 }
        passed: { type: integer, minimum: 0, maximum: 50 }
        failed: { type: integer, minimum: 0, maximum: 50 }
        error: { type: integer, minimum: 0, maximum: 50 }
        queued: { type: integer, minimum: 0, maximum: 50 }
        running: { type: integer, minimum: 0, maximum: 50 }
        cancelled: { type: integer, minimum: 0, maximum: 50 }

    ExportCase:
      type: object
      additionalProperties: false
      required:
        - case_id
        - snapshot_hash
        - status
        - attempts
        - latency_ms
        - failure_codes
        - error_code
        - evidence_ids
        - evidence_hashes
        - actual_outcome
        - comparison
      properties:
        case_id: { $ref: "#/components/schemas/SafeId" }
        snapshot_hash: { $ref: "#/components/schemas/ContentHash" }
        status: { $ref: "#/components/schemas/TerminalRunState" }
        attempts: { type: integer, minimum: 0, maximum: 100 }
        latency_ms: { type: integer, minimum: 0, maximum: 86400000 }
        failure_codes:
          type: array
          maxItems: 256
          items: { $ref: "#/components/schemas/SafeCode" }
        error_code:
          anyOf:
            - $ref: "#/components/schemas/SafeCode"
            - type: "null"
        evidence_ids:
          type: array
          maxItems: 256
          items: { $ref: "#/components/schemas/SafeId" }
        evidence_hashes:
          type: array
          maxItems: 256
          items: { $ref: "#/components/schemas/ContentHash" }
        actual_outcome: { $ref: "#/components/schemas/NullableActionOutcome" }
        comparison:
          anyOf:
            - $ref: "#/components/schemas/CaseComparison"
            - type: "null"

    JsonExport:
      type: object
      additionalProperties: false
      required:
        - schema_version
        - suite_run_id
        - suite_id
        - status
        - regression_status
        - baseline_run_id
        - summary
        - limits
        - terminal_reason
        - created_at
        - started_at
        - completed_at
        - cases
      properties:
        schema_version:
          type: integer
          const: 1
        suite_run_id: { $ref: "#/components/schemas/SafeId" }
        suite_id: { $ref: "#/components/schemas/SafeId" }
        status: { $ref: "#/components/schemas/TerminalRunState" }
        regression_status:
          $ref: "#/components/schemas/RegressionState"
        baseline_run_id:
          anyOf:
            - $ref: "#/components/schemas/SafeId"
            - type: "null"
        summary: { $ref: "#/components/schemas/ExportSummary" }
        limits: { $ref: "#/components/schemas/Limits" }
        terminal_reason:
          anyOf:
            - $ref: "#/components/schemas/TerminalReason"
            - type: "null"
        created_at:
          type: string
          format: date-time
          maxLength: 40
        started_at: { $ref: "#/components/schemas/NullableTimestamp" }
        completed_at: { $ref: "#/components/schemas/NullableTimestamp" }
        cases:
          type: array
          maxItems: 50
          items:
            $ref: "#/components/schemas/ExportCase"

    Error:
      type: object
      additionalProperties: false
      required: [code, message]
      description: |
        Bounded protocol error. It contains no evaluated content, assertions,
        case details, raw dependency failures, or credentials.
      properties:
        code: { $ref: "#/components/schemas/SafeCode" }
        message:
          type: string
          minLength: 1
          maxLength: 512
        request_id:
          anyOf:
            - $ref: "#/components/schemas/SafeId"
            - type: "null"
