openapi: "3.0.1"
info:
  title: Waratah HTTP API
  version: "1"
  description: |
    Waratah admin HTTP API under `/waratah` (ingest **and** cluster snapshot).
    This is **not** RDF4J. SPARQL stays at `/rdf4j` (see `rdf4j_openapi.yaml`).
    The WebUI lives in a separate repository; this spec is the server contract.

    Files uploaded through the ingest API and RDF files an operator copies into
    `{ingest_dir}/{repository_id}` share one tree (default
    `{data_dir}/ingest/main`). Ingest is a separate synchronous POST that
    names a relative path. SPARQL Update and RDF4J `/statements` are out of
    scope for v1.

    Body limit for uploads and snippets: 1 GiB.

    RDF extensions: `.ttl`, `.turtle`, `.nt`, `.nq`, `.trig`, `.rdf`, `.xml`,
    `.owl`, plus a trailing `.gz`.

    Cluster membership is static (`[[computes]]` in cluster config). The
    contacted compute is coordinator for that request only: it probes its
    configured hexastore/registry and peer compute gRPC Describe/Health.
    Gossip is out of scope.

servers:
  - url: http://localhost:8080
    description: waratah dev start default

tags:
  - name: Cluster
    description: Process health and coordinator cluster snapshot
  - name: Notebooks
    description: Jupyter notebook sidecar discovery (same-origin /jupyter proxy)
  - name: Ingest
    description: Staging directory list, upload, snippet, and ingest

paths:
  /waratah/health:
    get:
      tags: [Cluster]
      summary: Liveness of this compute HTTP process
      description: |
        Does not probe storage or peer computes. Always `{ "status": "ok" }`
        if this process accepted the request.
      operationId: getWaratahHealth
      responses:
        "200":
          description: This process is serving HTTP
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"

  /waratah/notebooks:
    get:
      tags: [Notebooks]
      summary: Notebook service discovery (dev stub)
      description: |
        `available` means the feature is enabled on this compute and the
        jupyter-server sidecar answered readiness. `token` is the
        startup-minted jupyter token for the same-origin `/jupyter/*`
        reverse proxy (REST + WebSocket kernel channels). Loopback
        single-user dev stub; no auth model yet. When disabled, the body is
        `{ "available": false, "path": "/jupyter" }` with no token.
      operationId: getWaratahNotebooks
      responses:
        "200":
          description: Notebook service status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotebooksStatus"

  /waratah/cluster:
    get:
      tags: [Cluster]
      summary: Cluster snapshot assembled by this compute
      description: |
        Coordinator view for the dashboard. Partial failure is still HTTP 200:
        a down hexastore, registry, or peer compute appears as
        `"status": "down"` with `"error"`. Backends without a cluster config
        return 501.
      operationId: getWaratahCluster
      responses:
        "200":
          description: Snapshot (nodes may individually be down)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClusterSnapshot"
        "501":
          description: This process has no cluster view
          content:
            text/plain:
              schema:
                type: string

  /waratah/repositories/{repository_id}/ingest:
    parameters:
      - $ref: "#/components/parameters/RepositoryID"
    get:
      tags: [Ingest]
      summary: List RDF files in the ingest directory
      description: |
        Recursive listing. Nested files are returned as POSIX relative paths
        (`nested/file1.ttl`). The UI should build a directory tree from those
        prefixes. Non-RDF files and names starting with `.` are omitted.
      operationId: listIngestFiles
      responses:
        "200":
          description: Staged RDF files
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestFileList"
        "404":
          description: Unknown repository
          content:
            text/plain:
              schema:
                type: string
        "501":
          description: Ingest directory is not configured
          content:
            text/plain:
              schema:
                type: string
    post:
      tags: [Ingest]
      summary: Ingest a staged file into the store
      description: |
        Synchronous parse and insert. The request can take a long time.
        The file remains in the ingest directory. Optional `base_iri` overrides
        the file URL (and any sidecar written with a snippet). Optional `graph`
        remaps RDF DefaultGraph triples into that named graph (Turtle / N-Triples
        / RDF/XML all go there; TriG / N-Quads keep explicit named graphs as written).
      operationId: ingestStagedFile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IngestRequest"
      responses:
        "200":
          description: Insert counts
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestResult"
        "400":
          description: Bad path, RDF parse error, or invalid graph IRI
          content:
            text/plain:
              schema:
                type: string
        "404":
          description: Unknown repository or missing file
          content:
            text/plain:
              schema:
                type: string

  /waratah/repositories/{repository_id}/ingest/files:
    parameters:
      - $ref: "#/components/parameters/RepositoryID"
    post:
      tags: [Ingest]
      summary: Upload one RDF file into the ingest directory
      description: |
        One complete multipart POST per file. The UI should report upload
        progress from the client (`loaded` / `total`) on this request.
        Optional `path` sets the destination (`nested/file1.ttl`). If omitted,
        the uploaded filename's basename is used.
      operationId: uploadIngestFile
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                path:
                  type: string
                  example: nested/file1.ttl
      responses:
        "201":
          description: File stored in the ingest directory
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadResult"
        "400":
          description: Missing file, bad path, or unknown RDF extension
          content:
            text/plain:
              schema:
                type: string
        "413":
          description: Body larger than 1 GiB
          content:
            text/plain:
              schema:
                type: string

  /waratah/repositories/{repository_id}/ingest/snippets:
    parameters:
      - $ref: "#/components/parameters/RepositoryID"
    post:
      tags: [Ingest]
      summary: Write an RDF snippet into the ingest directory
      description: |
        Same effect as uploading a small file. If `path` has no RDF extension,
        one is appended from `format`. Ingest it afterwards with POST …/ingest.
        There is no `graph` field; choose the named graph on ingest.
      operationId: uploadIngestSnippet
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SnippetRequest"
      responses:
        "201":
          description: Snippet stored as a file
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadResult"
        "400":
          description: Bad path or unknown format name
          content:
            text/plain:
              schema:
                type: string

