openapi: 3.1.0

info:
  title: CXR Sextants
  version: 0.3.0
  summary: Zone-based detection of cavities, infiltrates and nodules on chest radiographs.
  description: |
    `cxr_sextants` is one application of the **LungExpert** platform. Every application shares
    the same case lifecycle and the same URL shape, `/app/{app_label}/...`. This document covers
    `cxr_sextants` only; the platform's other applications are out of scope here.

    For each of the six lung zones (`R1`–`R3`, `L1`–`L3`) the application returns a probability
    and a binary label for three lesion types: cavities (`cavs`), infiltrates (`dens`) and
    nodules (`nods`). The binary label is taken at a fixed threshold of 0.5.

    **Lifecycle.** Create a case by uploading a radiograph, run the application, then read the
    case descriptor to obtain the predictions.

    **Research use only.** Not a certified medical device. Do not submit identifiable patient data.
  license:
    name: Apache-2.0
    url: https://www.apache.org/licenses/LICENSE-2.0
  contact:
    email: eduard.snezhko@gmail.com

servers:
  - url: https://lungs.org.by
    description: Institutional deployment

# TODO: confirm the mechanism. Access is gated; the platform delegates identity to Ory Kratos.
security:
  - sessionCookie: []

tags:
  - name: Cases
    description: Create, run and read a case.
  - name: Maintenance
    description: Bulk removal of data. Destructive.

paths:

  /app/app_list:
    get:
      tags: [Cases]
      summary: List the applications available on the platform
      description: |
        Platform-level route, documented here only so that callers can confirm `cxr_sextants`
        is registered. The live service returns every application it hosts.
      responses:
        "200":
          description: Application labels.
          content:
            application/json:
              schema:
                type: object
                properties:
                  app_list:
                    type: array
                    items: { type: string }
              example:
                app_list: [cxr_sextants]

  /app/cxr_sextants/new_case:
    post:
      tags: [Cases]
      summary: Create a case by uploading a radiograph
      description: Accepts a chest radiograph as a single NIfTI file.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: Chest radiograph, `.nii` or `.nii.gz`.
              required: [file]
      responses:
        "200":
          description: Case created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CaseCreated" }

  /app/cxr_sextants/new_case_url:
    post:
      tags: [Cases]
      summary: Create a case from a URL
      description: The server retrieves the radiograph itself. Accepts a NIfTI file or a zip/7z archive.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data_url:
                  type: string
                  format: uri
              required: [data_url]
      responses:
        "200":
          description: Case created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CaseCreated" }

  /app/cxr_sextants/cases_list:
    get:
      tags: [Cases]
      summary: List cases
      description: Metadata for every case belonging to this application.
      responses:
        "200":
          description: Cases.

  /app/cxr_sextants/{case_uid}:
    get:
      tags: [Cases]
      summary: Read case metadata and predictions
      description: |
        Returns the case descriptor. It holds the run's terminal state and paths to the
        artifacts; the predictions themselves are fetched from `proc_url`.

        An unknown `case_uid` yields an empty JSON object rather than a 404.
      parameters:
        - $ref: "#/components/parameters/CaseUid"
      responses:
        "200":
          description: Case descriptor.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CaseDescriptor" }

  /app/cxr_sextants/{case_uid}/run:
    get:
      tags: [Cases]
      summary: Run the analysis
      description: |
        Segments the lungs, cuts the six zones, applies the three classifiers and renders the
        overlay preview. The case must already exist.

        Note that this route changes server state while being exposed as `GET`.
      parameters:
        - $ref: "#/components/parameters/CaseUid"
      responses:
        "200":
          description: Processing finished.
          content:
            text/html:
              schema: { type: string }

  /app/cxr_sextants/{case_uid}/clean:
    get:
      tags: [Cases]
      summary: Discard processed data for a case
      description: Removes derived artifacts but keeps the uploaded image, so the case can be re-run.
      parameters:
        - $ref: "#/components/parameters/CaseUid"
      responses:
        "200":
          description: Cleaned.
          content:
            text/html:
              schema: { type: string }

  /app/cxr_sextants/{case_uid}/remove:
    get:
      tags: [Cases]
      summary: Remove a case
      description: Deletes the uploaded image together with every derived artifact.
      parameters:
        - $ref: "#/components/parameters/CaseUid"
      responses:
        "200":
          description: Removed.
          content:
            text/html:
              schema: { type: string }

  /app/cxr_sextants/clean_all:
    get:
      tags: [Maintenance]
      summary: Discard processed data for every case
      description: Destructive. Applies to all cases of this application.
      responses:
        "200":
          description: Cleaned.
          content:
            text/html:
              schema: { type: string }

  /app/cxr_sextants/remove_all:
    get:
      tags: [Maintenance]
      summary: Remove every case
      description: Destructive. Deletes all uploaded images and all derived artifacts.
      responses:
        "200":
          description: Removed.
          content:
            text/html:
              schema: { type: string }

