openapi: 3.1.0
info:
  title: Floor Sanding Australia - Architect Hub API
  version: 1.0.0
  summary: Retrieval, cited synthesis and clause-generation API over the Bona / ATFA / NCC corpus.
  description: |
    Public read-only API that powers the Architect Hub at
    `https://floorsandingaustralia.com/architects/`. The corpus covers 18 Bona
    products, 150+ technical PDFs, 28 third-party certificates and 165 FAQ
    entries. Every response is citation-backed.

    Endpoints:
      - `GET /architects/api/search` - vector search, top-k chunks
      - `POST /architects/api/ask` - cited synthesis via Haiku 4.5
      - `POST /architects/api/ask/stream` - Server-Sent Events stream
      - `GET  /architects/api/ask/health` - liveness for the synthesis layer
      - `POST /architects/api/clause` - generates a MasterFormat/NATSPEC/NBS clause

    **MCP endpoints.** A Model Context Protocol server is also hosted at
    `https://mcp.floorsandingaustralia.com`. It is not part of this OpenAPI
    document. For the full tool list and connection details see the
    `REMOTE_SETUP.md` file in the MCP repository.

    **Rate limiting.** Default limit is **60 requests per minute per IP**.
    Rate limit state is reflected on every response via the `X-RateLimit-*`
    headers listed on each endpoint. The synthesis endpoints apply an
    additional `10/minute` + `3/second` per-IP burst cap.

    **Auth.** All endpoints are public. An optional `Bearer` token is accepted
    for future elevated rate limits - it is currently a no-op.
  contact:
    name: Floor Sanding Australia
    email: info@sand-aid.com
    url: https://floorsandingaustralia.com/
  license:
    name: CC-BY-4.0
    url: https://creativecommons.org/licenses/by/4.0/
  termsOfService: https://floorsandingaustralia.com/

servers:
  - url: https://floorsandingaustralia.com
    description: Production

externalDocs:
  description: MCP server setup (REMOTE_SETUP.md)
  url: https://mcp.floorsandingaustralia.com/

tags:
  - name: search
    description: Vector retrieval over the Bona / ATFA / NCC corpus.
  - name: ask
    description: Cited AI synthesis (Anthropic Haiku 4.5) on top of the retrieval layer.
  - name: clause
    description: Generates a NATSPEC / MasterFormat / NBS specification clause.
  - name: health
    description: Liveness checks.

security:
  - {}
  - bearerAuth: []