components:
  parameters:
    RepositoryID:
      name: repository_id
      in: path
      required: true
      description: Repository ID (MVP is `main`)
      schema:
        type: string
        example: main

  schemas:
    IngestFileList:
      type: object
      required: [files]
      properties:
        files:
          type: array
          items:
            $ref: "#/components/schemas/IngestFile"

    IngestFile:
      type: object
      required: [name, bytes, mtime_ms, format, gzip]
      properties:
        name:
          type: string
          example: nested/file1.ttl
        bytes:
          type: integer
          format: int64
        mtime_ms:
          type: integer
          format: int64
          description: Last modified time as Unix milliseconds
        format:
          type: string
          enum: [turtle, ntriples, nquads, trig, rdfxml, unknown]
        gzip:
          type: boolean

    IngestRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          example: nested/file1.ttl
        base_iri:
          type: string
          nullable: true
        graph:
          type: string
          nullable: true
          description: |
            Absolute IRI of the named graph for default-graph triples.
            Omit, null, or empty to keep RDF DefaultGraph. Invalid IRI → 400.
          example: http://ex/g1

    IngestResult:
      type: object
      required: [name, quads, registered]
      properties:
        name:
          type: string
        quads:
          type: integer
          format: int64
        registered:
          type: integer
          format: int64
          description: String-registry entries written for this ingest

    SnippetRequest:
      type: object
      required: [path, format, data]
      properties:
        path:
          type: string
          example: nested/paste
        format:
          type: string
          enum: [turtle, ntriples, nquads, trig, rdfxml]
        data:
          type: string
        base_iri:
          type: string
          nullable: true

    UploadResult:
      type: object
      required: [name, bytes]
      properties:
        name:
          type: string
          example: nested/file1.ttl
        bytes:
          type: integer
          format: int64

    NotebooksCapability:
      type: object
      required: [available, path]
      description: |
        Notebook sidecar capability. Present only on the self compute entry
        and only when `--with-notebooks` is enabled; peer entries carry no
        block in v1.
      properties:
        available:
          type: boolean
          description: Feature enabled AND sidecar answered readiness
        path:
          type: string
          example: /jupyter

    NotebooksStatus:
      type: object
      required: [available, path]
      properties:
        available:
          type: boolean
        path:
          type: string
          example: /jupyter
        token:
          type: string
          description: |
            Startup-minted jupyter token (dev stub). Present only when the
            feature is enabled on this compute.

    HealthResponse:
      type: object
      required: [status]
      properties:
        status:
          type: string
          enum: [ok]
          example: ok

    ClusterSnapshot:
      type: object
      required: [coordinator_id, probed_at_ms, computes, storage, planner]
      properties:
        coordinator_id:
          type: string
          example: compute-0
        probed_at_ms:
          type: integer
          format: int64
          description: Unix milliseconds when probing finished
        computes:
          type: array
          items:
            $ref: "#/components/schemas/ComputeView"
        storage:
          type: array
          items:
            $ref: "#/components/schemas/StorageView"
          description: This coordinator's live hexastore + registry probes
        planner:
          $ref: "#/components/schemas/PlannerSummary"

    ComputeView:
      type: object
      required:
        - id
        - self
        - status
        - grpc
        - rdf4j
        - admin
        - repositories
        - describe_mode
        - default_union
        - standalone
        - routing
      properties:
        notebooks:
          $ref: "#/components/schemas/NotebooksCapability"
        id:
          type: string
          example: compute-0
        self:
          type: boolean
          description: True when this is the process that served the snapshot
        status:
          $ref: "#/components/schemas/NodeStatus"
        grpc:
          type: string
          example: 127.0.0.1:7005
        standalone:
          type: boolean
          description: |
            True when this process opened S/P/O + registry in-process
            (`waratah dev start --standalone`). Compute gRPC is still bound.
        version:
          type: string
          example: 0.1.0+v0.1.0-0-gd8b0052
          description: |
            Build version from `waratah_buildinfo`: semver plus
            `git describe --long` output as build metadata. The self row
            carries this process's build; peer rows echo their Compute
            Describe RPC. Omitted when a peer did not answer its probe.
        rdf4j:
          type: string
          example: http://127.0.0.1:8080/rdf4j/
        admin:
          type: string
          example: http://127.0.0.1:8080/waratah/
        repositories:
          type: array
          items:
            $ref: "#/components/schemas/RepoInfo"
        describe_mode:
          type: string
          enum: [star, cbd, scbd]
        default_union:
          type: boolean
        routing:
          $ref: "#/components/schemas/RoutingView"
        error:
          type: string
          description: Present when status is down

    StorageView:
      type: object
      required: [kind, id, status]
      properties:
        kind:
          type: string
          enum: [hexastore, registry]
        id:
          type: string
          example: s-0
        axis:
          type: string
          enum: [s, p, o]
          description: Hexastore axis; omitted for registry
        grpc:
          type: string
          nullable: true
          example: 127.0.0.1:7001
          description: |
            Storage-process gRPC listen. Omitted when the coordinator is
            `--standalone` (no hexastore/registry gRPC servers).
        status:
          $ref: "#/components/schemas/NodeStatus"
        version:
          type: string
          example: 0.1.0+v0.1.0-0-gd8b0052
          description: |
            Build version reported by this partition's / registry's Describe
            RPC (`waratah_buildinfo`: semver + `git describe --long`).
            Omitted when the probe failed.
        backend:
          type: string
          enum: [memory, rocksdb]
        data_dir:
          type: string
        metrics:
          $ref: "#/components/schemas/LsmMetrics"
        error:
          type: string

    PlannerSummary:
      type: object
      required: [total_puts, predicate_count, class_count, named_graph_count]
      properties:
        total_puts:
          type: integer
          format: int64
        predicate_count:
          type: integer
          format: int64
        class_count:
          type: integer
          format: int64
        named_graph_count:
          type: integer
          format: int64

    RepoInfo:
      type: object
      required: [id, readable, writable]
      properties:
        id:
          type: string
          example: main
        readable:
          type: boolean
        writable:
          type: boolean

    RoutingView:
      type: object
      required: [hexastore, registry, vnodes_per_node]
      properties:
        hexastore:
          type: array
          items:
            $ref: "#/components/schemas/RoutingNode"
        registry:
          type: array
          items:
            $ref: "#/components/schemas/RoutingNode"
        vnodes_per_node:
          type: integer
          format: int32
          example: 16

    RoutingNode:
      type: object
      required: [node_id, grpc]
      properties:
        node_id:
          type: string
          example: s-0
        grpc:
          type: string
          example: 127.0.0.1:7001
        axis:
          type: string
          enum: [s, p, o]

    LsmMetrics:
      type: object
      description: |
        Operational counters. RocksDB size/compaction fields are omitted on
        the memory backend. Hexastore also reports open_total_puts and
        compaction_event_count; S-axis named_graph_count; registry entry_count.
      properties:
        estimate_live_data_bytes:
          type: integer
          format: int64
        estimate_num_keys:
          type: integer
          format: int64
        total_sst_bytes:
          type: integer
          format: int64
        memtable_bytes:
          type: integer
          format: int64
        num_running_compactions:
          type: integer
          format: int64
        num_running_flushes:
          type: integer
          format: int64
        open_total_puts:
          type: integer
          format: int64
        compaction_event_count:
          type: integer
          format: int64
        named_graph_count:
          type: integer
          format: int64
          description: S-axis named-graph catalog count; omitted on P and O
        entry_count:
          type: integer
          format: int64

    NodeStatus:
      type: string
      enum: [up, down]

