openapi: 3.1.0
info:
  title: Clean Release API
  version: 0.1.0
  description: API-first contract for clean release candidate lifecycle, compliance runs, approvals and publications.
servers:
  - url: /api
x-interface-actor-mapping:
  description: External API request payloads use domain-specific actor fields; interface adapters may accept a unified actor context internally but must persist canonical *_by fields.
  mappings:
    candidate_register: created_by
    manifest_build: created_by
    compliance_run: requested_by
    approval_or_reject: decided_by
    publish: published_by
    revoke: actor
paths:
  /clean-release/candidates:
    post:
      summary: Register a release candidate
      operationId: registerCleanReleaseCandidate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterCandidateRequest'
      responses:
        '201':
          description: Candidate created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CandidateOverview'
    get:
      summary: List release candidates
      operationId: listCleanReleaseCandidates
      responses:
        '200':
          description: Candidate list
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/CandidateOverview'
  /clean-release/candidates/{candidate_id}:
    get:
      summary: Get candidate overview
      operationId: getCleanReleaseCandidate
      parameters:
        - $ref: '#/components/parameters/CandidateId'
      responses:
        '200':
          description: Candidate overview
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CandidateOverview'
        '404':
          $ref: '#/components/responses/NotFound'
  /clean-release/candidates/{candidate_id}/artifacts/import:
    post:
      summary: Import candidate artifacts
      operationId: importCleanReleaseArtifacts
      parameters:
        - $ref: '#/components/parameters/CandidateId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportArtifactsRequest'
      responses:
        '200':
          description: Artifacts imported
          content:
            application/json:
              schema:
                type: object
                required: [candidate_id, imported_count, status]
                properties:
                  candidate_id:
                    type: string
                  imported_count:
                    type: integer
                  status:
                    type: string
  /clean-release/candidates/{candidate_id}/artifacts:
    get:
      summary: List candidate artifacts
      operationId: listCleanReleaseArtifacts
      parameters:
        - $ref: '#/components/parameters/CandidateId'
      responses:
        '200':
          description: Candidate artifacts
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/CandidateArtifact'
  /clean-release/candidates/{candidate_id}/manifest:
    post:
      summary: Build a new manifest snapshot
      operationId: buildCleanReleaseManifest
      parameters:
        - $ref: '#/components/parameters/CandidateId'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                requested_by:
                  type: string
      responses:
        '201':
          description: Manifest created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DistributionManifest'
  /clean-release/candidates/{candidate_id}/manifests:
    get:
      summary: List manifests for candidate
      operationId: listCleanReleaseManifests
      parameters:
        - $ref: '#/components/parameters/CandidateId'
      responses:
        '200':
          description: Candidate manifests
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/DistributionManifest'
  /clean-release/manifests/{manifest_id}:
    get:
      summary: Get manifest snapshot
      operationId: getCleanReleaseManifest
      parameters:
        - name: manifest_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Manifest snapshot
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DistributionManifest'
        '404':
          $ref: '#/components/responses/NotFound'
  /clean-release/candidates/{candidate_id}/compliance-runs:
    post:
      summary: Request a compliance run
      operationId: createCleanReleaseComplianceRun
      parameters:
        - $ref: '#/components/parameters/CandidateId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateComplianceRunRequest'
      responses:
        '202':
          description: Compliance run accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ComplianceRun'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
  /clean-release/compliance-runs/{run_id}:
    get:
      summary: Get compliance run status
      operationId: getCleanReleaseComplianceRun
      parameters:
        - name: run_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Compliance run
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ComplianceRun'
        '404':
          $ref: '#/components/responses/NotFound'
  /clean-release/compliance-runs/{run_id}/stages:
    get:
      summary: List stage results for a run
      operationId: listCleanReleaseComplianceStages
      parameters:
        - name: run_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Stage list
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/ComplianceStageRun'
  /clean-release/compliance-runs/{run_id}/violations:
    get:
      summary: List violations for a run
      operationId: listCleanReleaseComplianceViolations
      parameters:
        - name: run_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Violation list
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/ComplianceViolation'
  /clean-release/compliance-runs/{run_id}/report:
    get:
      summary: Get final report for a run
      operationId: getCleanReleaseComplianceReport
      parameters:
        - name: run_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Compliance report
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ComplianceReport'
        '404':
          $ref: '#/components/responses/NotFound'
  /clean-release/candidates/{candidate_id}/approve:
    post:
      summary: Approve a candidate using a report
      operationId: approveCleanReleaseCandidate
      parameters:
        - $ref: '#/components/parameters/CandidateId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApprovalRequest'
      responses:
        '200':
          description: Approval created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalDecision'
        '409':
          $ref: '#/components/responses/Conflict'
  /clean-release/candidates/{candidate_id}/reject:
    post:
      summary: Reject a candidate using a report
      operationId: rejectCleanReleaseCandidate
      parameters:
        - $ref: '#/components/parameters/CandidateId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApprovalRequest'
      responses:
        '200':
          description: Rejection created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalDecision'
        '409':
          $ref: '#/components/responses/Conflict'
  /clean-release/candidates/{candidate_id}/publish:
    post:
      summary: Publish an approved candidate
      operationId: publishCleanReleaseCandidate
      parameters:
        - $ref: '#/components/parameters/CandidateId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublishRequest'
      responses:
        '200':
          description: Publication created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicationRecord'
        '409':
          $ref: '#/components/responses/Conflict'
  /clean-release/publications/{publication_id}/revoke:
    post:
      summary: Revoke a publication
      operationId: revokeCleanReleasePublication
      parameters:
        - name: publication_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RevokePublicationRequest'
      responses:
        '200':
          description: Publication revoked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicationRecord'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