paths:
  /architects/api/search:
    get:
      tags: [search]
      summary: Top-k vector search
      description: |
        Retrieves the top-k matching chunks from the Bona corpus using the
        query embedding (Ollama on heartbeat) and an sqlite-vec ANN index.
        Filters are AND-combined.
      operationId: searchCorpus
      parameters:
        - name: q
          in: query
          required: true
          description: The search query.
          schema:
            type: string
            minLength: 1
            maxLength: 500
          example: VOC of Bona Traffic HD
        - name: k
          in: query
          required: false
          description: Number of results to return (1-25, default 5).
          schema:
            type: integer
            minimum: 1
            maximum: 25
            default: 5
        - name: product
          in: query
          required: false
          description: Restrict to a single product slug, e.g. `traffic-hd`.
          schema:
            type: string
          example: traffic-hd
        - name: type
          in: query
          required: false
          description: Restrict to a source type.
          schema:
            type: string
            enum: [product, pdf, cert, faq, compatibility_matrix]
        - name: cert_type
          in: query
          required: false
          description: Restrict to a certificate family.
          schema:
            type: string
            enum:
              - as4586-slip
              - din51130-slip
              - dibt
              - emicode
              - en13501-fire
              - en14904-sport
              - en71-toy-safety
              - epd
              - greenguard
              - iso14001
      responses:
        "200":
          description: Search results.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResponse'
              examples:
                voc:
                  summary: "VOC query, top 2 results"
                  value:
                    ok: true
                    query: "VOC of Bona Traffic HD"
                    k: 2
                    filters: {}
                    elapsed_ms: 142.3
                    count: 2
                    results:
                      - score: 0.891
                        snippet: "Bona Traffic HD: VOC 130 g/L, solids 33%, two-component waterborne polyurethane..."
                        citation: "Bona Traffic HD TDS (2024), p.1"
                        source_type: "pdf"
                        product_slug: "traffic-hd"
                        cert_type: null
                        doc_title: "Bona Traffic HD Technical Data Sheet"
                        source_path: "static/bona-pdfs/bona-traffic-hd-tds.pdf"
                        product_url: "/architects/products/traffic-hd.html"
                      - score: 0.812
                        snippet: "Traffic HD meets EC1 Plus for low emissions and EN 71-3 toy safety..."
                        citation: "Bona Traffic HD Product Page"
                        source_type: "product"
                        product_slug: "traffic-hd"
                        cert_type: null
                        doc_title: "Bona Traffic HD"
                        source_path: "architects/products/traffic-hd"
                        product_url: "/architects/products/traffic-hd.html"
        "400":
          description: Missing or invalid query.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example: { ok: false, error: "missing q" }
        "429":
          $ref: '#/components/responses/RateLimited'
        "503":
          description: Vector search backend unavailable (Ollama down or DB missing).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /architects/api/ask:
    post:
      tags: [ask]
      summary: Cited synthesis (sync)
      description: |
        Retrieves k chunks, reranks them, then calls Anthropic Haiku 4.5 to
        synthesise a fully-cited Markdown answer. Every factual claim is tagged
        with `[Source N]` referring to the 1-based citation index.

        Falls back to a degraded response (raw chunks) on Anthropic 429/5xx;
        see the `degraded` and `note` fields on the response.

        Rate limit: **10 requests/minute + 3/second per IP** in addition to the
        default 60/min.
      operationId: askQuestion
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AskRequest'
            examples:
              basic:
                summary: Minimal question
                value:
                  question: "What is the VOC of Bona Traffic HD?"
              filtered:
                summary: Restricted to one product and source type
                value:
                  question: "Which primer should I use on Blackbutt?"
                  k: 8
                  product: "amber"
                  type: "compatibility_matrix"
      responses:
        "200":
          description: Synthesised answer.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AskResponse'
              example:
                ok: true
                answer: "Bona Traffic HD has a VOC content of 130 g/L [Source 1]..."
                citations:
                  - index: 1
                    label: "Bona Traffic HD TDS (2024), p.1"
                    url: "/static/bona-pdfs/bona-traffic-hd-tds.pdf"
                    page: 1
                    product: "traffic-hd"
                sources_used: 1
                elapsed_ms: 3421.7
                model: "claude-haiku-4-5-20251001"
                cache_hit: false
                degraded: false
        "400":
          description: Empty or oversized question (max 1000 chars).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        "429":
          $ref: '#/components/responses/RateLimited'
        "500":
          description: Retrieval layer failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        "502":
          description: Anthropic API returned a non-retryable error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /architects/api/ask/stream:
    post:
      tags: [ask]
      summary: Cited synthesis (SSE stream)
      description: |
        Same synthesis pipeline as `/architects/api/ask` but streamed back as
        Server-Sent Events. Useful for time-to-first-token UX.

        **Event types** (all event data is JSON):
          - `retrieval` - debug info: `{union_size, reranked_size, rewrite_queries}`
          - `citations` - tentative citations: `{citations: [Citation]}`
          - `delta`     - streamed token: `{text: string}`
          - `done`      - terminal frame: `{elapsed_ms, sources_used, citations?, usage?, model?, degraded?, note?}`
      operationId: askQuestionStream
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AskRequest'
            example:
              question: "List every Bona product that holds EN 71-3."
              k: 8
      responses:
        "200":
          description: Server-Sent Events stream.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
            Content-Type:
              schema:
                type: string
                const: text/event-stream
          content:
            text/event-stream:
              schema:
                type: string
                description: |
                  Raw SSE frames. Each frame is a pair of `event:` and `data:`
                  lines separated by a blank line. Example:

                  ```
                  event: delta
                  data: {"text": "Bona Traffic HD has a VOC of 130 g/L..."}

                  event: done
                  data: {"elapsed_ms": 3421.7, "sources_used": 2, "model": "claude-haiku-4-5-20251001"}
                  ```
              examples:
                stream:
                  summary: Two-frame stream
                  value: |
                    event: retrieval
                    data: {"union_size": 12, "reranked_size": 8, "rewrite_queries": ["VOC of Traffic HD", "Bona Traffic HD emissions"]}

                    event: citations
                    data: {"citations": [{"index":1,"label":"Bona Traffic HD TDS (2024)","url":"/static/bona-pdfs/bona-traffic-hd-tds.pdf","page":1,"product":"traffic-hd"}]}

                    event: delta
                    data: {"text": "Bona Traffic HD has a VOC content of 130 g/L [Source 1]."}

                    event: done
                    data: {"elapsed_ms": 3421.7, "sources_used": 1, "citations": [{"index":1,"label":"Bona Traffic HD TDS (2024)","url":"/static/bona-pdfs/bona-traffic-hd-tds.pdf","page":1,"product":"traffic-hd"}], "model": "claude-haiku-4-5-20251001"}
        "400":
          description: Empty or oversized question (max 1000 chars).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        "429":
          $ref: '#/components/responses/RateLimited'
        "500":
          description: Retrieval failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /architects/api/ask/health:
    get:
      tags: [ask, health]
      summary: Synthesis layer liveness
      description: Performs a 1-token Anthropic Messages API ping to confirm the synthesis layer is reachable and credentials are valid.
      operationId: askHealth
      responses:
        "200":
          description: Synthesis layer reachable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AskHealth'
              example:
                ok: true
                anthropic_reachable: true
                latency_ms: 312.4
                model: "claude-haiku-4-5-20251001"
                cache_entries: 17
        "503":
          description: Synthesis layer unreachable (no API key, or Anthropic returned error).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AskHealth'
              example:
                ok: false
                anthropic_reachable: false
                error: "ANTHROPIC_API_KEY not set"

  /architects/api/clause:
    post:
      tags: [clause]
      summary: Generate a specification clause (NATSPEC / MasterFormat / NBS)
      description: |
        Generates a copy-paste-ready specification clause for a named Bona
        product. Cites every numeric value back to source PDFs. Can also list
        any attachments (TDS, SDS, EPD, certificates) that should ship with the
        spec package.

        **Note:** this endpoint is under active development. The response
        schema is stable but some optional fields (`masterformat_section`,
        `natspec_worksection`) may be `null` while the authoring pipeline is
        tuned.
      operationId: generateClause
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClauseRequest'
            examples:
              natspec_au:
                summary: NATSPEC clause for AU
                value:
                  product: "traffic-hd"
                  substrate: "blackbutt"
                  gloss: "matte"
                  use_case: "commercial"
                  region: "AU"
                  format: "natspec"
                  compliance_targets: ["EC1-Plus", "EN71-3"]
              masterformat_us:
                summary: MasterFormat section 09 64 29
                value:
                  product: "traffic-hd"
                  region: "US"
                  format: "masterformat"
      responses:
        "200":
          description: Generated clause.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClauseResponse'
              example:
                ok: true
                product: "traffic-hd"
                format: "natspec"
                clause_text: |
                  09 64 29 TIMBER FLOOR COATING - BONA TRAFFIC HD
                  Apply Bona Traffic HD two-component waterborne polyurethane...
                masterformat_section: null
                natspec_worksection: "0654 TIMBER FLOORING"
                citations:
                  - label: "Bona Traffic HD TDS (2024)"
                    url: "/static/bona-pdfs/bona-traffic-hd-tds.pdf"
                    page: 1
                required_attachments:
                  - filename: "bona-traffic-hd-tds.pdf"
                    url: "/static/bona-pdfs/bona-traffic-hd-tds.pdf"
                  - filename: "bona-traffic-hd-sds.pdf"
                    url: "/static/bona-pdfs/bona-traffic-hd-sds.pdf"
                warnings: []
        "400":
          description: Missing product slug or invalid parameters.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        "404":
          description: Product slug not in corpus.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        "429":
          $ref: '#/components/responses/RateLimited'
        "502":
          description: Upstream synthesis layer failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: opaque
      description: |
        Optional bearer token. Reserved for future elevated rate limits; the
        token is currently accepted but not enforced.

  headers:
    XRateLimitLimit:
      description: Max requests allowed in the current window (default 60/min).
      schema:
        type: integer
        example: 60
    XRateLimitRemaining:
      description: Requests remaining in the current window.
      schema:
        type: integer
        example: 59
    XRateLimitReset:
      description: Unix epoch (seconds) at which the current window resets.
      schema:
        type: integer
        example: 1744994400

  responses:
    RateLimited:
      description: Rate limit exceeded.
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/XRateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/XRateLimitReset'
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
            example: 30
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            ok: false
            error: "rate limit exceeded"

  schemas:
    Error:
      type: object
      required: [ok, error]
      properties:
        ok:
          type: boolean
          const: false
        error:
          type: string
          description: Short machine-readable error token.
          example: "missing q"
        detail:
          type: string
          description: Optional human-readable extra context.

    SourceType:
      type: string
      enum: [product, pdf, cert, faq, compatibility_matrix]
      description: |
        Kind of corpus chunk this result came from.
          - `product` - per-product landing page body
          - `pdf` - indexed TDS/SDS/EPD/brochure
          - `cert` - third-party certificate metadata
          - `faq` - generated FAQ answer
          - `compatibility_matrix` - primer x species matrix entry

    CertType:
      type: string
      enum:
        - as4586-slip
        - din51130-slip
        - dibt
        - emicode
        - en13501-fire
        - en14904-sport
        - en71-toy-safety
        - epd
        - greenguard
        - iso14001

    SearchFilters:
      type: object
      description: |
        Echoed server-side representation of the applied filters. Keys may be
        absent when not supplied.
      additionalProperties: false
      properties:
        product_slug:
          type: string
        source_type:
          $ref: '#/components/schemas/SourceType'
        cert_type:
          $ref: '#/components/schemas/CertType'

    SearchResult:
      type: object
      required: [score, snippet, citation, source_type]
      properties:
        score:
          type: number
          format: float
          description: Vector similarity score (0-1, higher is better).
          example: 0.891
        snippet:
          type: string
          description: Trimmed snippet of the chunk (max 320 chars).
        citation:
          type: string
          description: Human-readable citation for this chunk.
          example: "Bona Traffic HD TDS (2024), p.1"
        source_type:
          $ref: '#/components/schemas/SourceType'
        product_slug:
          type: [string, "null"]
          description: Product slug when the chunk is product-scoped.
        cert_type:
          oneOf:
            - $ref: '#/components/schemas/CertType'
            - type: "null"
        doc_title:
          type: [string, "null"]
        source_path:
          type: [string, "null"]
          description: Internal corpus path (filename / page URL).
        product_url:
          type: string
          description: Convenience deep-link into the product page.
          example: "/architects/products/traffic-hd.html"
        cert_url:
          type: string
          description: Convenience deep-link into the compliance page when source_type=cert.
          example: "/architects/compliance/ec1-plus.html"

    SearchResponse:
      type: object
      required: [ok, query, k, filters, elapsed_ms, count, results]
      properties:
        ok:
          type: boolean
          const: true
        query:
          type: string
        k:
          type: integer
          minimum: 1
          maximum: 25
        filters:
          $ref: '#/components/schemas/SearchFilters'
        elapsed_ms:
          type: number
          format: float
          description: Total server-side latency in milliseconds.
        timing:
          type: [object, "null"]
          description: Optional per-stage timing debug from the vector backend.
          additionalProperties: true
        count:
          type: integer
          minimum: 0
        results:
          type: array
          items:
            $ref: '#/components/schemas/SearchResult'

    AskRequest:
      type: object
      required: [question]
      properties:
        question:
          type: string
          minLength: 1
          maxLength: 1000
          description: The architect's question in plain English.
        k:
          type: integer
          minimum: 1
          maximum: 20
          default: 8
          description: Number of chunks to retrieve and feed to the synthesiser.
        product:
          type: string
          description: Optional product-slug filter applied before retrieval.
          example: "traffic-hd"
        type:
          $ref: '#/components/schemas/SourceType'
        cert_type:
          $ref: '#/components/schemas/CertType'

    Citation:
      type: object
      required: [index, label]
      properties:
        index:
          type: integer
          minimum: 1
          description: 1-based citation number that `[Source N]` references in the answer body.
        label:
          type: string
          description: Human-readable citation label, e.g. "Bona Traffic HD TDS (2024)".
        url:
          type: string
          format: uri-reference
          description: Optional deep-link back to the source document or page.
        page:
          type: integer
          minimum: 1
          description: Page number in the source PDF when applicable.
        product:
          type: string
          description: Product slug when the citation is product-scoped.

    Usage:
      type: object
      description: Anthropic Messages API usage block, passed through when available.
      additionalProperties: true
      properties:
        input_tokens:
          type: integer
        output_tokens:
          type: integer
        cache_read_input_tokens:
          type: integer
        cache_creation_input_tokens:
          type: integer

    RetrievalDebug:
      type: object
      properties:
        rewrite_queries:
          type: array
          items:
            type: string
        union_size:
          type: integer
        reranked_size:
          type: integer
        retrieval_ms:
          type: number
          format: float

    AskResponse:
      type: object
      required: [ok, answer, citations, sources_used, elapsed_ms, cache_hit, degraded]
      properties:
        ok:
          type: boolean
          const: true
        answer:
          type: string
          description: Markdown answer with inline `[Source N]` citations.
        citations:
          type: array
          items:
            $ref: '#/components/schemas/Citation'
        sources_used:
          type: integer
          minimum: 0
          description: Count of distinct citations referenced in the answer body.
        elapsed_ms:
          type: number
          format: float
        model:
          type: [string, "null"]
          description: Anthropic model identifier, or null when the response is degraded.
          example: "claude-haiku-4-5-20251001"
        cache_hit:
          type: boolean
          description: True when the answer was served from the in-process 1h TTL LRU.
        degraded:
          type: boolean
          description: True when the response is a raw-chunk fallback (Anthropic 429/5xx/timeout).
        note:
          type: string
          description: Present on degraded responses to describe the fallback cause.
        usage:
          $ref: '#/components/schemas/Usage'
        retrieval_debug:
          $ref: '#/components/schemas/RetrievalDebug'

    AskHealth:
      type: object
      required: [ok, anthropic_reachable]
      properties:
        ok:
          type: boolean
        anthropic_reachable:
          type: boolean
        latency_ms:
          type: number
          format: float
        model:
          type: string
        cache_entries:
          type: integer
          minimum: 0
        error:
          type: string

    ClauseRequest:
      type: object
      required: [product]
      properties:
        product:
          type: string
          description: Product slug (required), e.g. `traffic-hd`, `mega-one`, `craft-oil-2k`.
          minLength: 1
        substrate:
          type: string
          description: Optional substrate / species slug, e.g. `blackbutt`, `spotted-gum`, `concrete`.
        gloss:
          type: string
          description: Optional gloss level, e.g. `matte`, `silk-matt`, `gloss`.
        use_case:
          type: string
          description: Optional building use, e.g. `residential`, `commercial`, `sport`.
        region:
          type: string
          enum: [AU, UK, US]
          default: AU
          description: Spec flavour locale.
        format:
          type: string
          enum: [natspec, masterformat, nbs]
          default: natspec
          description: Output spec format. `natspec` for AU, `masterformat` for US, `nbs` for UK.
        compliance_targets:
          type: array
          description: Certifications / standards the clause must cite, e.g. `EC1-Plus`, `EN71-3`, `AS4586`.
          items:
            type: string

    ClauseAttachment:
      type: object
      required: [filename, url]
      properties:
        filename:
          type: string
        url:
          type: string
          format: uri-reference

    ClauseResponse:
      type: object
      required: [ok, product, format, clause_text, citations, required_attachments, warnings]
      properties:
        ok:
          type: boolean
          const: true
        product:
          type: string
        format:
          type: string
          enum: [natspec, masterformat, nbs]
        clause_text:
          type: string
          description: The full clause body, ready to paste into a specification document.
        masterformat_section:
          type: [string, "null"]
          description: CSI MasterFormat section code when `format=masterformat`, e.g. `09 64 29`.
        natspec_worksection:
          type: [string, "null"]
          description: NATSPEC worksection code when `format=natspec`, e.g. `0654`.
        citations:
          type: array
          items:
            $ref: '#/components/schemas/Citation'
        required_attachments:
          type: array
          items:
            $ref: '#/components/schemas/ClauseAttachment'
        warnings:
          type: array
          description: Non-fatal issues the spec writer should review (missing data, substitutions).
          items:
            type: string