components:

  securitySchemes:
    sessionCookie:
      type: apiKey
      in: cookie
      name: ory_kratos_session
      description: |
        Session established through the platform identity provider. Browser clients on another
        origin must send credentials with every request, and the server must return
        `Access-Control-Allow-Credentials: true` together with an explicit
        `Access-Control-Allow-Origin`.

  parameters:
    CaseUid:
      name: case_uid
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Identifier returned when the case was created.

  schemas:

    CaseCreated:
      type: object
      properties:
        case_uid:
          type: string
          format: uuid
      required: [case_uid]

    Prediction:
      type: object
      properties:
        probability:
          type: number
          minimum: 0
          maximum: 1
          description: Probability that the finding is present in this zone.
        class_label:
          type: string
          enum: [positive, negative]
          description: The label, taken at a threshold of 0.5 on `probability`.
      required: [probability, class_label]

    Zone:
      type: object
      properties:
        cavs: { $ref: "#/components/schemas/Prediction" }
        dens: { $ref: "#/components/schemas/Prediction" }
        nods: { $ref: "#/components/schemas/Prediction" }
      required: [cavs, dens, nods]

    CaseDescriptor:
      description: |
        Metadata for one case, shared by every application on the platform. It carries no
        predictions itself: it points at the artifacts the run produced. Fetch `proc_url` to
        obtain the per-zone predictions.

        All `*_url` values are server-relative paths under `/data/{app_label}/case-{case_uid}/`.
        An empty string means the artifact does not apply to this application.
      type: object
      properties:
        app_label:
          type: string
          const: cxr_sextants
        case_uid:
          type: string
          format: uuid
        case_label:
          type: string
          description: Name of the uploaded file, as submitted.
        status_text:
          type: string
          enum: [unknown, submitted, rejected, running, error, success]
          description: |
            State of the case. `submitted` and `running` are transient; `success`, `error` and
            `rejected` are terminal. `rejected` means the input was refused before processing
            began — for example, a file that is not a readable 2-D NIfTI image.
        error_text:
          type: string
          description: Empty unless `status_text` is `error` or `rejected`.
        input_url:
          type: string
          description: The radiograph as uploaded (NIfTI).
        proc_url:
          type: string
          description: Per-zone predictions. See `SextantPredictions`.
        zip_result_url:
          type: string
          description: Every artifact of the run, archived.
        input_preview_url:
          type: string
          description: PNG of the radiograph before analysis.
        proc_preview_url:
          type: string
          description: PNG of the radiograph with each zone tinted by its severity score.
        proc_rgb_url:
          type: string
          const: ""
          description: Not produced by this application.
        annotation_url:
          type: string
          const: ""
          description: Not produced by this application.
      required:
        [app_label, case_uid, case_label, status_text, error_text,
         input_url, proc_url, input_preview_url, proc_preview_url, zip_result_url]
      examples:
        - app_label: cxr_sextants
          case_uid: 9ea444e6-7628-11f1-bb8a-2387e58ac733
          case_label: 14_4_15437.dcm_norm.nii.gz
          status_text: success
          error_text: ""
          input_url: /data/cxr_sextants/case-9ea444e6-7628-11f1-bb8a-2387e58ac733/14_4_15437.dcm_norm.nii.gz
          proc_url: /data/cxr_sextants/case-9ea444e6-7628-11f1-bb8a-2387e58ac733/14_4_15437.dcm_norm-cxr_sextants.json
          zip_result_url: /data/cxr_sextants/case-9ea444e6-7628-11f1-bb8a-2387e58ac733/14_4_15437.dcm_norm-cxr_sextants-result.zip
          input_preview_url: /data/cxr_sextants/case-9ea444e6-7628-11f1-bb8a-2387e58ac733/14_4_15437.dcm_norm-preview.png
          proc_preview_url: /data/cxr_sextants/case-9ea444e6-7628-11f1-bb8a-2387e58ac733/14_4_15437.dcm_norm-cxr_sextants-preview.png
          proc_rgb_url: ""
          annotation_url: ""

    SextantPredictions:
      description: |
        The document served at `proc_url`. Zones on a missing lung are absent from `predictions`;
        see `lung_side`.
      type: object
      properties:
        predictions:
          type: object
          properties:
            R1: { $ref: "#/components/schemas/Zone" }
            R2: { $ref: "#/components/schemas/Zone" }
            R3: { $ref: "#/components/schemas/Zone" }
            L1: { $ref: "#/components/schemas/Zone" }
            L2: { $ref: "#/components/schemas/Zone" }
            L3: { $ref: "#/components/schemas/Zone" }
        lung_side:
          type: string
          enum: [both, right, left]
        status:
          type: string
          enum: [ok, partial]
      required: [predictions, status, lung_side]
      examples:
        - status: ok
          lung_side: both
          predictions:
            R3:
              cavs: { probability: 0.193898, class_label: negative }
              dens: { probability: 0.904472, class_label: positive }
              nods: { probability: 0.682770, class_label: positive }
