openapi: 3.0.3
info:
  title: DNotes API
  version: 0.1.0
  description: |
    DNotes is an API-first MVP for collecting developer notes, embedding them
    locally, grouping them into topics, and allowing users to manually correct
    topic assignments.

servers:
  - url: http://localhost:8080
    description: Local Docker Compose API

tags:
  - name: Health
    description: Process and dependency health checks
  - name: Notes
    description: Developer notes and assignment workflows
  - name: Topics
    description: Topic management and grouped notes
  - name: Documentation
    description: API documentation

paths:
  /healthz:
    get:
      tags:
        - Health
      summary: Check API process health
      operationId: getHealth
      responses:
        "200":
          description: API process is healthy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"

  /readyz:
    get:
      tags:
        - Health
      summary: Check API dependency readiness
      operationId: getReadiness
      responses:
        "200":
          description: API dependencies are reachable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReadinessResponse"
        "503":
          description: One or more dependencies are unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /openapi.yaml:
    get:
      tags:
        - Documentation
      summary: Return the OpenAPI document
      operationId: getOpenAPI
      responses:
        "200":
          description: OpenAPI YAML document
          content:
            application/yaml:
              schema:
                type: string
            text/yaml:
              schema:
                type: string

  /notes:
    post:
      tags:
        - Notes
      summary: Create a developer note
      description: |
        Creates a note, generates a real local embedding through the embedding
        sidecar, and attempts automatic topic assignment using cosine similarity
        against existing topic centroids.

        If no topic is similar enough, the note remains unclear so a user can
        assign it later.
      operationId: createNote
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateNoteRequest"
      responses:
        "201":
          description: Note created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Note"
        "400":
          description: Invalid request body or note content
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Embedding service or database unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    get:
      tags:
        - Notes
      summary: List notes
      operationId: listNotes
      parameters:
        - name: topic_id
          in: query
          required: false
          description: Return only notes assigned to this topic ID
          schema:
            type: string
            format: uuid
        - name: unclear
          in: query
          required: false
          description: Return only notes requiring manual assignment
          schema:
            type: boolean
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Notes list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotesResponse"
        "400":
          description: Invalid query parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /notes/{id}:
    get:
      tags:
        - Notes
      summary: Get a note by ID
      operationId: getNote
      parameters:
        - $ref: "#/components/parameters/NoteID"
      responses:
        "200":
          description: Note details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Note"
        "404":
          description: Note not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /notes/{id}/topic:
    patch:
      tags:
        - Notes
      summary: Manually update a note topic assignment
      description: |
        Manually assigns a note to an existing topic, assigns it to a newly
        created topic, or marks it as unclear.

        Manual assignment overrides automatic assignment.
      operationId: updateNoteTopicAssignment
      parameters:
        - $ref: "#/components/parameters/NoteID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateNoteTopicRequest"
      responses:
        "200":
          description: Updated note
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Note"
        "400":
          description: Invalid assignment request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Note or topic not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /topics:
    post:
      tags:
        - Topics
      summary: Create a topic manually
      operationId: createTopic
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTopicRequest"
      responses:
        "201":
          description: Topic created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Topic"
        "400":
          description: Invalid topic request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    get:
      tags:
        - Topics
      summary: List topics
      operationId: listTopics
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Topics list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TopicsResponse"

  /topics/{id}:
    get:
      tags:
        - Topics
      summary: Get a topic by ID
      operationId: getTopic
      parameters:
        - $ref: "#/components/parameters/TopicID"
      responses:
        "200":
          description: Topic details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Topic"
        "404":
          description: Topic not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    patch:
      tags:
        - Topics
      summary: Update a topic
      operationId: updateTopic
      parameters:
        - $ref: "#/components/parameters/TopicID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateTopicRequest"
      responses:
        "200":
          description: Updated topic
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Topic"
        "400":
          description: Invalid topic update request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Topic not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /topics/{id}/notes:
    get:
      tags:
        - Topics
      summary: List notes assigned to a topic
      operationId: listTopicNotes
      parameters:
        - $ref: "#/components/parameters/TopicID"
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Topic notes list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotesResponse"
        "404":
          description: Topic not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

components:
  parameters:
    NoteID:
      name: id
      in: path
      required: true
      description: Note ID
      schema:
        type: string
        format: uuid

    TopicID:
      name: id
      in: path
      required: true
      description: Topic ID
      schema:
        type: string
        format: uuid

  schemas:
    HealthResponse:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          example: ok

    ReadinessResponse:
      type: object
      required:
        - status
        - postgres
        - embedder
      properties:
        status:
          type: string
          enum:
            - ok
            - degraded
        postgres:
          type: string
          enum:
            - ok
            - unavailable
        embedder:
          type: string
          enum:
            - ok
            - unavailable

    CreateNoteRequest:
      type: object
      required:
        - content
      properties:
        content:
          type: string
          minLength: 1
          maxLength: 12000
          example: Need to add request tracing around checkout retries.

    UpdateNoteTopicRequest:
      type: object
      description: |
        Use topic_id to assign to an existing topic.
        Use topic_name without topic_id to create and assign a new topic.
        Use mark_unclear=true to remove the current assignment.
        Do not provide topic_id and topic_name together.
        Do not combine mark_unclear with topic_id or topic_name.
      properties:
        topic_id:
          type: string
          format: uuid
          nullable: true
        topic_name:
          type: string
          nullable: true
          minLength: 1
          maxLength: 120
        mark_unclear:
          type: boolean
          default: false

    CreateTopicRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
          example: Checkout reliability
        summary:
          type: string
          maxLength: 1000
          default: ""

    UpdateTopicRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        summary:
          type: string
          maxLength: 1000

    Note:
      type: object
      required:
        - id
        - content
        - assignment_status
        - assignment_source
        - embedding_model
        - embedding_dimensions
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        content:
          type: string
        topic_id:
          type: string
          format: uuid
          nullable: true
        assignment_status:
          type: string
          enum:
            - assigned
            - unclear
        assignment_source:
          type: string
          enum:
            - auto
            - manual
        similarity_score:
          type: number
          format: double
          nullable: true
          minimum: -1
          maximum: 1
        embedding_model:
          type: string
          example: sentence-transformers/all-MiniLM-L6-v2
        embedding_dimensions:
          type: integer
          minimum: 0
          example: 384
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Topic:
      type: object
      required:
        - id
        - name
        - summary
        - embedding_model
        - note_count
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        summary:
          type: string
        embedding_model:
          type: string
          example: sentence-transformers/all-MiniLM-L6-v2
        note_count:
          type: integer
          minimum: 0
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    NotesResponse:
      type: object
      required:
        - notes
      properties:
        notes:
          type: array
          items:
            $ref: "#/components/schemas/Note"

    TopicsResponse:
      type: object
      required:
        - topics
      properties:
        topics:
          type: array
          items:
            $ref: "#/components/schemas/Topic"

    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          $ref: "#/components/schemas/APIError"

    APIError:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          enum:
            - bad_request
            - not_found
            - validation_failed
            - embedding_unavailable
            - database_error
            - dependency_unavailable
            - internal_error
        message:
          type: string
        details:
          type: object
          additionalProperties: true