components:
  parameters:
    CandidateId:
      name: candidate_id
      in: path
      required: true
      schema:
        type: string
  responses:
    NotFound:
      description: Entity not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Conflict:
      description: Illegal lifecycle transition or conflicting state
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    RegisterCandidateRequest:
      type: object
      required: [candidate_id, version, source_snapshot_ref, created_by]
      properties:
        candidate_id:
          type: string
        version:
          type: string
        source_snapshot_ref:
          type: string
        build_id:
          type: string
        created_by:
          type: string
        provenance_ref:
          type: string
    ImportArtifactsRequest:
      type: object
      required: [artifacts]
      properties:
        artifacts:
          type: array
          items:
            $ref: '#/components/schemas/CandidateArtifactInput'
    CandidateArtifactInput:
      type: object
      required: [path, sha256, size]
      properties:
        path:
          type: string
        sha256:
          type: string
        size:
          type: integer
        declared_category:
          type: string
        detected_category:
          type: string
        source_uri:
          type: string
        source_host:
          type: string
        metadata:
          type: object
          additionalProperties: true
    CreateComplianceRunRequest:
      type: object
      required: [requested_by]
      properties:
        manifest_id:
          type: string
        requested_by:
          type: string
    ApprovalRequest:
      type: object
      required: [report_id, decided_by]
      properties:
        report_id:
          type: string
        decided_by:
          type: string
        comment:
          type: string
    PublishRequest:
      type: object
      required: [report_id, target_channel, actor]
      properties:
        report_id:
          type: string
        target_channel:
          type: string
        actor:
          type: string
    RevokePublicationRequest:
      type: object
      required: [actor, reason]
      properties:
        actor:
          type: string
        reason:
          type: string
    CandidateOverview:
      type: object
      required: [candidate_id, version, source_snapshot_ref, status]
      properties:
        candidate_id:
          type: string
        version:
          type: string
        source_snapshot_ref:
          type: string
        status:
          type: string
        latest_manifest_id:
          type: string
        latest_manifest_digest:
          type: string
        latest_run_id:
          type: string
        latest_run_status:
          type: string
        latest_report_id:
          type: string
        latest_report_final_status:
          type: string
        latest_policy_snapshot_id:
          type: string
        latest_policy_version:
          type: string
        latest_registry_snapshot_id:
          type: string
        latest_registry_version:
          type: string
        latest_approval_decision:
          type: string
        latest_publication_id:
          type: string
        latest_publication_status:
          type: string
    CandidateArtifact:
      type: object
      required: [id, candidate_id, path, sha256, size, metadata]
      properties:
        id:
          type: string
        candidate_id:
          type: string
        path:
          type: string
        sha256:
          type: string
        size:
          type: integer
        detected_category:
          type: string
        declared_category:
          type: string
        source_uri:
          type: string
        source_host:
          type: string
        metadata:
          type: object
          additionalProperties: true
    DistributionManifest:
      type: object
      required: [id, candidate_id, manifest_version, manifest_digest, artifacts_digest, created_at, created_by, source_snapshot_ref, content_json, immutable]
      properties:
        id:
          type: string
        candidate_id:
          type: string
        manifest_version:
          type: integer
        manifest_digest:
          type: string
        artifacts_digest:
          type: string
        created_at:
          type: string
          format: date-time
        created_by:
          type: string
        source_snapshot_ref:
          type: string
        content_json:
          type: object
          additionalProperties: true
        immutable:
          type: boolean
    ComplianceRun:
      type: object
      required: [run_id, candidate_id, status, task_id]
      properties:
        run_id:
          type: string
        candidate_id:
          type: string
        manifest_id:
          type: string
        manifest_digest:
          type: string
        policy_snapshot_id:
          type: string
        registry_snapshot_id:
          type: string
        requested_by:
          type: string
        requested_at:
          type: string
          format: date-time
        started_at:
          type: string
          format: date-time
        finished_at:
          type: string
          format: date-time
        status:
          type: string
        final_status:
          type: string
        failure_reason:
          type: string
        report_id:
          type: string
        task_id:
          type: string
    ComplianceStageRun:
      type: object
      required: [id, run_id, stage_name, status, details_json]
      properties:
        id:
          type: string
        run_id:
          type: string
        stage_name:
          type: string
        status:
          type: string
        started_at:
          type: string
          format: date-time
        finished_at:
          type: string
          format: date-time
        decision:
          type: string
        details_json:
          type: object
          additionalProperties: true
    ComplianceViolation:
      type: object
      required: [id, run_id, stage_name, code, severity, message, evidence_json]
      properties:
        id:
          type: string
        run_id:
          type: string
        stage_name:
          type: string
        code:
          type: string
        severity:
          type: string
        artifact_path:
          type: string
        artifact_sha256:
          type: string
        message:
          type: string
        evidence_json:
          type: object
          additionalProperties: true
    ComplianceReport:
      type: object
      required: [report_id, run_id, candidate_id, final_status, summary_json, generated_at, immutable]
      properties:
        report_id:
          type: string
        run_id:
          type: string
        candidate_id:
          type: string
        final_status:
          type: string
        summary_json:
          type: object
          additionalProperties: true
        generated_at:
          type: string
          format: date-time
        immutable:
          type: boolean
    ApprovalDecision:
      type: object
      required: [id, candidate_id, report_id, decision, decided_by, decided_at]
      properties:
        id:
          type: string
        candidate_id:
          type: string
        report_id:
          type: string
        decision:
          type: string
        decided_by:
          type: string
        decided_at:
          type: string
          format: date-time
        comment:
          type: string
    PublicationRecord:
      type: object
      required: [id, candidate_id, report_id, published_by, published_at, target_channel, status]
      properties:
        id:
          type: string
        candidate_id:
          type: string
        report_id:
          type: string
        published_by:
          type: string
        published_at:
          type: string
          format: date-time
        target_channel:
          type: string
        publication_ref:
          type: string
        status:
          type: string
    ErrorResponse:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
        message:
          type: string
        details:
          type: object
          additionalProperties: true
