openapi: 3.1.0
info:
  title: MaterialGraph HTTP API
  version: 0.7.0
  description: |
    HTTP surface for the MaterialGraph operations app. MaterialGraph's canonical
    catalog model is brand -> product family -> product model -> variant, with
    source-backed attributes and changes persisted through the graph runtime.

    This OpenAPI document describes only routes that are actually served by the
    web app. `/api/v1/**` is the stable, authenticated public surface (search,
    material reads, pairings, spec queries, durable capability Runs) — every operation requires a bearer
    token with the `mg:read` scope. Canonical catalog browsing and operator
    automation are otherwise exposed through the app shell, tRPC procedures, Mastra
    workflows, and the `mg` CLI rather than a broad REST catalog API.

    Entitlement is never enforced by MaterialGraph — the API returns candidates;
    Material Bank applies its own `brg_id` entitlement filter.

    The public surface is language-neutral: OpenAPI (this document), MCP, and A2A.
    Operator/admin surfaces use tRPC and are not part of this contract.

    This document is generated from the Zod schemas that back the routes; run
    `pnpm --filter @materialgraph/web generate:openapi` to regenerate it. Do not
    edit by hand.
  contact:
    name: MaterialGraph
    url: https://beta.materialgraph.com
  license:
    name: Proprietary
    url: https://beta.materialgraph.com/license
servers:
  - url: /
    description: Same-origin
  - url: http://localhost:30300
    description: Local development
tags:
  - name: Discovery
    description: API and agent discovery documents.
  - name: Assets
    description: R2-backed asset redirects.
  - name: Sources
    description: Source intake and crawl entry points.
  - name: Activity
    description: Operator activity snapshots.
  - name: Auth
    description: Local token exchange.
  - name: Enrichment
    description: Streaming operator enrichment.
  - name: Automation
    description: tRPC-backed operator automation procedures.
  - name: Search
    description: |
      The stable, authenticated `/api/v1/search` surface — visual/lexical/colour/image search, Material Match (`/api/v1/search/match`), plus the colour-focused helper endpoints (colours, browse, palettes/extract, colours/resolve, colours/suggest).
  - name: Materials
    description: Material (variant) detail, siblings, and batch reads.
  - name: Pairings
    description: Co-specification and colour-based "what pairs with this" rails.
  - name: Catalog
    description: Cross-brand specification queries, the spec vocabulary, and the
      schema glossary.
  - name: Codes
    description: Advisory building-code evaluation — badge materials against the
      in-repo model-code requirement registry (never a legal determination).
  - name: Composition
    description: Scheme composition and variation — derive alternative material
      schemes from a composed Run along a stated constraint axis.
  - name: Schema
    description: Governed blocks, attributes, product types, units, vocabularies,
      source requirements, templates, and mappings.
  - name: Brands
    description: Brand identity and trade / specification signals.
  - name: Runs
    description: Governed capability discovery and durable asynchronous agent,
      workflow, and tool execution.
  - name: Projects
    description: Persistent Material Box projects — the durable entity plus its
      append-only designer-process event log.
  - name: Meta
    description: Credential and platform sanity-check endpoints.
paths:
  /api/openapi:
    get:
      operationId: get_openapi
      summary: Get this OpenAPI document
      tags:
        - Discovery
      responses:
        "200":
          description: YAML OpenAPI document.
          content:
            application/vnd.oai.openapi+yaml;version=3.1:
              schema:
                type: string
        "500":
          $ref: "#/components/responses/Problem"
  /openapi.yaml:
    get:
      operationId: get_openapi_static
      summary: Get the static OpenAPI document
      tags:
        - Discovery
      responses:
        "200":
          description: YAML OpenAPI document served from the public directory.
          content:
            application/vnd.oai.openapi+yaml;version=3.1:
              schema:
                type: string
  /api/assets/{assetKey}:
    get:
      operationId: redirect_asset
      summary: Redirect an asset key to its R2 object URL
      description: |
        Resolves a persisted asset id or canonical key and redirects to the R2-backed public object URL. Missing storage metadata is treated as corrupt data; the route does not fall back to local files or remote manufacturer URLs.
      tags:
        - Assets
      parameters:
        - name: assetKey
          in: path
          required: true
          description: Persisted asset id or canonical asset key.
          schema:
            type: string
      responses:
        "308":
          description: Permanent redirect to the R2 object URL.
          headers:
            Location:
              schema:
                type: string
                format: uri
        "404":
          $ref: "#/components/responses/Problem"
  /api/v1/sources/crawls:
    post:
      operationId: start_source_crawl
      summary: Start a bounded source crawl
      description: |
        Starts a source crawl for a brand seed URL. Use this for controlled source discovery only; source crawl results inform later site-zone, family, product-model, variant, option, document, and image ingestion. Requires the `mg:ingest` bearer scope.
      tags:
        - Sources
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StartCrawlRequest"
      responses:
        "201":
          description: Crawl start result from the runtime.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "401":
          $ref: "#/components/responses/Problem"
  /api/v1/sources/crawls/{jobId}:
    get:
      operationId: get_source_crawl
      summary: Get a crawl job snapshot
      description: |
        Returns the job status, counters, last event sequence, and a bounded page of discovered URLs. Requires the default `mg:read` bearer scope.
      tags:
        - Sources
      parameters:
        - name: jobId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Crawl job snapshot.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "401":
          $ref: "#/components/responses/Problem"
        "404":
          $ref: "#/components/responses/Problem"
  /api/v1/sources/crawls/{jobId}/events:
    get:
      operationId: stream_source_crawl_events
      summary: Stream crawl job events
      description: |
        Server-sent events for a crawl job. The `?after=` cursor resumes from a given sequence so a reconnecting client never replays events it already saw. Requires the default `mg:read` bearer scope.
      tags:
        - Sources
      parameters:
        - name: jobId
          in: path
          required: true
          schema:
            type: string
        - name: after
          in: query
          required: false
          description: Resume from this event sequence.
          schema:
            type: integer
      responses:
        "200":
          description: SSE stream of crawl events.
          content:
            text/event-stream:
              schema:
                type: string
        "401":
          $ref: "#/components/responses/Problem"
  /api/v1/activity:
    get:
      operationId: get_activity_feed
      summary: Get the operator activity snapshot
      description: |
        Returns the operator activity snapshot. The optional `?since=` cursor returns only activity newer than the supplied marker. Requires the default `mg:read` bearer scope.
      tags:
        - Activity
      parameters:
        - name: since
          in: query
          required: false
          description: Optional cursor or timestamp understood by the activity snapshot.
          schema:
            type: string
      responses:
        "200":
          description: Activity snapshot.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "401":
          $ref: "#/components/responses/Problem"
  /api/auth/token:
    post:
      operationId: exchange_token
      summary: Exchange a subject token
      description: |
        RFC 8693 token exchange endpoint. Accepts form-encoded input and returns a short-lived MaterialGraph token or an RFC 7807 problem body.
      tags:
        - Auth
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              additionalProperties:
                type: string
      responses:
        "200":
          description: Token exchange response.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "500":
          $ref: "#/components/responses/Problem"
  /api/trpc/{procedure}:
    get:
      operationId: trpc_query
      summary: Run a tRPC query procedure
      description: |
        tRPC transport for the operator app. The public router currently exposes `automation`, `catalog`, `docs`, `health`, `jobs`, `schema`, and `usage` procedure groups. Prefer generated tRPC clients inside the app.
      tags:
        - Automation
      parameters:
        - $ref: "#/components/parameters/TrpcProcedure"
        - name: input
          in: query
          required: false
          schema:
            type: string
        - name: batch
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: tRPC JSON response.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
    post:
      operationId: trpc_mutation
      summary: Run a tRPC mutation procedure
      tags:
        - Automation
      parameters:
        - $ref: "#/components/parameters/TrpcProcedure"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: tRPC JSON response.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
  /api/v1/capabilities:
    get:
      operationId: list_public_capabilities
      summary: List invocable agents, workflows, and tools
      description: Returns the governed public capability catalogue, including live
        input/output schemas, effects, scopes, timeouts, and retry policy.
        Internal capabilities such as Web Browser Agent are excluded. Requires
        `mg:read`.
      tags:
        - Runs
      security:
        - bearerAuth: []
      responses:
        "200":
          description: The public capability catalogue.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CapabilityManifestResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/runs:
    post:
      operationId: create_public_run
      summary: Start an asynchronous capability Run
      description: Validates capability input and scope, transactionally records the
        Run and encrypted outbox command, then acknowledges without executing
        inline. Supply `Idempotency-Key` or `idempotencyKey` to make retries
        safe. The capability may require `mg:compose`, `mg:ingest`, `mg:write`,
        or `mg:admin` in addition to `mg:read`.
      tags:
        - Runs
      security:
        - bearerAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: Stable caller key for idempotent Run creation.
          schema:
            type: string
            minLength: 1
            maxLength: 500
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePublicRunRequest"
      responses:
        "200":
          description: An idempotent replay of an existing Run.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatePublicRunResponse"
        "202":
          description: The accepted durable Run.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatePublicRunResponse"
        "400":
          description: The request or capability input failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          description: The idempotency key was reused with different input.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "413":
          description: The JSON request body exceeds the 272 KiB wire limit.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          description: Public Run admission is disabled in this environment. Reads,
            cancellation, and resume remain available.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /api/v1/runs/{runId}:
    get:
      operationId: get_public_run
      summary: Get a Run's current state
      description: Returns the caller-owned redacted Run ledger record. Prompts, raw
        model messages, reasoning, credentials, provider payloads, and
        source-document contents are never returned.
      tags:
        - Runs
      security:
        - bearerAuth: []
      parameters:
        - name: runId
          in: path
          required: true
          description: The public Run identifier returned by `POST /api/v1/runs`.
          schema:
            type: string
      responses:
        "200":
          description: The current Run state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicRunResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: The Run does not exist in this environment or is not caller-owned.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/runs/{runId}/events:
    get:
      operationId: list_public_run_events
      summary: Stream ordered Run events
      description: Streams caller-owned, redacted Run events as server-sent events
        (SSE). Each frame uses the Run event sequence as `id`, the event type as
        `event`, and the serialized public Run event as `data`. Resume after the
        last observed sequence with the `after` query parameter or
        `Last-Event-ID` header; `after` takes precedence when both are supplied.
        The server polls for new events about once per second and returns at
        most 100 events per read. A terminal `succeeded`, `failed`, or
        `canceled` event closes the stream immediately. Non-terminal streams
        close after at most 25 seconds, so clients must reconnect with their
        cursor. The route emits no SSE heartbeat/comment frames or `retry`
        directive; the bounded connection lifetime is its reconnect and liveness
        boundary.
      tags:
        - Runs
      security:
        - bearerAuth: []
      parameters:
        - name: runId
          in: path
          required: true
          description: The public Run identifier returned by `POST /api/v1/runs`.
          schema:
            type: string
        - name: after
          in: query
          required: false
          description: Stream events whose sequence is greater than this non-negative
            cursor. Takes precedence over `Last-Event-ID` when both are
            supplied.
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: Last-Event-ID
          in: header
          required: false
          description: Standard SSE reconnect cursor. Stream events whose sequence is
            greater than this non-negative integer when `after` is absent.
          schema:
            type: integer
            minimum: 0
      responses:
        "200":
          description: An SSE stream of ordered public Run events. Reconnect after the
            bounded stream closes unless a terminal event was received.
          content:
            text/event-stream:
              schema:
                type: string
                description: "SSE frames encoded as `id: <sequence>`, `event: <type>`, and
                  `data: <PublicRunEvent JSON>`, followed by a blank line."
        "400":
          description: The event cursor is invalid.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: The Run does not exist in this environment or is not caller-owned.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/runs/{runId}/resume:
    post:
      operationId: resume_public_run
      summary: Resume a suspended Run
      description: Transactionally moves a caller-owned suspended Run to claimed and
        records an encrypted resume command. Execution continues asynchronously
        through Inngest.
      tags:
        - Runs
      security:
        - bearerAuth: []
      parameters:
        - name: runId
          in: path
          required: true
          description: The public Run identifier returned by `POST /api/v1/runs`.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ResumePublicRunRequest"
      responses:
        "202":
          description: The claimed Run.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicRunResponse"
        "400":
          description: The resume payload failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: The Run does not exist in this environment or is not caller-owned.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: The Run is not suspended or is already being resumed.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "413":
          description: The JSON request body exceeds the 272 KiB wire limit.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/runs/{runId}/cancel:
    post:
      operationId: cancel_public_run
      summary: Request durable Run cancellation
      description: For an active tool Run, records an encrypted cancellation command
        for the associated Inngest execution; cancellation completes
        asynchronously with a guarded state transition. Active agent and
        workflow Runs are not currently cancellable because their child
        executions cannot be correlated safely across processes, so they return
        `409 Conflict` without recording a cancellation command. Terminal Runs
        of any capability kind are returned unchanged with `202 Accepted`
        because no cancellation is required.
      tags:
        - Runs
      security:
        - bearerAuth: []
      parameters:
        - name: runId
          in: path
          required: true
          description: The public Run identifier returned by `POST /api/v1/runs`.
          schema:
            type: string
      responses:
        "202":
          description: The Run after accepting the cancellation request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicRunResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: The Run does not exist in this environment or is not caller-owned.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: The active Run is an agent or workflow whose cross-process child
            execution cannot currently be canceled safely.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/search:
    get:
      operationId: search_materials_get
      summary: Search materials (query-string form)
      description: 'The stable, authenticated search endpoint — GET query-string form.
        Provide `q` and/or `colours` and/or `mood`, OR `variantId` for a "looks
        like this material" seed search. At least one of `q`, `colours`, `mood`,
        or `variantId` is required. Enum params (`mood`/`sort`/`tol`) are
        validated against the shared search schema; an invalid value returns a
        400 RFC 7807 problem listing the valid options in `suggestions`. Search
        is dynamic, so success responses carry `Cache-Control: no-store`.
        Requires the `mg:read` bearer scope.'
      tags:
        - Search
      security:
        - bearerAuth: []
      parameters:
        - name: q
          in: query
          required: false
          description: Free-text query, matched visually against material imagery. A bare
            colour word ("sage") automatically runs as a colour-only search.
          schema:
            type: string
        - name: colours
          in: query
          required: false
          description: Comma-separated `HEX.WEIGHT` palette, weight optional, e.g.
            `9CAF88.6,E8E0D0.4`. Leading `#` optional on each hex.
          schema:
            type: string
        - name: logic
          in: query
          required: false
          description: Cross-colour combination when `colours` is set. `all` (default) or
            `any`.
          schema:
            type: string
            enum:
              - all
              - any
            default: all
        - name: mood
          in: query
          required: false
          description: Mood-language colour entry — resolves to a curated palette.
          schema:
            type: string
        - name: sort
          in: query
          required: false
          description: Re-order results by a colour-appearance axis.
          schema:
            type: string
            enum:
              - relevance
              - light
              - dark
              - vivid
              - muted
              - colourful
        - name: tol
          in: query
          required: false
          description: How close a colour must be to count as a match.
          schema:
            type: string
            enum:
              - tight
              - normal
              - loose
        - name: variantId
          in: query
          required: false
          description: '"Looks like this material" mode — seeds an image kNN from an
            existing variant. When set, `q`/`colours`/`mood` are ignored and the
            response additionally carries `seed` (a `getVariantBrief` echo of
            the seed variant).'
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Results per page (default 24, max 48).
          schema:
            type: integer
            minimum: 1
            maximum: 48
            default: 24
        - name: explain
          in: query
          required: false
          description: Set to `1` to include per-signal scores (`signals`) on each result.
          schema:
            type: string
            enum:
              - "0"
              - "1"
        - name: count
          in: query
          required: false
          description: Set to `true` for an opt-in `counts.variants` feed total — EXACT up
            to a cap of 1,000 (`basis:"exact"`), `lowerBound` beyond the cap and
            always for ANN-ranked pools. Never silently approximated.
          schema:
            type: string
            enum:
              - "true"
        - name: hasImages
          in: query
          required: false
          description: Set to `true` to return only materials with at least one catalog
            image. Off by default so results stay honest about the full catalog
            (including image-less records); the `count` total honours it too.
          schema:
            type: string
            enum:
              - "true"
        - name: view
          in: query
          required: false
          description: "`card` returns the lean grid projection (`searchCardItemSchema`:
            identity + name + thumbnail + score) instead of the full items; the
            response echoes `view`, which discriminates the `results` union.
            Default `full`."
          schema:
            type: string
            enum:
              - full
              - card
            default: full
      responses:
        "200":
          description: Search results (plus a `query` echo, and `seed` in `?variantId=`
            mode).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SearchResponse"
        "400":
          description: Missing query (no `q`/`colours`/`mood`/`variantId`) or an invalid
            enum value (valid options listed in `suggestions`).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "499":
          description: The client aborted the request before the search completed.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "503":
          description: The search engine is unavailable (e.g. warming up). Retriable.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    post:
      operationId: search_materials_post
      summary: Search materials (full JSON contract)
      description: "The stable, authenticated search endpoint — POST JSON-body form,
        accepting the full `searchMaterialsInputSchema` contract: `text` /
        `textModes`, an explicit `colours[]` palette (`colourLogic` /
        `colourTolerance` / `colourAppearance`), `mood`, `sort`, `similarTo`
        (seed by `variantId` or a raw `vector`), a raw incoming `image` (`url`
        or `base64` — the adapter embeds it server-side and drives the image
        arm), `category` (soft `boost` + hard `filter`), `weights`, typed
        `filters` (term match or `hasValue` presence), `limit` (max 48),
        `cursor`, `collapse`, `count`, `view`, and `explain`. Precedence:
        `image`/`similarTo` win over `text`; explicit `colours` override any
        colour word detected in `text`. `collapse: \"product\"` groups results
        to one representative (highest-ranked) variant per product, reporting
        hidden variants on that result's `siblingCount` plus a pool-bounded
        `counts` (variants + products, both `basis:\"lowerBound\"`) on the
        envelope; default `\"none\"` returns every variant. `count: true` adds
        an opt-in `counts.variants` FEED total — EXACT up to a cap of 1,000
        (`basis:\"exact\"`), `basis:\"lowerBound\"` beyond the cap and always
        for ANN-ranked pools (image / colour / visual-text), never silently
        approximated. `view: \"card\"` returns the lean `searchCardItemSchema`
        grid projection instead of the full items; the response echoes `view`,
        which discriminates the `results` union. `hasImages: true` returns only
        materials with at least one catalog image (default false keeps the full
        catalog, and the `count` total honours it — the visual arms are
        inherently image-backed, so it only filters the lexical attribute lens).
        Each result's `visualMeanings` (existing accepted/measured meanings —
        descriptive evidence, never a ranking cause) is included only when
        `explain` is set and at least one meaning exists. Returns the engine
        result verbatim (`mode`, `results[]`, `rerank`, `maxScore`, `counts`)
        plus a `query` echo (the raw `image` bytes/vector are never echoed back
        — only `{ type }`). Search is dynamic, so success responses carry
        `Cache-Control: no-store`. Requires the `mg:read` bearer scope."
      tags:
        - Search
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SearchMaterialsRequest"
      responses:
        "200":
          description: Search results (plus a `query` echo; image bytes/vector are never
            echoed).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SearchResponse"
        "400":
          description: Invalid JSON body, an unknown category filter, or the body failed
            `searchMaterialsInputSchema` validation (Zod `issues` ride the
            `issues` member), or the query image URL/bytes were rejected (SSRF
            guard / bad base64 — bare `{ error }` shape from the shared
            image-input helper, not RFC 7807).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
            application/json:
              schema:
                $ref: "#/components/schemas/SimpleError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "499":
          description: The client aborted the request before the search completed.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "502":
          description: The query image failed to embed. Bare `{ error }` shape.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SimpleError"
        "503":
          description: The search engine is unavailable (e.g. warming up). Retriable.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /api/v1/search/specifications:
    get:
      operationId: catalog_by_spec
      summary: Find materials by canonical specification
      description: "Find materials matching a canonical specification across every
        brand — the value-anchored primitive behind the find-by-spec MCP tool
        and the `catalog.filterBySpec` tRPC procedure. Repeat `spec` once per
        criterion as `attribute:value` (a variant must satisfy ALL of them).
        Each match carries per-criterion trust (`confidence` + `sourceBasis`)
        and a weakest-link `minConfidence`. Discover queryable attributes via
        `catalog/specifications/facets` and one attribute's values via
        `catalog/attributes/{attributeId}/values`. Responses are `Cache-Control:
        no-store`. Requires the `mg:read` bearer scope."
      tags:
        - Search
      security:
        - bearerAuth: []
      parameters:
        - name: spec
          in: query
          required: true
          description: Repeatable `attribute:value` criterion, e.g.
            `primary_material:textile`. A variant must satisfy every `spec`
            given.
          schema:
            type: string
          style: form
          explode: true
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 250
            default: 50
      responses:
        "200":
          description: Matching variants with per-criterion trust.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CatalogBySpecResponse"
        "400":
          description: No `?spec=` criteria provided. Bare `{ error }` shape.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SimpleError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/search/match:
    post:
      operationId: match_material
      summary: Material Match — image → category-scoped lookalikes with quality floor
      description: 'Match a query image to catalog materials: embed via Voyage
        multimodal-3.5, infer product-type categories from the image, hard-scope
        image search to those types, drop hits below a quality floor on image
        cosine, and return one result per product/collection. Exactly one of
        `imageBase64`, `imageUrl`, or a pre-computed `vector` is required (plus
        optional `limit` / `qualityFloor` / category knobs and detector hints
        `detectionLabel` / `materialFunction` / `detectionCategory` /
        `requestedType`). The HTTP adapter also accepts the search-style `image:
        { type: "url"|"base64", … }` envelope and normalises it, and streams
        NDJSON when called with `?stream=1` (or `Accept: application/x-ndjson`).
        Image URLs are SSRF-guarded; base64 is capped at 8 MB and spends the
        `embed` rate bucket. Dynamic — success responses carry `Cache-Control:
        no-store`. Requires the `mg:read` bearer scope.'
      tags:
        - Search
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MatchMaterialRequest"
      responses:
        "200":
          description: Match results with inferred categories and floor stats.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MatchMaterialResponse"
        "400":
          description: Invalid JSON body, failed `matchMaterialInputSchema` validation
            (Zod `issues` ride the `issues` member), or a rejected image URL
            (SSRF guard).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          description: Image exceeds the 8 MB cap.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
        "499":
          description: The client aborted the request before Match completed.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "503":
          description: Match embed or search engine unavailable. Retriable.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /api/v1/search/colours:
    get:
      operationId: search_by_colour
      summary: Find materials by colour palette
      description: "Find materials whose colour palette is closest to a target colour
        or palette, across every brand, by colour-palette embedding similarity.
        Pass a single `hex`, repeat `colour` for a multi-colour palette, and/or
        an OKLab `l`/`a`/`b` triple (all-or-none). An optional `category` filter
        (repeatable; category-group or product-type display names, or canonical
        taxonomy keys) hard-restricts the result; groups expand to their child
        types. Each match additionally carries `category` (the primary product
        type) and `oklab`. Success responses carry `Cache-Control: public,
        max-age=300`. Requires the `mg:read` bearer scope."
      tags:
        - Search
      security:
        - bearerAuth: []
      parameters:
        - name: hex
          in: query
          required: false
          description: A single 6-digit hex colour (with or without `#`).
          schema:
            type: string
          style: form
          explode: true
        - name: colour
          in: query
          required: false
          description: Repeatable — one 6-digit hex per palette entry.
          schema:
            type: string
          style: form
          explode: true
        - name: l
          in: query
          required: false
          description: OKLab lightness L ∈ [0,1]; provide with `a` and `b` (all-or-none).
          schema:
            type: number
            minimum: 0
            maximum: 1
        - name: a
          in: query
          required: false
          description: OKLab a axis (green↔red) ∈ [-0.5,0.5]; all-or-none with `l`, `b`.
          schema:
            type: number
            minimum: -0.5
            maximum: 0.5
        - name: b
          in: query
          required: false
          description: OKLab b axis (blue↔yellow) ∈ [-0.5,0.5]; all-or-none with `l`, `a`.
          schema:
            type: number
            minimum: -0.5
            maximum: 0.5
        - name: category
          in: query
          required: false
          description: HARD taxonomy group/type filter by name or key; groups expand to
            types.
          schema:
            type: string
          style: form
          explode: true
        - name: limit
          in: query
          required: false
          description: Results per page (default 24, cap 100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 24
      responses:
        "200":
          description: Colour matches.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ByColourResponse"
        "400":
          description: Invalid query/unknown category; validation errors include `issues`.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          description: The category-filter search engine is unavailable. Retriable.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /api/v1/search/browse:
    get:
      operationId: search_browse_by_colour
      summary: Browse the corpus by colour scalar
      description: "Pure colour browse (no search signals) — a single-table,
        btree-indexed keyset scan over the `variants` colour scalars, ordered
        lightest→darkest / vivid→muted / most-colourful with true DB pagination
        (`cursor`). This bypasses `searchMaterials` entirely (a no-signal query
        cannot run through it). Success responses carry `Cache-Control: public,
        max-age=300`. Requires the `mg:read` bearer scope."
      tags:
        - Search
      security:
        - bearerAuth: []
      parameters:
        - name: sort
          in: query
          required: true
          schema:
            type: string
            enum:
              - light
              - dark
              - vivid
              - muted
              - colourful
        - name: cursor
          in: query
          required: false
          description: Opaque keyset cursor from a previous page's `cursor`.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 48
        - name: types
          in: query
          required: false
          description: Comma-separated product-type ids to restrict the browse to.
          schema:
            type: string
      responses:
        "200":
          description: A page of browse results.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BrowseColourResponse"
        "400":
          description: Invalid browse input. RFC 7807 problem with `issues`.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/search/palettes/extract:
    post:
      operationId: extract_image_palette
      summary: Extract a named colour palette from an uploaded image
      description: 'Upload→palette for the "match the colours" image path: send image
        bytes (a data URI or bare base64) and get back the colorscope-extracted
        named palette. Node-only (sharp-backed). Not cached (best-effort,
        per-request analysis). Requires the `mg:read` bearer scope.'
      tags:
        - Search
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ExtractPaletteRequest"
      responses:
        "200":
          description: The extracted palette, or null when extraction found no usable
            colour.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExtractPaletteResponse"
        "400":
          description: Invalid JSON body, failed schema validation (Zod `issues` ride the
            `issues` member), or non-decodable base64.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          description: Image exceeds the 8 MB cap.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/search/colours/resolve:
    get:
      operationId: resolve_colour_name
      summary: Resolve a colour name to a hex
      description: Resolve a colour name or phrase to a hex via the colorscope brand
        libraries (exact + Fuse.js fuzzy) with a semantic fallback. Node-only.
        Requires the `mg:read` bearer scope.
      tags:
        - Search
      security:
        - bearerAuth: []
      parameters:
        - name: name
          in: query
          required: true
          description: A colour word or phrase, e.g. "sage" or "Gauntlet Gray".
          schema:
            type: string
      responses:
        "200":
          description: The resolved match, or null when nothing resolved.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResolveColourResponse"
        "400":
          description: Missing `?name=`. RFC 7807 problem.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/search/colours/suggest:
    get:
      operationId: suggest_colour_names
      summary: Ranked colour-name typeahead
      description: Ranked colour-name typeahead for UI autocomplete — descriptive
        names and brand paint names, fuzzy-tolerant. Node-only, synchronous (no
        network). Requires the `mg:read` bearer scope.
      tags:
        - Search
      security:
        - bearerAuth: []
      parameters:
        - name: q
          in: query
          required: true
          description: Partial colour name.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 20
            default: 8
      responses:
        "200":
          description: Ranked suggestions.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuggestColourResponse"
        "400":
          description: Missing `?q=`. RFC 7807 problem.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/colours/facets:
    get:
      operationId: get_colour_facets
      summary: Enumerate the colour value dictionaries
      description: "The three canonical colour vocabularies — family, mood, and
        undertone — each with its full list of terms and how many material
        variants and brands carry every term. This is the source for a canvas's
        colour filters: every canonical term appears even at zero counts, terms
        are sorted most-used first, and the dictionaries are returned in the
        order family, mood, undertone. No query parameters. The taxonomy shifts
        only with the data, so responses carry `Cache-Control: public,
        max-age=300`. Requires the `mg:read` bearer scope."
      tags:
        - Search
      security:
        - bearerAuth: []
      responses:
        "200":
          description: The colour value dictionaries with live counts.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ColourFacetsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/materials/{variantId}:
    get:
      operationId: get_material
      summary: Get a single material's detail record
      description: "Returns the `MaterialDetail` projection for a variant id:
        identity, image + source, colour block, category path, palette,
        variant-scoped facets + dimensions, installation shot.
        `?include=siblings` (comma list; only `siblings` recognised — an unknown
        value 400s naming the valid set) additionally hydrates the other
        variants of the same product as tiles. Detail reads are effectively
        static, so success responses carry `Cache-Control: public, max-age=300`.
        Requires the `mg:read` bearer scope."
      tags:
        - Materials
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/VariantIdPath"
        - name: include
          in: query
          required: false
          description: Comma list of includes. Only `siblings` is recognised.
          schema:
            type: string
            enum:
              - siblings
      responses:
        "200":
          description: The material detail record, with optional siblings.
          content:
            application/json:
              schema:
                type: object
                properties:
                  material:
                    $ref: "#/components/schemas/MaterialDetail"
                  siblings:
                    description: Present only when `?include=siblings` was requested.
                    type: array
                    items:
                      $ref: "#/components/schemas/MaterialTile"
                required:
                  - material
                additionalProperties: false
        "400":
          description: Unknown `?include=` value. RFC 7807 problem.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No material exists with the given variant id (`MATERIAL_NOT_FOUND`).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/materials/{variantId}/blocks:
    get:
      operationId: list_material_blocks
      summary: List a material's canonical blocks
      description: "Returns the canonical block completeness and status projection for
        one variant: canonical block ids and names, field values and statuses,
        and missing attribute ids. Success responses carry `Cache-Control:
        public, max-age=300`. Requires the `mg:read` bearer scope."
      tags:
        - Materials
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/VariantIdPath"
      responses:
        "200":
          description: The material's canonical block projection.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MaterialBlockProjection"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No material exists with the given variant id (`MATERIAL_NOT_FOUND`).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/materials/{variantId}/assets:
    get:
      operationId: list_material_assets
      summary: List a material's safe assets
      description: "Returns proxy-addressed images, safe downloadable files, and
        derived asset counts for one variant. Raw PIM image URLs and internal
        storage/provider metadata are excluded. Success responses carry
        `Cache-Control: public, max-age=300`. Requires the `mg:read` bearer
        scope."
      tags:
        - Materials
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/VariantIdPath"
      responses:
        "200":
          description: The material's safe asset projection.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MaterialAssetProjection"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No material exists with the given variant id (`MATERIAL_NOT_FOUND`).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/materials/{variantId}/siblings:
    get:
      operationId: get_material_siblings
      summary: Get a material's sibling variants
      description: "The other variants of the same product (colourways / finishes), as
        tiles. Returns `{ siblings: [] }` when the variant resolves but is the
        only variant of its product; 404 when the variant id itself does not
        resolve (the null-vs-empty-array distinction is load-bearing). Success
        responses carry `Cache-Control: public, max-age=300`. Requires the
        `mg:read` bearer scope."
      tags:
        - Materials
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/VariantIdPath"
      responses:
        "200":
          description: Sibling tiles.
          content:
            application/json:
              schema:
                type: object
                properties:
                  siblings:
                    type: array
                    items:
                      $ref: "#/components/schemas/MaterialTile"
                required:
                  - siblings
                additionalProperties: false
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No material exists with the given variant id (`MATERIAL_NOT_FOUND`).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/materials/{variantId}/pairings:
    get:
      operationId: get_material_pairings
      summary: Get the "what pairs with this material" rail
      description: 'A two-arm rail: real co-specification evidence (`source:
        "cospec"`) first, topped up by colour palette-similarity (`source:
        "colour"`) when cospec evidence is thin. A seed with no sampled order
        evidence instead falls back to board curation (`source: "considered"` —
        materials considered together on project boards). HONESTY CONTRACT: no
        source is true semantic pairing relevance — `cospec` is demand-lift
        co-occurrence (what projects specified together), `considered` is board
        activity (moodboard curation), `colour` is nearest-palette similarity
        (what merely looks adjacent). Never present a `considered` or `colour`
        item as "specified together"; `cospecCount` reports how many items came
        from real order evidence (the `cospec` source only). LATENCY: a
        thin-cospec seed runs up to 4–5 sequential round trips, so this is a
        click-to-expand rail, not a hover surface. Success responses carry
        `Cache-Control: public, max-age=300`. Requires the `mg:read` bearer
        scope.'
      tags:
        - Pairings
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/VariantIdPath"
        - name: categories
          in: query
          required: false
          description: Comma-separated coarse category display names; filters both arms.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 24
            default: 12
      responses:
        "200":
          description: The pairings rail.
          content:
            application/json:
              schema:
                type: object
                properties:
                  seed:
                    $ref: "#/components/schemas/MaterialTile"
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/PairingItem"
                  cospecCount:
                    type: integer
                    minimum: -9007199254740991
                    maximum: 9007199254740991
                    description: How many items came from real order evidence (the cospec arm).
                required:
                  - seed
                  - items
                  - cospecCount
                additionalProperties: false
        "400":
          description: Validation failure. RFC 7807 problem with `issues`.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No material found for the given variant id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/materials/{variantId}/substitutes:
    post:
      operationId: find_material_substitutes
      summary: Find exact-spec substitute candidates across brands
      description: "Finds cross-brand candidates with the same accepted leaf product
        type, primary material, and discrete performance class as the seed
        variant. The response states the exact `signatureUsed`, preserves
        per-criterion trust in `matched[].confidence` and
        `matched[].sourceBasis`, and reports the weakest-link `minConfidence`.
        Candidate reads are capped at 10,000 before deterministic
        trust/diversity ranking. Pagination uses an opaque `cursor`: pass the
        previous response's `nextCursor` unchanged to continue without
        duplicates or skips. Unsupported category families and insufficient
        signatures return explicit empty 200 envelopes. Results are discovery
        candidates only: this endpoint does not approve a project substitution
        or establish regulatory, performance, or specification equivalence.
        Responses carry `Cache-Control: no-store`. Requires the `mg:read` bearer
        scope."
      tags:
        - Materials
        - Search
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/VariantIdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FindSubstitutesRequest"
      responses:
        "200":
          description: Exact-spec candidates, or an explicit
            unsupported/insufficient-signature envelope.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FindSubstitutesResponse"
        "400":
          description: Invalid JSON body, cursor, exclusions, limit, or diversity boost.
            RFC 7807 problem with `issues`.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No material exists with the given variant id (`MATERIAL_NOT_FOUND`).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "413":
          description: Request body exceeds the 16 KiB cap.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/materials/{variantId}/revit:
    get:
      operationId: export_material_revit_package
      summary: Download a material's Revit specification package (.zip)
      description: 'Compiles a single material into a Revit "specification package"
        and streams it as a zip attachment: a shared-parameters definition, a
        type catalog (one row, keyed by stable GUID-derived parameter ids), a
        CSI MasterFormat classification, a manifest.json, a README, and — when
        the variant has one — a swatch.png. No PBR and no self-authored
        `.adsklib`; the native-appearance rung is marked "planned" in the
        manifest (it goes through APS Design Automation). Bytes are built fresh
        per request, so the response is `Cache-Control: no-store`. Requires the
        `mg:read` bearer scope.'
      tags:
        - Materials
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/VariantIdPath"
      responses:
        "200":
          description: The Revit spec-package zip.
          headers:
            Content-Disposition:
              description: Attachment disposition, e.g. `attachment;
                filename="<variantId>.revit-package.zip"`.
              schema:
                type: string
          content:
            application/zip:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No material exists with the given variant id (`MATERIAL_NOT_FOUND`).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/materials/batch:
    get:
      operationId: get_materials_batch_get
      summary: Batch-read materials by id (query-string form)
      description: "Cacheable query-string form of the batch read:
        `?ids=a,b,c&view=tile&idKind=variantId`. `missingIds` echoes any
        requested id (in the `idKind` namespace) that did not resolve. Mounted
        as a static sibling of the dynamic `/materials/{variantId}` segment —
        Next's router always prefers the static match, so this never falls
        through. Success responses carry `Cache-Control: public, max-age=300`.
        Requires the `mg:read` bearer scope."
      tags:
        - Materials
      security:
        - bearerAuth: []
      parameters:
        - name: ids
          in: query
          required: true
          description: Comma-separated ids, in the `idKind` namespace (max 100).
          schema:
            type: string
        - name: view
          in: query
          required: false
          schema:
            type: string
            enum:
              - tile
              - detail
            default: tile
        - name: idKind
          in: query
          required: false
          schema:
            type: string
            enum:
              - variantId
              - materialBankId
            default: variantId
      responses:
        "200":
          description: The resolved batch.
          content:
            application/json:
              schema:
                type: object
                properties:
                  materials:
                    type: array
                    items:
                      anyOf:
                        - $ref: "#/components/schemas/MaterialTile"
                        - $ref: "#/components/schemas/MaterialDetail"
                    description: "`MaterialTile[]` when `view=tile` (default), `MaterialDetail[]`
                      when `view=detail`. Order follows the resolved subset of
                      `ids`."
                  missingIds:
                    type: array
                    items:
                      type: string
                    description: Requested ids (in the `idKind` namespace) that did not resolve.
                required:
                  - materials
                  - missingIds
                additionalProperties: false
        "400":
          description: Validation failure (e.g. more than 100 ids). RFC 7807 problem with
            `issues`.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      operationId: get_materials_batch_post
      summary: Batch-read materials by id (JSON body form)
      description: "POST body is the full `getMaterialsInputSchema` contract (`{ ids,
        view?, idKind? }`, up to 100 ids per call). `missingIds` echoes any
        requested id (in the `idKind` namespace) that did not resolve. POST is
        `Cache-Control: no-store` (arbitrary id sets aren't cache-key-friendly).
        Requires the `mg:read` bearer scope."
      tags:
        - Materials
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MaterialsBatchRequest"
      responses:
        "200":
          description: The resolved batch.
          content:
            application/json:
              schema:
                type: object
                properties:
                  materials:
                    type: array
                    items:
                      anyOf:
                        - $ref: "#/components/schemas/MaterialTile"
                        - $ref: "#/components/schemas/MaterialDetail"
                    description: "`MaterialTile[]` when `view=tile` (default), `MaterialDetail[]`
                      when `view=detail`. Order follows the resolved subset of
                      `ids`."
                  missingIds:
                    type: array
                    items:
                      type: string
                    description: Requested ids (in the `idKind` namespace) that did not resolve.
                required:
                  - materials
                  - missingIds
                additionalProperties: false
        "400":
          description: Invalid JSON body or schema validation failure. RFC 7807 problem
            with `issues`.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/pairings/coordinate:
    post:
      operationId: coordinate_materials
      summary: Coordinate a palette of already-chosen materials
      description: "Given several already-chosen materials (by free-text `materials[]`
        and/or `variantIds[]`), returns demand-lift-ranked palette companions
        annotated with colour swatches and a `colourFit` score. v1 is
        ANNOTATE-ONLY: `colourFit` never reorders companions — ranking stays
        demand-lift. The engine result (`coordinateMaterials`) is passed through
        verbatim. Coordination is dynamic, so responses carry `Cache-Control:
        no-store`. Requires the `mg:read` bearer scope."
      tags:
        - Pairings
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CoordinateMaterialsRequest"
      responses:
        "200":
          description: The coordinated companion set.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CoordinateMaterialsResponse"
        "400":
          description: Invalid JSON body or schema validation failure. RFC 7807 problem
            with `issues`.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/demand:
    get:
      operationId: get_material_demand
      summary: How much a material is specified, plus what's specified alongside it
      description: "Composes two cospec domain reads for one seed and returns them
        side by side: `demand` (`getMaterialDemand` — headline
        spec/project/customer counts, the quarterly series, and momentum) and
        `cospec` (`getCoSpecifiedMaterials` — the materials specified alongside
        it, ranked by real order-history lift). Both domain results are passed
        through VERBATIM (no reshaping) — the route only runs them in parallel
        and echoes the resolved `query`. Provide EXACTLY ONE of `variantId` (an
        opaque material id) or `material` (free text); supplying neither is a
        400. This is the transport the ops demand console fetches. Demand is
        dynamic, so responses carry `Cache-Control: no-store`. Requires the
        `mg:read` bearer scope."
      tags:
        - Pairings
      security:
        - bearerAuth: []
      parameters:
        - name: variantId
          in: query
          required: false
          description: An opaque material (variant) id to seed the demand read. Provide
            exactly one of `variantId` or `material`; `variantId` wins if both
            are somehow present.
          schema:
            type: string
        - name: material
          in: query
          required: false
          description: Free-text material reference (e.g. a product or colour name) to
            resolve to a seed. Provide exactly one of `variantId` or `material`.
          schema:
            type: string
      responses:
        "200":
          description: The seed's demand and co-specified materials, side by side (both
            domain results verbatim).
          content:
            application/json:
              schema:
                type: object
                properties:
                  query:
                    type: string
                    description: Echo of the resolved reference (`variantId` or `material`).
                  demand:
                    $ref: "#/components/schemas/MaterialDemandResult"
                  cospec:
                    $ref: "#/components/schemas/CoSpecifiedResult"
                required:
                  - query
                  - demand
                  - cospec
                additionalProperties: false
        "400":
          description: Neither `variantId` nor `material` was supplied. RFC 7807 problem.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/demand/batch:
    post:
      operationId: get_demand_batch
      summary: Enrich a bounded set of exact candidate ids with specification demand
      description: "Given 1–48 already-selected canonical variant ids, return each
        one's real specification demand in INPUT ORDER and CARDINALITY
        (duplicates echoed) — with no fuzzy resolution, ranking, or reordering.
        The bounded batch complement to the single-seed `/demand` read; mirrors
        the `/materials/batch` POST convention. Each input yields one
        discriminated `state`: `available` (headline spec/project/customer
        `population`, the exact quarterly `intervals`, and `momentum`),
        `insufficient_data` (a stats row exists but is below the quality floor),
        `no_signal` (no stats), or `unknown_variant` (no such variant). PRIVACY:
        a public (`mg:read`) caller can never tell a privacy-suppressed variant
        from an absent one — both read `no_signal`; an elevated (`mg:admin`)
        principal is `internal` and receives exact per-result `evidence`. The
        envelope carries the `generationId` (the dataset build), the
        `metric`/`universe`, the `guardrail` floors, and `coverage` tallies
        (never a denominator over the submitted ids). Demand is dynamic, so
        responses carry `Cache-Control: no-store`. Requires the `mg:read` bearer
        scope."
      tags:
        - Pairings
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EnrichCandidateDemandRequest"
      responses:
        "200":
          description: One discriminated demand result per input id, in input order and
            cardinality.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnrichCandidateDemandResponse"
        "400":
          description: Invalid JSON body or a schema validation failure (e.g. more than 48
            ids, or a non-canonical id). RFC 7807 problem with `issues`.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          description: The demand dataset is unavailable (no completed build for the
            window/signal). Retriable.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /api/v1/demand/signals:
    get:
      operationId: get_demand_signals
      summary: Resolve free text to one contextual demand mover
      description: "Maps a free-text query (`q`, min 3 chars) to the single most
        relevant eligible demand mover, walking leaf → group → brand → colour
        precedence against the shared `/ops/demand` overview cache. Returns `{
        query, signal }` with `signal: null` when nothing matches.
        `Cache-Control: no-store`. Requires the `mg:read` bearer scope."
      tags:
        - Pairings
      security:
        - bearerAuth: []
      parameters:
        - name: q
          in: query
          required: true
          description: Free-text demand query; trimmed length must be ≥ 3.
          schema:
            type: string
            minLength: 3
      responses:
        "200":
          description: The query echo and matched contextual signal (or null).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DemandSignalsResponse"
        "400":
          description: "`q` missing or shorter than 3 characters after trim."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/codes/advisory:
    post:
      operationId: resolveCodeAdvisory
      summary: Badge materials against the advisory building-code registry
      description: "Evaluate up to 48 materials against the in-repo code-requirement
        registry for one context (`jurisdictionId`, `occupancyGroup`,
        `spaceType`, `sprinklered`). For each variant, every applicable
        requirement is badged `meets` / `shortfall` / `unknown` from the
        variant's accepted `fire_classifications` and `slip_resistance_dcof`
        facts (variant-scope facts shadow product-scope). `results` preserve
        input order and cardinality; a variant that does not resolve is returned
        with `state: \"unknown_variant\"` and `requirements: []`. Fire tokens
        with no defensible mapping onto an IBC scale (euroclass, CAL TB 117, …)
        resolve to `unknown`, never a guessed equivalence. ADVISORY ONLY — every
        verdict and the envelope carry the AHJ caveat; this is model code as
        adopted, never a legal determination. A 400 is returned for an unknown
        jurisdiction, an uncovered occupancy group, or an unrecognised space
        type. Reads live facts, so responses carry `Cache-Control: no-store`.
        Requires the `mg:read` bearer scope."
      tags:
        - Codes
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CodeAdvisoryRequest"
      responses:
        "200":
          description: Per-variant, per-requirement advisory verdicts.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CodeAdvisoryResponse"
        "400":
          description: Invalid JSON body, a schema validation failure (Zod `issues`), or a
            rejected context (unknown jurisdiction / uncovered occupancy /
            unrecognised space). RFC 7807 problem.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/schemes/vary:
    post:
      operationId: vary_schemes
      summary: Derive an option-B scheme from an existing one along a constraint axis
      description: "Re-derive a composed scheme along one constraint axis, re-picking
        every unlocked slot while preserving the source brief's hard gates. The
        scheme is referenced by `runId` (a succeeded `compose-material-schemes`
        Run, owned by the caller) and, optionally, `schemeId` (defaults to the
        Run's first scheme). Axes: `palette` (shift each unlocked slot's colour
        cues to the programme palette rotated by one position), `sustainability`
        (add a certification floor filter on top of the required criteria),
        `brandDiversity` (hard-exclude any brand used ≥2× in the source scheme
        and double the brand-repeat penalty), and `priceTier` — an HONEST
        brand-level prestige-tier proxy (there is NO per-SKU price data) that
        leans toward the OPPOSITE tier half from the source's dominant band.
        `locks` keep chosen slots pinned to their current pick; every unlocked
        slot's source pick is excluded from its own re-derivation, so option B
        differs wherever the pool allows. Returns the hydrated compose wire
        shape stamped with the composition version, so option B renders
        identically to option A, plus axis + source provenance. Deterministic
        (it skips the LLM parse pipeline) and `Cache-Control: no-store`.
        Requires the `mg:read` bearer scope. 404 when the run is unknown, not
        owned, or not a compose Run; 409 when it has not succeeded or its output
        is unusable; 400 on an invalid body, unknown `schemeId`, or an invalid
        lock."
      tags:
        - Composition
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VarySchemesRequest"
      responses:
        "200":
          description: The hydrated option-B scheme with axis and source provenance.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VarySchemesResponse"
        "400":
          description: Invalid JSON body, a schema failure (Zod `issues`), an unknown
            `schemeId`, or an invalid lock. RFC 7807 problem.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No compose Run exists for `runId`, it is not owned by the caller,
            or it is a different capability.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: The Run has not succeeded, or its stored output is unusable.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/extract:
    get:
      operationId: get_extract_run
      summary: Get a persisted extract run snapshot
      description: Returns the replayable snapshot for one brand-agnostic
        graph-extraction run (events, status, source URL). Requires the
        `mg:read` bearer scope.
      tags:
        - Runs
      security:
        - bearerAuth: []
      parameters:
        - name: runId
          in: query
          required: true
          description: Persisted run id returned by the matching POST stream.
          schema:
            type: string
            minLength: 1
      responses:
        "200":
          description: The persisted extract run snapshot.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExtractRunSnapshot"
        "400":
          description: Missing or empty `runId`.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No extract run exists with the given id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      operationId: start_extract_run
      summary: Start graph extraction from a public product URL
      description: 'Starts (or resumes) brand-agnostic graph extraction for one public
        https product URL and streams progress as server-sent events. The first
        frame carries `{ type: "run", runId, href }`; subsequent frames are
        extract stage/attribute/media/error events. Heartbeats are sent as SSE
        comments. Requires the `mg:ingest` bearer scope. Soft wall budget up to
        300s.'
      tags:
        - Runs
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PublicHttpsUrlRequest"
      responses:
        "200":
          description: SSE extract progress frames (run / stage / attribute / media /
            error).
          content:
            text/event-stream:
              schema:
                type: string
                description: 'SSE `data:` JSON frames. First frame is `{ type: "run", runId,
                  href }`; later frames mirror the extract event union.'
        "400":
          description: Invalid JSON body or a non-public https URL.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/extract-agent:
    post:
      operationId: stream_extract_agent
      summary: Stream a bounded Agentic Extraction console round
      description: Streams the non-durable `graph-extraction-agent` for one AI
        Elements console round via Mastra `handleChatStream` → AI SDK UIMessage
        stream. Each request is a BOUNDED slice (≤6 steps or ~200s soft wall) so
        the client can resume the same memory thread instead of blowing the 300s
        function ceiling. Requires the `mg:read` bearer scope.
      tags:
        - Enrichment
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentChatRequest"
      responses:
        "200":
          description: AI SDK UIMessage stream for one extraction round.
          content:
            text/event-stream:
              schema:
                type: string
                description: AI SDK UIMessage stream frames from `createUIMessageStreamResponse`
                  (reasoning + sources when enabled).
        "400":
          description: Invalid JSON or missing non-empty `messages` array.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/verify:
    get:
      operationId: get_verify_run
      summary: Get a persisted verify run snapshot
      description: Returns the replayable snapshot for one attribute-verify run
        (events, status, scope). Requires the `mg:read` bearer scope.
      tags:
        - Runs
      security:
        - bearerAuth: []
      parameters:
        - name: runId
          in: query
          required: true
          description: Persisted run id returned by the matching POST stream.
          schema:
            type: string
            minLength: 1
      responses:
        "200":
          description: The persisted verify run snapshot.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VerifyRunSnapshot"
        "400":
          description: Missing or empty `runId`.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No verify run exists with the given id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      operationId: start_verify_run
      summary: Re-check stored attribute values against source evidence
      description: "Starts a verify run for exactly one of `variantId` or `productId`
        (plus required `ingestionSessionId`) and streams progress as server-sent
        events. Write policy is propose-only: discrepancies become review tasks
        rather than silent overwrites. Requires the `mg:ingest` bearer scope.
        Soft wall budget up to 300s."
      tags:
        - Runs
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VerifyScopeRequest"
      responses:
        "200":
          description: SSE verify progress frames (run / stage / attribute / verdict /
            proposed / error).
          content:
            text/event-stream:
              schema:
                $ref: "#/components/schemas/VerifyStreamEvent"
        "400":
          description: Invalid JSON body, missing exactly-one scope, or schema validation
            failure.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/material-agent:
    post:
      operationId: stream_material_agent
      summary: Stream the Material Research Agent console
      description: Streams the non-durable `material-research-agent` over the
        normalised graph (discover / query / similar) as an AI SDK UIMessage
        stream. Read-only research; confidence + source basis when tools emit
        them. Requires the `mg:read` bearer scope. Soft wall budget up to 300s.
      tags:
        - Enrichment
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentChatRequest"
      responses:
        "200":
          description: AI SDK UIMessage stream for a research turn.
          content:
            text/event-stream:
              schema:
                type: string
                description: AI SDK UIMessage stream frames from `createUIMessageStreamResponse`
                  (reasoning + sources when enabled).
        "400":
          description: Invalid JSON or missing non-empty `messages` array.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/content-compare:
    post:
      operationId: compare_content_providers
      summary: Compare Firecrawl / Exa / Tavily harvest quality for one URL
      description: Fetches one public https URL through Firecrawl, Exa /contents, and
        Tavily extract in parallel and returns side-by-side harvest quality (ok,
        ms, chars, excerpt, title, answer, error). Paid provider traffic —
        requires the `mg:ingest` bearer scope. Soft wall budget up to 120s.
      tags:
        - Sources
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PublicHttpsUrlRequest"
      responses:
        "200":
          description: Per-provider harvest comparison.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContentCompareResponse"
        "400":
          description: Invalid JSON body or a non-public https URL.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          description: Comparison failed after the providers ran.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /api/v1/normalisation-proof:
    get:
      operationId: get_normalisation_proof
      summary: North-star normalisation proof stats
      description: "Returns precomputed collapse + lift stats for the normalisation
        north-star claim (messy source spellings → canonical tokens, and how
        many more materials a canonical query finds). Reads
        `normalisation_proof_stats` only — never measures live. `Cache-Control:
        no-store`. Requires the `mg:read` bearer scope."
      tags:
        - Catalog
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Ordered normalisation proof entries.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NormalisationProofResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          description: The proof stats table is empty (seed required).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /api/v1/agentic/subject-chat:
    post:
      operationId: stream_subject_chat
      summary: Stream one operator subject-chat turn
      description: Streams one subject-chat turn for an operator agentic subject
        surface as SSE `data:` frames (`token` / `activity` / `done` / `error`).
        Mid-stream failures arrive as in-band error frames rather than HTTP
        errors. Requires the `mg:ingest` bearer scope. Soft wall budget up to
        300s.
      tags:
        - Enrichment
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SubjectChatRequest"
      responses:
        "200":
          description: SSE subject-chat frames.
          content:
            text/event-stream:
              schema:
                $ref: "#/components/schemas/SubjectChatStreamEvent"
        "400":
          description: Invalid JSON body or schema validation failure.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/agentic/subject-override:
    post:
      operationId: apply_subject_override
      summary: Apply an operator manual attribute override
      description: "Writes one source-backed attribute override for an
        attribute-scoped target through a runtime ingestion session (auditable).
        Returns `{ success: true, status }` where `status` is `proposed` or
        `accepted`. Requires the `mg:ingest` bearer scope."
      tags:
        - Enrichment
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SubjectOverrideRequest"
      responses:
        "200":
          description: Override accepted or proposed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubjectOverrideResponse"
        "400":
          description: Invalid JSON, unknown attribute, or a target not scoped to the
            attribute.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          description: The override write failed after the session started.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /api/v1/catalog/attributes/resolve:
    get:
      operationId: catalog_resolve_attribute
      summary: Resolve a spec phrase to canonical attribute ids
      description: Resolves a free-text spec phrase ("double rubs", "how
        fire-resistant", "eco certifications") to the canonical registry
        attribute ids it most likely means, ranked by meaning (best first).
        Alias matching is included — it rides the same registry embeddings, so
        it never drifts from a hand-maintained alias table. Each candidate is
        flagged `facetable`, telling you which ids work with
        `search/specifications`. The phrase is embedded per request (voyage text
        encoder), so this is a dynamic, Node-only endpoint. Requires the
        `mg:read` bearer scope.
      tags:
        - Catalog
      security:
        - bearerAuth: []
      parameters:
        - name: q
          in: query
          required: true
          description: A free-text spec phrase to resolve, e.g. "double rubs".
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Max candidates to return.
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 8
        - name: facetableOnly
          in: query
          required: false
          description: When `true` (or `1`), drop non-facetable attributes and return only
            queryable spec facets.
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: The ranked attribute candidates for the phrase.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResolveAttributeResponse"
        "400":
          description: Missing `?q=`, or a non-integer `?limit=`. RFC 7807 problem.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/catalog/attributes/{attributeId}/values/resolve:
    get:
      operationId: catalog_resolve_value
      summary: Resolve a value phrase to a canonical term for one attribute
      description: Resolves a free-text value phrase ("navy", "flat, no shine") to the
        canonical term id it most likely means within one dictionary-backed
        registry attribute's value dictionary, ranked by confidence with a
        `resolved` / `ambiguous` / `unresolved` verdict — fail-fast, so the
        caller must act on the verdict rather than receive a silent best guess.
        An `{attributeId}` of `colour_family` additionally runs a colorscope
        fallback (brand paint names, descriptive phrases the dictionary can't
        name) on top of the base term ladder; every other dictionary-backed
        attribute resolves through the plain lexical/semantic ladder alone.
        `{attributeId}` must name a dictionary-backed attribute (one with a
        value dictionary) — a 400 explains when it doesn't exist or is
        free-form. Requires the `mg:read` bearer scope.
      tags:
        - Catalog
      security:
        - bearerAuth: []
      parameters:
        - name: attributeId
          in: path
          required: true
          description: A dictionary-backed registry attribute id, e.g. "colour_family".
            See `catalog/attributes/resolve` to find it from a phrase.
          schema:
            type: string
        - name: q
          in: query
          required: true
          description: A free-text value phrase to resolve, e.g. "navy".
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Max candidates to return.
          schema:
            type: integer
            minimum: 1
            maximum: 20
            default: 8
      responses:
        "200":
          description: The ranked term candidates for the phrase.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResolveValueResponse"
        "400":
          description: Missing `?q=`, an unknown or non-dictionary-backed `{attributeId}`,
            or a non-integer `?limit=`. RFC 7807 problem.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/catalog/vocabulary/search:
    get:
      operationId: catalog_vocabulary_search
      summary: Search the controlled vocabularies by meaning or spelling
      description: Lexical + semantic search over MaterialGraph's controlled
        vocabularies — registry attributes AND value-dictionary terms — at once.
        Turns a free-text phrase ("herringbone", "double rubs") into the
        canonical tokens it most likely means, ranked by meaning, each flagged
        with its `kind` (`attribute` or `term`); a term also carries the ids of
        the attributes that own its value dictionary. Use this to discover a
        canonical token when you don't know it; once you know the attribute, use
        `attributes/{attributeId}/values/resolve` to resolve a value against its
        dictionary. Requires the `mg:read` bearer scope.
      tags:
        - Catalog
      security:
        - bearerAuth: []
      parameters:
        - name: q
          in: query
          required: true
          description: A free-text phrase to search the vocabularies for, e.g.
            "herringbone".
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Max candidates to return.
          schema:
            type: integer
            minimum: 1
            maximum: 25
            default: 10
        - name: kind
          in: query
          required: false
          description: Restrict the search to one vocabulary. Omit to search both
            attributes and terms.
          schema:
            type: string
            enum:
              - attribute
              - term
      responses:
        "200":
          description: The ranked vocabulary candidates for the phrase.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VocabularySearchResponse"
        "400":
          description: Missing `?q=`, a non-integer `?limit=`, or an invalid `?kind=`. RFC
            7807 problem.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/catalog/specifications/facets:
    get:
      operationId: list_spec_facets
      summary: Discover the queryable specification vocabulary
      description: "Lists the facetable material-spec attributes that have live
        product-graph data, each with its top canonical values and the variants
        and brands behind them. This is the discovery companion to
        `search/specifications`: learn an `attribute:value` vocabulary here,
        then use it as a repeatable search criterion. Facets shift only with
        data, so responses carry `Cache-Control: public, max-age=300`. Requires
        the `mg:read` bearer scope."
      tags:
        - Catalog
      security:
        - bearerAuth: []
      responses:
        "200":
          description: The live specification-facet collection.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SpecFacetsListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/catalog/attributes/{attributeId}/values:
    get:
      operationId: list_attribute_values
      summary: List the live values for one attribute
      description: "Lists one attribute's live product-graph value distribution,
        including induced vocabularies absent from the static registry. Values
        are ordered by cross-brand reach and include variant and brand counts.
        Attribute identity is carried by the path. Responses carry
        `Cache-Control: public, max-age=300`. Requires the `mg:read` bearer
        scope."
      tags:
        - Catalog
      security:
        - bearerAuth: []
      parameters:
        - name: attributeId
          in: path
          required: true
          description: The canonical attribute id whose live values should be listed.
          schema:
            type: string
      responses:
        "200":
          description: The attribute's live value distribution.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SpecFacetsDistributionResponse"
        "400":
          description: The required `{attributeId}` path identity is absent. RFC 7807
            problem.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/taxonomy/categories:
    get:
      operationId: get_category_taxonomy
      summary: Get the product-category tree
      description: 'The MaterialGraph product-category tree, flattened into a single
        array of `{key, name, parentKey, path, level}` nodes projected purely
        from the schema registry (no database). Groups are top-level
        (`parentKey: null`); each product type points back at its group via
        `parentKey`, with a display-name `path` like "Tile Surfaces / Ceramic
        and Porcelain Tile". The taxonomy only changes on deploy, so responses
        carry `Cache-Control: public, max-age=3600`. Requires the `mg:read`
        bearer scope.'
      tags:
        - Catalog
      security:
        - bearerAuth: []
      responses:
        "200":
          description: The flat category-node list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CategoryTaxonomyResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/taxonomy/categories/{key}/specs:
    get:
      operationId: get_applicable_specs
      summary: Get the specs a product type could carry
      description: "Abstract applicability — given a product type (`key` = its id or
        display name), the blocks + attributes the SCHEMA says COULD apply, each
        block graded required / recommended / optional / unusual and each
        attribute flagged when the type pins it, regardless of whether any
        variant carries data. The abstract complement to
        `catalog/specifications/facets` (which reports the values that actually
        exist). Pure and in-memory, so responses carry `Cache-Control: public,
        max-age=86400`. 404 when `key` names no product type (including a
        category GROUP, whose problem `detail` hints to pick a specific type).
        Requires the `mg:read` bearer scope."
      tags:
        - Catalog
      security:
        - bearerAuth: []
      parameters:
        - name: key
          in: path
          required: true
          description: A product-type id or display name (from `taxonomy/categories`).
          schema:
            type: string
      responses:
        "200":
          description: The applicable blocks and attributes for the product type.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApplicableSpecsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: The key names no product type (`NOT_FOUND`).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/glossary/{term}:
    get:
      operationId: explain_material_term
      summary: Resolve a term to its meaning in the schema
      description: "The schema's own glossary — resolves `term` (an attribute
        id/alias, or a canonical dictionary value) to its MEANING: an attribute
        definition (kind / unit / block membership) or a dictionary value's
        definition, so an agent can interpret the attribute ids and value tokens
        it sees in responses. Pure and in-memory, so responses carry
        `Cache-Control: public, max-age=86400`. 404 when the term does not
        resolve, with a hint to discover the vocabulary via
        `catalog/specifications/facets`. Requires the `mg:read` bearer scope."
      tags:
        - Catalog
      security:
        - bearerAuth: []
      parameters:
        - name: term
          in: path
          required: true
          description: An attribute id/alias or a canonical dictionary value token.
          schema:
            type: string
      responses:
        "200":
          description: The resolved term meaning.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GlossaryTermResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: The term does not resolve to any schema attribute or value
            (`NOT_FOUND`).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/schema/blocks:
    get:
      operationId: list_schema_blocks
      summary: List canonical schema blocks
      description: "Returns one deterministic governed schema registry with semantic
        and lifecycle metadata. Responses carry `Cache-Control: public,
        max-age=86400`. Requires the `mg:read` bearer scope."
      tags:
        - Schema
      security:
        - bearerAuth: []
      responses:
        "200":
          description: List canonical schema blocks
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SchemaBlocksResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/schema/attributes:
    get:
      operationId: list_schema_attributes
      summary: List canonical schema attributes
      description: "Returns one deterministic governed schema registry with semantic
        and lifecycle metadata. Responses carry `Cache-Control: public,
        max-age=86400`. Requires the `mg:read` bearer scope."
      tags:
        - Schema
      security:
        - bearerAuth: []
      responses:
        "200":
          description: List canonical schema attributes
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SchemaAttributesResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/schema/verticals:
    get:
      operationId: list_schema_verticals
      summary: List schema verticals
      description: "Returns one deterministic governed schema registry with semantic
        and lifecycle metadata. Responses carry `Cache-Control: public,
        max-age=86400`. Requires the `mg:read` bearer scope."
      tags:
        - Schema
      security:
        - bearerAuth: []
      responses:
        "200":
          description: List schema verticals
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SchemaVerticalsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/schema/product-types:
    get:
      operationId: list_schema_product_types
      summary: List governed product types
      description: "Returns one deterministic governed schema registry with semantic
        and lifecycle metadata. Responses carry `Cache-Control: public,
        max-age=86400`. Requires the `mg:read` bearer scope."
      tags:
        - Schema
      security:
        - bearerAuth: []
      responses:
        "200":
          description: List governed product types
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SchemaProductTypesResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/schema/units:
    get:
      operationId: list_schema_units
      summary: List canonical units
      description: "Returns one deterministic governed schema registry with semantic
        and lifecycle metadata. Responses carry `Cache-Control: public,
        max-age=86400`. Requires the `mg:read` bearer scope."
      tags:
        - Schema
      security:
        - bearerAuth: []
      responses:
        "200":
          description: List canonical units
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SchemaUnitsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/schema/value-dictionaries:
    get:
      operationId: list_schema_value_dictionaries
      summary: List canonical value dictionaries
      description: "Returns one deterministic governed schema registry with semantic
        and lifecycle metadata. Responses carry `Cache-Control: public,
        max-age=86400`. Requires the `mg:read` bearer scope."
      tags:
        - Schema
      security:
        - bearerAuth: []
      responses:
        "200":
          description: List canonical value dictionaries
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SchemaValueDictionariesResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/schema/source-requirements:
    get:
      operationId: list_schema_source_requirements
      summary: List source requirements
      description: "Returns one deterministic governed schema registry with semantic
        and lifecycle metadata. Responses carry `Cache-Control: public,
        max-age=86400`. Requires the `mg:read` bearer scope."
      tags:
        - Schema
      security:
        - bearerAuth: []
      responses:
        "200":
          description: List source requirements
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SchemaSourceRequirementsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/schema/product-type-templates:
    get:
      operationId: list_schema_product_type_templates
      summary: List product-type completeness templates
      description: "Returns one deterministic governed schema registry with semantic
        and lifecycle metadata. Responses carry `Cache-Control: public,
        max-age=86400`. Requires the `mg:read` bearer scope."
      tags:
        - Schema
      security:
        - bearerAuth: []
      responses:
        "200":
          description: List product-type completeness templates
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SchemaProductTypeTemplatesResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/schema/mappings:
    get:
      operationId: list_schema_mappings
      summary: List interoperability mappings
      description: "Returns one deterministic governed schema registry with semantic
        and lifecycle metadata. Responses carry `Cache-Control: public,
        max-age=86400`. Requires the `mg:read` bearer scope."
      tags:
        - Schema
      security:
        - bearerAuth: []
      responses:
        "200":
          description: List interoperability mappings
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SchemaMappingsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/brands:
    get:
      operationId: list_brands
      summary: List registered brands
      description: Lists canonical brand registry entries, including brands that do
        not yet have variants. Results are ordered deterministically by name,
        slug, and id and use offset pagination. Requires the `mg:read` bearer
        scope.
      tags:
        - Brands
      security:
        - bearerAuth: []
      parameters:
        - name: q
          in: query
          required: false
          description: Case-insensitive brand-name filter.
          schema:
            type: string
            minLength: 1
        - name: limit
          in: query
          required: false
          description: Maximum brands to return.
          schema:
            type: integer
            minimum: 1
            maximum: 250
            default: 50
        - name: offset
          in: query
          required: false
          description: Zero-based number of ordered brands to skip.
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: The requested page of canonical brands.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BrandCatalogListPage"
        "400":
          description: Invalid q, limit, or offset. RFC 7807 problem.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/brands/discovery/runs:
    post:
      operationId: create_brand_discovery_run
      summary: Start a brand-site discovery Run
      description: Starts a persisted, non-destructive discovery Run for one public
        HTTPS brand homepage and streams run, progress, structure, and error
        frames as server-sent events. `maxCandidates` is optional and bounded
        from 10 to 5000. Requires the `mg:ingest` bearer scope.
      tags:
        - Brands
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BrandDiscoveryRunInput"
      responses:
        "200":
          description: Brand discovery Run frames.
          content:
            text/event-stream:
              schema:
                $ref: "#/components/schemas/BrandDiscoveryRunStreamFrame"
        "400":
          description: Invalid body, homepage URL, or max-candidate bound.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          description: The discovery Run store is unavailable.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /api/v1/brands/discovery/runs/{runId}:
    get:
      operationId: get_brand_discovery_run
      summary: Get a brand discovery Run snapshot
      description: Returns the replayable snapshot for the path-owned brand discovery
        Run. Requires the `mg:read` bearer scope.
      tags:
        - Brands
      security:
        - bearerAuth: []
      parameters:
        - name: runId
          in: path
          required: true
          description: The persisted brand discovery Run id.
          schema:
            type: string
      responses:
        "200":
          description: The persisted brand discovery Run snapshot.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BrandDiscoveryRunSnapshot"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No brand discovery Run exists with the given id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          description: The discovery Run store is unavailable.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /api/v1/brands/{brandSlug}/fields/{fieldKey}/enrich:
    post:
      operationId: stream_enrichment
      summary: Stream brand-field enrichment
      description: Streams progress and the terminal result or error for one
        path-owned brand field. The request has no body. Requires the
        `mg:ingest` bearer scope.
      tags:
        - Enrichment
      security:
        - bearerAuth: []
      parameters:
        - name: brandSlug
          in: path
          required: true
          description: The canonical brand slug.
          schema:
            type: string
            minLength: 1
            maxLength: 128
            pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        - name: fieldKey
          in: path
          required: true
          description: The brand field key to enrich.
          schema:
            type: string
            minLength: 1
            maxLength: 128
            pattern: ^[a-z0-9]+(?:_[a-z0-9]+)*$
      responses:
        "200":
          description: Brand enrichment stream events.
          content:
            text/event-stream:
              schema:
                $ref: "#/components/schemas/BrandEnrichmentStreamEvent"
        "400":
          description: Missing or invalid brand slug or field key.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/brands/{brandSlug}:
    get:
      operationId: get_brand_profile
      summary: Resolve a brand and return its profile
      description: "Brand identity (HQ, parent company, positioning, website) plus
        trade / specification signals (Material Bank presence, BIM library,
        sample program, trade program, CPD/CEU) — the spec-channel questions the
        per-material reads can't answer. `brandSlug` is the brand slug, name, or
        id; `?q=` supplies an alternate fuzzy query and wins over the path.
        Resolution is exact → slug/id → trigram fuzzy; on ambiguity the best
        match is returned and the rest ride `alternates`. Brand identity is
        effectively static, so responses carry `Cache-Control: public,
        max-age=300`. Requires the `mg:read` bearer scope."
      tags:
        - Brands
      security:
        - bearerAuth: []
      parameters:
        - name: brandSlug
          in: path
          required: true
          description: The brand slug, name, or id.
          schema:
            type: string
        - name: q
          in: query
          required: false
          description: Alternate fuzzy query; takes precedence over the path when present.
          schema:
            type: string
      responses:
        "200":
          description: The resolved brand profile.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BrandProfileResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No brand resolved for the given slug/name/query (`NOT_FOUND`).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/brands/{brandSlug}/revit/library:
    get:
      operationId: export_brand_revit_library
      summary: Download a brand's bulk Revit library (.zip)
      description: "Reads every variant of a brand in one round-trip and composes a
        bulk Revit library zip — a shared-parameters definition, one
        type-catalog row per material, a colours.csv reference, and a README.
        Leaner than the per-material package (no per-material classification).
        `?limit=` (default 1000, capped at 5000) bounds the export; when the
        brand has more materials than the cap, the library is truncated, the
        response carries `X-MG-Truncated: true`, AND the README states the
        truncation in band. Bytes are built fresh per request, so the response
        is `Cache-Control: no-store`. 404 when the brandSlug resolves to no
        exportable materials. Requires the `mg:read` bearer scope."
      tags:
        - Brands
      security:
        - bearerAuth: []
      parameters:
        - name: brandSlug
          in: path
          required: true
          description: The brand slug, name, or id.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: "Max materials to export (default 1000, capped at 5000). When the
            brand has more, the library is truncated and `X-MG-Truncated: true`
            is set."
          schema:
            type: integer
            minimum: 1
            maximum: 5000
            default: 1000
      responses:
        "200":
          description: The bulk Revit library zip.
          headers:
            Content-Disposition:
              description: Attachment disposition, e.g. `attachment;
                filename="<slug>.revit-library.zip"`.
              schema:
                type: string
            X-MG-Truncated:
              description: Present as `"true"` only when the brand overflowed the cap.
              schema:
                type: string
                enum:
                  - "true"
          content:
            application/zip:
              schema:
                type: string
                format: binary
        "400":
          description: Invalid `?limit=` value (non-integer or < 1). RFC 7807 problem.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No brand resolved for the given slug (`NOT_FOUND`).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/source-material:
    post:
      operationId: upload_source_material
      summary: Upload a source-material batch — a heterogeneous pile of files
      description: "Accepts one atomic multipart batch as a PILE of source files: a
        repeatable `files` field carrying any mix of PDF (≤30 MiB, ≤250 pages),
        plain-text/markdown (≤2 MiB), and images (JPEG, PNG, WebP, or AVIF, each
        ≤8 MiB) — at least one file, at most 16, ≤128 MiB aggregate. There is no
        need to pre-sort a brief from its references: the pipeline classifies
        each file and stamps its receipt `role` (`brief`, `reference_image`,
        `document`, `document_image`, `inspiration_image`, or `unclassified`).
        The legacy pre-classified shape — `brief` (a single PDF) plus a
        repeatable `references` field (images) — is still accepted and folded
        into the pile. At least one of `files`, `brief`, or `references` must be
        present. Bytes are validated strictly (media type, magic bytes, PDF
        structure and page count) before any write; any invalid file rejects the
        whole batch with per-file reasons in the problem `issues` member and
        nothing is stored. On success the bytes are stored content-addressed and
        a typed receipt is returned. Requires the `mg:compose` bearer scope
        (designer working data — a `compose` key suffices; broader
        ingest/write/admin keys also carry it)."
      tags:
        - Sources
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              description: Send the pile via `files`. `brief`/`references` are the legacy
                shape, folded into the pile. At least one file must be present
                across these fields.
              properties:
                files:
                  type: array
                  items:
                    type: string
                    format: binary
                  description: "The heterogeneous pile: any mix of PDF, text/markdown, and images
                    (≥1, ≤16 files across the whole batch)."
                brief:
                  type: string
                  format: binary
                  description: "Legacy: the single PDF brief for the batch. Folded into `files`."
                references:
                  type: array
                  items:
                    type: string
                    format: binary
                  description: "Legacy: up to 12 reference images (JPEG, PNG, WebP, or AVIF).
                    Folded into `files`."
      responses:
        "201":
          description: The stored batch receipt.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SourceMaterialBatchReceipt"
        "400":
          description: The request was malformed or one or more files failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/projects:
    post:
      operationId: create_box_project
      summary: Create a project
      description: Creates a persistent Material Box project owned by the
        authenticated principal. An optional `name` defaults to "Untitled
        project". The response is the freshly assembled project state (an empty
        event log). Requires the `mg:compose` bearer scope (designer working
        data — a `compose` key suffices; broader ingest/write/admin keys also
        carry it).
      tags:
        - Projects
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateProjectRequest"
      responses:
        "201":
          description: The created project's assembled state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectState"
        "400":
          description: The request body failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
    get:
      operationId: list_box_projects
      summary: List your projects
      description: Lists the authenticated principal's own projects, newest first,
        capped at 50. Summaries only — no event fold. Requires the `mg:read`
        bearer scope.
      tags:
        - Projects
      security:
        - bearerAuth: []
      responses:
        "200":
          description: The caller's projects, newest first.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/projects/{projectId}:
    get:
      operationId: get_box_project
      summary: Get a project's assembled state
      description: Returns the caller-owned project's read model — its identity plus
        the collections folded from its append-only event log (attached batches,
        started Runs, clarifying answers, pairwise preferences, chosen
        references, adopted schemes) and the full ordered event trail. A project
        owned by another principal is indistinguishable from a missing one
        (404). Requires `mg:read`.
      tags:
        - Projects
      security:
        - bearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          description: The project identifier returned by `POST /api/v1/projects`
            (`proj_…`).
          schema:
            type: string
      responses:
        "200":
          description: The assembled project state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectState"
        "400":
          description: The projectId path segment is empty.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No project with that id is owned by the authenticated principal.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/projects/{projectId}/events:
    post:
      operationId: append_box_project_event
      summary: Append a project event
      description: Appends one immutable event to the caller-owned project's process
        trail. The body is `{ kind, payload }`; `kind` must be one of
        `batch-attached`, `run-started`, `clarifying-answer`,
        `pairwise-preference`, `reference-chosen`, or `scheme-adopted`, and
        `payload` must satisfy that kind's schema (else 400 with per-issue
        detail). Appending against a project owned by another principal returns
        404. Requires `mg:compose` (designer working data; broader
        ingest/write/admin keys also carry it).
      tags:
        - Projects
      security:
        - bearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          description: The project identifier returned by `POST /api/v1/projects`
            (`proj_…`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AppendProjectEventRequest"
      responses:
        "201":
          description: The appended event receipt.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectEventReceipt"
        "400":
          description: The event kind or payload failed validation.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No project with that id is owned by the authenticated principal.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/whoami:
    get:
      operationId: whoami
      summary: Echo the authenticated principal and scopes
      description: "A sanity-check endpoint: fire it with your key to confirm the
        credential is accepted and see which principal and scopes the platform
        resolved it to, before spending a real query. Echoes the auth context
        verbatim — no database. Identity is per-request, so responses carry
        `Cache-Control: no-store`. Requires the `mg:read` bearer scope."
      tags:
        - Meta
      security:
        - bearerAuth: []
      responses:
        "200":
          description: The resolved principal and scopes.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhoamiResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        API key or JWT presented as `Authorization: Bearer <token>` (an API key may alternatively be sent via `X-API-Key`). Every `/api/v1` operation requires the `mg:read` scope; a missing/invalid credential returns a 401 problem, insufficient scope returns a 403 problem. The `mg:compose` scope grants designer working data (source-material uploads, projects, and compose runs) without catalog-truth ingest powers; it is carried by `compose` keys and included in the broader `ingest`/`admin` tiers.
  parameters:
    TrpcProcedure:
      name: procedure
      in: path
      required: true
      description: Dot-separated tRPC procedure path, for example `jobs.list`.
      schema:
        type: string
    VariantIdPath:
      name: variantId
      in: path
      required: true
      description: The variant id to resolve.
      schema:
        type: string
  responses:
    Problem:
      description: RFC 7807 problem response.
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
    Unauthorized:
      description: Missing or invalid bearer credential.
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
    Forbidden:
      description: The credential lacks the required `mg:read` scope.
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
    RateLimited:
      description: Rate limit exceeded. Retriable — honour `Retry-After` /
        `X-RateLimit-Reset`.
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
  schemas:
    StartCrawlRequest:
      type: object
      required:
        - brandId
        - seedUrl
      properties:
        brandId:
          type: string
        seedUrl:
          type: string
          format: uri
        maxUrls:
          type: integer
          default: 50000
        maxDepth:
          type: integer
          default: 8
        httpConcurrency:
          type: integer
          default: 8
        renderConcurrency:
          type: integer
          default: 2
        includeSubdomains:
          type: boolean
          default: false
        ignoreRobots:
          type: boolean
          default: false
    SimpleError:
      type: object
      required:
        - error
      description: |
        The non-7807 `{ error }` shape used by routes migrated verbatim from their legacy predecessors (moved, not rewritten). `issues` rides alongside `error` on routes that additionally forward Zod validation issues.
      properties:
        error:
          type: string
        issues:
          type: array
          items:
            type: object
            additionalProperties: true
    Problem:
      type: object
      description: RFC 7807 Problem Detail, as emitted by every `/api/v1` route and
        `withAuth`.
      required:
        - type
        - title
        - status
        - detail
        - is_retriable
        - trace_id
      properties:
        type:
          type: string
          format: uri
          description: Stable URI identifying this error type.
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string
          description: URI identifying this specific occurrence (usually the request path).
        is_retriable:
          type: boolean
          description: Whether the caller should retry the request.
        suggestions:
          type: array
          items:
            type: string
          description: Concrete recovery hints aimed at agent consumers.
        doc_uri:
          type: string
          description: Link to human-readable documentation about this error type.
        trace_id:
          type: string
          description: Request-scoped identifier for log correlation.
        issues:
          type: array
          description: |
            Zod validation issues, present as an extension member on 400 responses from body/query schema failures.
          items:
            type: object
            additionalProperties: true
    CreatePublicRunRequest:
      type: object
      properties:
        capabilityKind:
          type: string
          enum:
            - agent
            - tool
            - workflow
        capabilityId:
          type: string
          minLength: 1
        inputJson: {}
        idempotencyKey:
          type: string
          minLength: 1
          maxLength: 500
        clientReference:
          type: string
          minLength: 1
          maxLength: 500
      required:
        - capabilityKind
        - capabilityId
        - inputJson
      additionalProperties: false
    ResumePublicRunRequest:
      type: object
      properties:
        resumeData: {}
      required:
        - resumeData
    CreateProjectRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
      additionalProperties: false
    AppendProjectEventRequest:
      type: object
      properties:
        kind:
          type: string
          enum:
            - batch-attached
            - run-started
            - clarifying-answer
            - pairwise-preference
            - reference-chosen
            - scheme-adopted
        payload:
          type: object
          propertyNames:
            type: string
          additionalProperties: {}
          description: The event payload; validated against the schema for `kind`.
      required:
        - kind
        - payload
    VarySchemesRequest:
      type: object
      properties:
        runId:
          type: string
          minLength: 1
          description: The succeeded `compose-material-schemes` Run to vary from.
        schemeId:
          description: Which scheme in the Run to treat as option A. Defaults to the Run's
            FIRST scheme when omitted.
          type: string
          minLength: 1
        axis:
          type: string
          enum:
            - palette
            - sustainability
            - brandDiversity
            - priceTier
          description: The constraint axis to vary along. 'palette' shifts colour cues
            (each unlocked slot retrieves against the programme palette rotated
            by one position). 'sustainability' adds a certification floor filter
            on top of the slot's required criteria. 'brandDiversity'
            hard-excludes any brand used ≥2× in the source scheme and doubles
            the brand-repeat penalty. 'priceTier' is an HONEST brand-level
            prestige-tier proxy — there is NO per-SKU price data. It reads the
            source scheme's dominant `price_tier` band and boosts (never
            hard-gates) candidates in the OPPOSITE half (dominant luxury
            {$$$,$$$$} → prefer value {$,$$}, and vice-versa).
        locks:
          default: []
          description: Slots to keep pinned to their source pick (each lock's `variantId`
            must equal that slot's current pick). Locked slots are copied
            through verbatim; only unlocked slots are re-derived.
          type: array
          items:
            type: object
            properties:
              slotId:
                type: string
                minLength: 1
              variantId:
                type: string
                minLength: 1
            required:
              - slotId
              - variantId
        exclusions:
          default:
            variantIds: []
            brandSlugs: []
          description: Caller exclusions applied on top of the automatic per-slot
            source-pick exclusion (option B differs from option A wherever the
            pool allows).
          type: object
          properties:
            variantIds:
              default: []
              type: array
              items:
                type: string
            brandSlugs:
              default: []
              type: array
              items:
                type: string
      required:
        - runId
        - axis
    SearchMaterialsRequest:
      type: object
      properties:
        text:
          description: Free-text query, matched VISUALLY against material imagery
            ("striped wallpaper", "warm terrazzo"). A bare colour word ("sage")
            automatically runs as a colour-only search.
          type: string
          maxLength: 500
        textModes:
          description: How to interpret `text`. Default ["visual"] — a text→image VISUAL
            search. "lexical" (opt-in) runs a BM25 ATTRIBUTE-lens search over
            material names + canonical attribute terms (find by what it's called
            / what it IS); detected colour words are stripped from the lexical
            terms and reranked instead. Requesting BOTH ["visual","lexical"]
            runs the visual search and notes it — compose a separate lexical
            search to blend them.
          type: array
          items:
            type: string
            enum:
              - visual
              - lexical
        colours:
          description: "Explicit colour palette to match. Each entry is EITHER a `hex` OR
            a `name` (a descriptive or brand paint name resolved server-side —
            never invent a hex for a name). PRECEDENCE: when set, these override
            any colour word detected in `text`. Unresolvable names fail with a
            typed error listing them, so you can correct and retry."
          maxItems: 8
          type: array
          items:
            type: object
            properties:
              hex:
                description: The colour to match, as 3- or 6-digit hex. Provide EITHER `hex` OR
                  `name`, never both. Only pass `hex` when you already hold an
                  EXACT hex — for a colour NAME use `name` and let the server
                  resolve it.
                type: string
                pattern: ^#?(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$
              name:
                description: A colour NAME to match — a descriptive name ('sage green',
                  'terracotta', 'warm sand') or a brand paint name ('Gauntlet
                  Gray', 'Hale Navy', 'White Dove'). Resolved server-side to its
                  exact library hex (misspellings are fuzzy-matched). NEVER
                  invent a hex for a name — pass the name here. Provide EITHER
                  `name` OR `hex`, never both.
                type: string
                minLength: 1
                maxLength: 120
              weight:
                description: Relative proportion of this colour in the desired palette (0–1).
                  Weights are normalised across the palette; omit for equal
                  parts.
                type: number
                exclusiveMinimum: 0
                maximum: 1
              role:
                type: string
                enum:
                  - any
                  - is
                  - accent
                description: "Per-colour swatch eligibility (default 'any'). PRECEDENCE:
                  `colourLogic` governs cross-colour combination (all/any);
                  `role` governs which swatch of a product may satisfy THIS
                  colour, and ANDs on top of the logic. 'is' = the colour must
                  be the product's DOMINANT swatch; 'accent' = the colour must
                  be a minor swatch (proportion 0.05–0.35) and is excluded from
                  the proportion-match term; 'any' = today's nearest-swatch
                  match."
              sourceName:
                description: Human name the colour came from (e.g. "sage", "Elephant's Breath")
                  — echoed back for display; never affects matching. (For a name
                  you want RESOLVED to a hex, use `name` instead.)
                type: string
                maxLength: 120
        colourLogic:
          description: '"all" (default): results should contain the whole palette, ranked
            by average distance + proportion match. "any": results matching ANY
            palette colour, ranked by their best single-colour distance.'
          type: string
          enum:
            - all
            - any
        colourTolerance:
          description: How close a colour must be to count as a match. 'normal' (default)
            is today's behaviour; 'tight' narrows the OKLab match threshold and
            prunes far single-colour matches; 'loose' widens the threshold.
          type: string
          enum:
            - tight
            - normal
            - loose
        colourAppearance:
          type: object
          properties:
            lightness:
              description: OKLab lightness window (L ∈ [0,1]); dark ≈ 0, light ≈ 1.
              type: object
              properties:
                min:
                  type: number
                  minimum: 0
                  maximum: 1
                max:
                  type: number
                  minimum: 0
                  maximum: 1
              required:
                - min
                - max
            warmth:
              description: OKLab warmth window (b axis); cool < 0 < warm, ≈ −0.1..0.1.
              type: object
              properties:
                min:
                  type: number
                  minimum: -0.5
                  maximum: 0.5
                max:
                  type: number
                  minimum: -0.5
                  maximum: 0.5
              required:
                - min
                - max
          description: HARD appearance gate on the material's DOMINANT colour, independent
            of the colour palette — works on a text-only query too. Variants
            without colour extraction are EXCLUDED whenever any axis is set
            (NULL scalars never match).
        mood:
          type: string
          enum:
            - warm
            - cool
            - earthy
            - moody
            - bold
            - light-airy
            - natural
            - neutral
            - romantic
            - coastal
            - industrial
            - luxe
            - vintage
            - serene
            - scandi
            - cosy
            - heritage
            - mid-century
            - pastel
          description: "Mood-language colour entry — resolves to a curated checked-in
            colour palette. Use it for evocative mood queries ('coastal',
            'moody', 'earthy') rather than a single colour. A mood ALONE runs as
            a colour search; a mood WITH text reranks the text results by the
            mood palette (a gentle tint). One of: warm (Warm — Reds, oranges,
            yellows - inviting and cozy); cool (Cool — Blues, teals, cool grays
            - calm and professional); earthy (Earthy — Browns, tans, terracotta
            - natural and grounded); moody (Moody — Deep, saturated, dramatic -
            rich and atmospheric); bold (Bold — High chroma, vibrant - energetic
            and eye-catching); light-airy (Light + Airy — Pastels, pale tones -
            soft and spacious); natural (Natural — Greens, wood tones, stone -
            organic and balanced); neutral (Neutral — Whites, grays, blacks -
            versatile and timeless); romantic (Romantic — Soft pinks, mauves,
            blush - tender and elegant); coastal (Coastal — Sandy beiges, ocean
            blues, seafoam - relaxed seaside); industrial (Industrial — Cool
            grays, concrete, metallic - urban and modern); luxe (Luxe — Deep
            jewel tones, rich emeralds, sapphires - opulent); vintage (Vintage —
            Muted, dusty tones - nostalgic and timeworn); serene (Serene — Soft
            blues, sage, muted lavender - calm and peaceful); scandi (Scandi —
            Warm whites, pale woods, cream - Scandinavian minimalism); cosy
            (Cosy — Deep plaster, amber, tobacco - enveloping warmth); heritage
            (Heritage — Rich historical tones with saturated depth - classic and
            grounded); mid-century (Mid-Century — Olive, mustard, teal,
            terracotta - retro modern); pastel (Pastel — Sage, duck egg, blush -
            soft colors with body)."
        sort:
          type: string
          enum:
            - relevance
            - light
            - dark
            - vivid
            - muted
            - colourful
          description: Re-order the final scored results by a colour-appearance axis
            (default 'relevance' = the fused score). 'light'/'dark' by dominant
            lightness, 'vivid'/'muted' by chroma, 'colourful' by overall
            vibrancy. With search signals present this REORDERS the matches; for
            pure colour browsing use the dedicated browse surface instead.
            Variants without colour extraction sink to the end.
        similarTo:
          description: "Find materials that LOOK like a seed image. Supply `variantId` to
            seed from an existing variant's primary image, or `vector` — a
            pre-computed halfvec literal (`[a,b,…]`) from an externally-embedded
            image — to drive the image arm directly. PRECEDENCE: wins over
            `text` (the response carries a note when both were given)."
          anyOf:
            - type: object
              properties:
                variantId:
                  type: string
              required:
                - variantId
            - type: object
              properties:
                vector:
                  type: string
              required:
                - vector
        image:
          description: "A raw incoming image to search by (URL or base64). The HTTP
            adapter embeds it and sets `similarTo: { vector }`; wins over
            `text`."
          oneOf:
            - type: object
              properties:
                type:
                  type: string
                  const: url
                url:
                  type: string
                  format: uri
                  description: HTTP(S) URL of the query image. MUST be an https URL.
              required:
                - type
                - url
            - type: object
              properties:
                type:
                  type: string
                  const: base64
                data:
                  type: string
                  minLength: 1
                  description: Base64-encoded image bytes (or a data URI) the caller already holds
                    — e.g. a form upload — so there is nothing to host.
              required:
                - type
                - data
          type: object
        category:
          type: object
          properties:
            boost:
              description: Soft category preference — results of this product type score
                higher but others still appear.
              type: string
            filter:
              description: "HARD gate: only these public taxonomy categories are returned.
                Values are category-group or product-type display names
                (case-insensitive), or canonical taxonomy keys. A group expands
                to every child product type; an unknown value errors listing
                every valid name."
              type: array
              items:
                type: string
        weights:
          type: object
          properties:
            image:
              type: number
              minimum: 0
            category:
              type: number
              minimum: 0
            colour:
              type: number
              minimum: 0
            lexical:
              type: number
              minimum: 0
          description: "FREE multipliers over the per-signal scores (each signal ∈ [0,1]);
            no sum-to-1 requirement. Omitted keys use the mode defaults (text:
            image 1.0 / category 0.5, plus a colour RERANK weight of 0.4 for a
            strong/explicit colour or 0.25 for a weak/derived one; colour mode:
            colour 1.0). Max achievable score = sum of active weights."
        filters:
          description: "Typed attribute filters applied as a hard restriction before
            ranking. Each entry is either a term match (`attributeId` + `term`)
            or a presence check (`attributeId` + `hasValue: true` — any accepted
            value). Presence filters always route as hard spec gates.
            Aesthetic/meaning term filters (pattern, texture, directionality,
            and the other visual-meaning attributes) are applied as a
            pool-bounded post-filter over the visual candidate pool rather than
            a whole-catalog restriction."
          type: array
          items:
            anyOf:
              - type: object
                properties:
                  attributeId:
                    type: string
                    description: Canonical attribute id, e.g. "primary_material" or "fire_rating".
                  term:
                    type: string
                    description: Canonical term the attribute must equal, e.g. "textile".
                required:
                  - attributeId
                  - term
                additionalProperties: false
              - type: object
                properties:
                  attributeId:
                    type: string
                    description: Canonical attribute id that must have an accepted value, e.g.
                      "sample_image_mm_per_px".
                  hasValue:
                    type: boolean
                    const: true
                    description: Require any accepted value for this attribute.
                required:
                  - attributeId
                  - hasValue
                additionalProperties: false
        limit:
          description: Results per page (default 24, max 48).
          type: integer
          minimum: 1
          maximum: 48
        cursor:
          description: "Opaque pagination cursor from a previous response. NOTE: ANN
            search is approximate — tail items may reorder slightly between
            pages."
          type: string
        explain:
          description: Include per-signal scores on each result (`signals`).
          type: boolean
        collapse:
          description: Group results by product before pagination. "product" returns one
            representative (highest-ranked) variant per product, with hidden
            siblings counted in `siblingCount`. Default "none" returns every
            variant unchanged.
          type: string
          enum:
            - none
            - product
        count:
          default: false
          description: 'Opt-in feed total. When true the response carries
            `counts.variants` (and `counts.products` under
            `collapse:"product"`). The variant total is EXACT up to a cap of
            1000 and reported as `basis:"lowerBound"` beyond it — and ALWAYS
            `lowerBound` for ANN-ranked pools (image / colour / visual-text /
            visual-meaning post-filters), where the finite candidate pool is the
            only honest figure. Counts are never silently approximated: an exact
            count is labelled `exact`, a capped or pool-bounded one
            `lowerBound`. Default false (skip the count query entirely).'
          type: boolean
        hasImages:
          default: false
          description: When true, only return results that have at least one catalog image
            (a servable card thumbnail). Off by default so search stays honest
            about the FULL catalog, including image-less records. It filters the
            LEXICAL attribute lens — the one source that can surface an
            image-less record; the visual arms (image / colour / text→image) are
            inherently image-backed, so the flag is a no-op there. The opt-in
            `counts.variants` feed total honours it too (image-less records are
            excluded from the count). Default false.
          type: boolean
        view:
          default: full
          description: Result projection. "full" (default) returns the complete
            `searchResultItemSchema` (palette, colourMeta, hasImage, siblings,
            explain signals/meanings). "card" returns the lean
            `searchCardItemSchema` (identity + name + thumbnail + score) for
            grid rendering, skipping the per-page palette / appearance / meaning
            reads. The response echoes the chosen `view`, which discriminates
            the `results` union.
          type: string
          enum:
            - full
            - card
    MatchMaterialRequest:
      type: object
      properties:
        imageBase64:
          description: Raw base64 or data-URL image.
          type: string
          minLength: 1
        imageUrl:
          description: Public https image URL Voyage can fetch.
          type: string
          format: uri
        vector:
          description: Pre-computed halfvec literal — skips Voyage when already embedded.
          type: string
          pattern: ^\[[\d.eE+,\s-]+\]$
        limit:
          type: integer
          minimum: 1
          maximum: 100
        qualityFloor:
          description: Min signals.image in [0,1].
          type: number
          minimum: 0
          maximum: 1
        categoryTopK:
          type: integer
          minimum: 1
          maximum: 12
        categoryMinBoost:
          type: number
          minimum: 0
          maximum: 1
        detectionLabel:
          description: Detector label for the surface (e.g. 'white oak floor'); embedded
            as text and unioned into category scope, never an override.
          type: string
          minLength: 1
        materialFunction:
          description: Detector material function (flooring, countertop…) — enriches the
            label.
          type: string
          minLength: 1
        detectionCategory:
          description: Detector-assigned catalog category; an exact vocabulary match leads
            the scope.
          type: string
          minLength: 1
        requestedType:
          description: Scene hint from the visual console.
          type: string
          enum:
            - swatch
            - installation
            - flatplan
    ExtractPaletteRequest:
      type: object
      properties:
        image:
          type: string
          minLength: 1
          description: The query image as a data URI or bare base64 string.
        max:
          description: Max palette swatches to return (default 6).
          type: integer
          minimum: 1
          maximum: 12
      required:
        - image
    FindSubstitutesRequest:
      type: object
      properties:
        excludeBrands:
          maxItems: 50
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 128
            pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        diversityBoost:
          default: 0
          type: number
          minimum: 0
          maximum: 1
        limit:
          default: 24
          type: integer
          minimum: 1
          maximum: 48
        cursor:
          type: string
          minLength: 1
          maxLength: 1024
      additionalProperties: false
    MaterialsBatchRequest:
      type: object
      properties:
        ids:
          maxItems: 100
          type: array
          items:
            type: string
          description: The ids to hydrate, in the `idKind` namespace.
        view:
          default: tile
          description: "'tile' (grid card, default) or 'detail' (full record)."
          type: string
          enum:
            - tile
            - detail
        idKind:
          default: variantId
          description: Whether `ids` are variant ids or Material Bank ids.
          type: string
          enum:
            - variantId
            - materialBankId
      required:
        - ids
    CoordinateMaterialsRequest:
      type: object
      properties:
        materials:
          type: array
          items:
            type: string
            minLength: 1
        variantIds:
          type: array
          items:
            type: string
            minLength: 1
        projectType:
          type: string
          minLength: 1
        limit:
          default: 20
          type: integer
          minimum: 1
          maximum: 100
    EnrichCandidateDemandRequest:
      type: object
      properties:
        variantIds:
          minItems: 1
          maxItems: 48
          type: array
          items:
            type: string
            minLength: 1
          description: 1–48 canonical variant ids. Order AND cardinality are preserved in
            `results` — duplicates are echoed, never collapsed.
        signal:
          default: sampled
          description: Which behavioural signal to read demand from — `sampled` order
            co-occurrence (default), `considered` board curation, or `specified`
            (stats only).
          type: string
          enum:
            - sampled
            - considered
            - specified
        context:
          description: "Optional declared context (e.g. `{ projectType: 'hospitality' }`).
            Echoed back for provenance; never used to filter, rank, or derive a
            denominator."
          type: object
          propertyNames:
            type: string
          additionalProperties:
            type: string
      required:
        - variantIds
    CodeAdvisoryRequest:
      type: object
      properties:
        variantIds:
          minItems: 1
          maxItems: 48
          type: array
          items:
            type: string
            minLength: 1
        jurisdictionId:
          type: string
          minLength: 1
        occupancyGroup:
          type: string
          minLength: 1
        spaceType:
          type: string
          minLength: 1
        sprinklered:
          type: boolean
      required:
        - variantIds
        - jurisdictionId
        - occupancyGroup
        - spaceType
        - sprinklered
    BrandDiscoveryRunInput:
      type: object
      properties:
        homepageUrl:
          type: string
        maxCandidates:
          type: number
          minimum: 10
          maximum: 5000
      required:
        - homepageUrl
    PublicHttpsUrlRequest:
      type: object
      properties:
        url:
          type: string
          format: uri
          description: "Public https URL. SSRF-guarded: https + a real public domain only."
      required:
        - url
      additionalProperties: false
    AgentChatRequest:
      type: object
      properties:
        messages:
          minItems: 1
          type: array
          items: {}
          description: Non-empty AI SDK UIMessage (or legacy) messages array.
      required:
        - messages
      additionalProperties: {}
    VerifyScopeRequest:
      type: object
      properties:
        variantId:
          type: string
          minLength: 1
        productId:
          type: string
          minLength: 1
        attributeIds:
          description: "Optional filter: verify only these attribute ids."
          type: array
          items:
            type: string
            minLength: 1
        ingestionSessionId:
          type: string
          minLength: 1
          maxLength: 30
        sourceUrlHints:
          description: Optional source URLs to prefer when gathering evidence.
          type: array
          items:
            type: string
      required:
        - ingestionSessionId
    SubjectChatRequest:
      type: object
      properties:
        agentId:
          type: string
        conversation:
          type: array
          items:
            type: object
            properties:
              role:
                type: string
              status:
                type: string
              text:
                type: string
        instruction:
          type: string
          minLength: 1
        maxSteps:
          type: number
        subject:
          type: object
          properties:
            attributeId:
              type: string
            id:
              type: string
            label:
              type: string
              minLength: 1
            scope:
              type: string
              minLength: 1
          required:
            - label
            - scope
          additionalProperties: {}
      required:
        - agentId
        - instruction
        - subject
    SubjectOverrideRequest:
      type: object
      properties:
        target:
          type: object
          properties:
            type:
              type: string
              minLength: 1
            scope:
              type: string
              enum:
                - brand
                - product
                - variant
            id:
              type: string
              minLength: 1
            attributeId:
              type: string
              minLength: 1
            label:
              type: string
              minLength: 1
          required:
            - type
            - scope
            - id
            - label
          additionalProperties: {}
        attributeId:
          type: string
          minLength: 1
        value:
          type: string
          minLength: 1
        rationale:
          type: string
      required:
        - target
        - attributeId
        - value
    CapabilityManifestResponse:
      type: object
      properties:
        capabilities:
          type: array
          items:
            type: object
            properties:
              publicId:
                type: string
                pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
              runtimeId:
                type: string
                minLength: 1
              kind:
                type: string
                enum:
                  - agent
                  - tool
                  - workflow
              exposure:
                type: string
                enum:
                  - public
                  - partner
                  - internal
              name:
                type: string
                minLength: 1
              description:
                type: string
                minLength: 1
              schemas:
                type: object
                properties:
                  input:
                    type: object
                    propertyNames:
                      type: string
                    additionalProperties: {}
                  output:
                    type: object
                    propertyNames:
                      type: string
                    additionalProperties: {}
                required:
                  - input
                  - output
                additionalProperties: false
              sideEffectClass:
                type: string
                enum:
                  - read_only
                  - browser_local
                  - external_read
                  - writes_proposed
                  - writes_persisted
                  - planned_only
              scopes:
                minItems: 1
                type: array
                items:
                  type: string
                  enum:
                    - mg:read
                    - mg:compose
                    - mg:write
                    - mg:ingest
                    - mg:admin
              timeoutMs:
                type: integer
                exclusiveMinimum: 0
                maximum: 300000
              retryPolicy:
                type: object
                properties:
                  maxAttempts:
                    type: integer
                    minimum: 1
                    maximum: 10
                  backoff:
                    type: string
                    enum:
                      - none
                      - fixed
                      - exponential
                required:
                  - maxAttempts
                  - backoff
                additionalProperties: false
              tags:
                type: array
                items:
                  type: string
                  minLength: 1
              links:
                type: object
                properties:
                  invoke:
                    type: object
                    properties:
                      href:
                        type: string
                        pattern: ^\/.*
                      method:
                        type: string
                        enum:
                          - GET
                          - POST
                    required:
                      - href
                      - method
                    additionalProperties: false
                required:
                  - invoke
                additionalProperties: false
            required:
              - publicId
              - runtimeId
              - kind
              - exposure
              - name
              - description
              - schemas
              - sideEffectClass
              - scopes
              - timeoutMs
              - retryPolicy
              - tags
              - links
            additionalProperties: false
      required:
        - capabilities
      additionalProperties: false
    CreatePublicRunResponse:
      type: object
      properties:
        accepted:
          type: boolean
        replayed:
          type: boolean
        workflowRunId:
          type: string
        run:
          $ref: "#/components/schemas/PublicRun"
      required:
        - accepted
        - replayed
        - workflowRunId
        - run
      additionalProperties: false
    PublicRun:
      type: object
      properties:
        id:
          type: string
        capabilityKind:
          type: string
          enum:
            - agent
            - tool
            - workflow
        capabilityId:
          type: string
        clientReference:
          anyOf:
            - type: string
            - type: "null"
        status:
          type: string
          enum:
            - queued
            - claimed
            - running
            - suspended
            - succeeded
            - failed
            - canceled
        redactedInputJson:
          type: object
          propertyNames:
            type: string
          additionalProperties: {}
        redactedOutputJson:
          anyOf:
            - type: object
              propertyNames:
                type: string
              additionalProperties: {}
            - type: "null"
        usageJson:
          anyOf:
            - type: object
              propertyNames:
                type: string
              additionalProperties: {}
            - type: "null"
        artifactRefs:
          anyOf:
            - type: array
              items:
                type: object
                properties:
                  kind:
                    type: string
                  ref:
                    type: string
                required:
                  - kind
                  - ref
                additionalProperties: false
            - type: "null"
        failureJson:
          anyOf:
            - $ref: "#/components/schemas/PublicRunFailure"
            - type: "null"
        suspensionStepId:
          anyOf:
            - type: string
            - type: "null"
        eventCursor:
          type: integer
          minimum: 0
          maximum: 9007199254740991
        attempts:
          type: integer
          minimum: 0
          maximum: 9007199254740991
        startedAt:
          anyOf:
            - type: string
              format: date-time
              pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
            - type: "null"
        cancellationRequestedAt:
          anyOf:
            - type: string
              format: date-time
              pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
            - type: "null"
        completedAt:
          anyOf:
            - type: string
              format: date-time
              pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
            - type: "null"
        createdAt:
          type: string
          format: date-time
          pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
        updatedAt:
          type: string
          format: date-time
          pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
      required:
        - id
        - capabilityKind
        - capabilityId
        - clientReference
        - status
        - redactedInputJson
        - redactedOutputJson
        - usageJson
        - artifactRefs
        - failureJson
        - suspensionStepId
        - eventCursor
        - attempts
        - startedAt
        - cancellationRequestedAt
        - completedAt
        - createdAt
        - updatedAt
      additionalProperties: false
    PublicRunFailure:
      type: object
      properties:
        code:
          type: string
        message:
          type: string
        retryable:
          type: boolean
        detail:
          type: object
          propertyNames:
            type: string
          additionalProperties: {}
      required:
        - code
        - message
        - retryable
      additionalProperties: false
    PublicRunResponse:
      type: object
      properties:
        run:
          $ref: "#/components/schemas/PublicRun"
      required:
        - run
      additionalProperties: false
    SourceMaterialBatchReceipt:
      type: object
      properties:
        batchId:
          type: string
          description: The stored batch id (`smb_…`).
        sources:
          type: array
          items:
            type: object
            properties:
              sourceId:
                type: string
                description: The stored file id (`smf_…`).
              role:
                type: string
                enum:
                  - brief
                  - reference_image
                  - document
                  - document_image
                  - inspiration_image
                  - unclassified
                description: The pipeline-assigned role. `unclassified` is a legitimate terminal
                  value when the file could not be placed; clients should
                  tolerate new values.
              filename:
                type: string
              mediaType:
                type: string
              sha256:
                type: string
                description: Lowercase hex SHA-256 of the stored bytes.
              bytes:
                type: integer
                minimum: -9007199254740991
                maximum: 9007199254740991
              pages:
                anyOf:
                  - type: integer
                    minimum: -9007199254740991
                    maximum: 9007199254740991
                  - type: "null"
                description: PDF page count; null for images.
            required:
              - sourceId
              - role
              - filename
              - mediaType
              - sha256
              - bytes
              - pages
            additionalProperties: false
      required:
        - batchId
        - sources
      additionalProperties: false
    ProjectState:
      type: object
      properties:
        projectId:
          type: string
        principalId:
          type: string
        name:
          type: string
        createdAt:
          type: string
          format: date-time
          pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
        updatedAt:
          type: string
          format: date-time
          pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
        batchIds:
          type: array
          items:
            type: string
          description: Attached source-material batch ids, first-seen order.
        runIds:
          type: array
          items:
            type: string
          description: Started capability Run ids, first-seen order.
        answers:
          type: array
          items:
            type: object
            properties:
              question:
                type: string
              answer:
                type: string
            required:
              - question
              - answer
            additionalProperties: false
        preferences:
          type: array
          items:
            type: object
            properties:
              leftId:
                type: string
              rightId:
                type: string
              chosenId:
                type: string
                description: Equals `leftId` or `rightId`.
            required:
              - leftId
              - rightId
              - chosenId
            additionalProperties: false
        chosenReferences:
          type: array
          items:
            type: string
        adoptedSchemes:
          type: array
          items:
            type: string
        events:
          type: array
          items:
            $ref: "#/components/schemas/ProjectEvent"
          description: The full ordered event trail — the receipts.
      required:
        - projectId
        - principalId
        - name
        - createdAt
        - updatedAt
        - batchIds
        - runIds
        - answers
        - preferences
        - chosenReferences
        - adoptedSchemes
        - events
      additionalProperties: false
    ProjectEvent:
      type: object
      properties:
        eventId:
          type: string
          description: The stored event id (`pje_…`).
        kind:
          type: string
          enum:
            - batch-attached
            - run-started
            - clarifying-answer
            - pairwise-preference
            - reference-chosen
            - scheme-adopted
        payload:
          type: object
          propertyNames:
            type: string
          additionalProperties: {}
          description: Validated to the shape of `kind`.
        createdAt:
          type: string
          format: date-time
          pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
      required:
        - eventId
        - kind
        - payload
        - createdAt
      additionalProperties: false
    ProjectListResponse:
      type: object
      properties:
        projects:
          type: array
          items:
            $ref: "#/components/schemas/ProjectSummary"
      required:
        - projects
      additionalProperties: false
    ProjectSummary:
      type: object
      properties:
        projectId:
          type: string
          description: The stored project id (`proj_…`).
        principalId:
          type: string
        name:
          type: string
        createdAt:
          type: string
          format: date-time
          pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
        updatedAt:
          type: string
          format: date-time
          pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
          description: Last-activity timestamp; bumped on each appended event.
      required:
        - projectId
        - principalId
        - name
        - createdAt
        - updatedAt
      additionalProperties: false
    ProjectEventReceipt:
      type: object
      properties:
        eventId:
          type: string
        projectId:
          type: string
        kind:
          type: string
          enum:
            - batch-attached
            - run-started
            - clarifying-answer
            - pairwise-preference
            - reference-chosen
            - scheme-adopted
        createdAt:
          type: string
          format: date-time
          pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
      required:
        - eventId
        - projectId
        - kind
        - createdAt
      additionalProperties: false
    VarySchemesResponse:
      type: object
      properties:
        version:
          type: string
          const: 2026-07-18.demo.1
        scheme:
          type: object
          properties:
            schemeId:
              type: string
              minLength: 1
            title:
              type: string
              minLength: 1
            picks:
              minItems: 1
              type: array
              items:
                type: object
                properties:
                  slotId:
                    type: string
                    minLength: 1
                  slotName:
                    type: string
                    minLength: 1
                  spaceId:
                    anyOf:
                      - type: string
                        minLength: 1
                      - type: "null"
                  element:
                    anyOf:
                      - type: string
                        enum:
                          - flooring
                          - wall
                          - ceiling
                          - textile
                          - surface
                          - furniture
                          - lighting
                          - other
                      - type: "null"
                  material:
                    type: object
                    properties:
                      variantId:
                        type: string
                        minLength: 1
                      brand:
                        type: string
                        minLength: 1
                      product:
                        type: string
                        minLength: 1
                      variant:
                        type: string
                        minLength: 1
                      imageUrl:
                        anyOf:
                          - type: string
                          - type: "null"
                      hex:
                        anyOf:
                          - type: string
                            pattern: ^#[0-9a-f]{6}$
                          - type: "null"
                    required:
                      - variantId
                      - brand
                      - product
                      - variant
                      - imageUrl
                      - hex
                    additionalProperties: false
                  reason:
                    type: object
                    properties:
                      score:
                        type: number
                      matched:
                        type: array
                        items:
                          type: object
                          properties:
                            attributeId:
                              type: string
                              minLength: 1
                            state:
                              type: string
                              enum:
                                - matched
                                - mismatched
                                - unknown
                            confidence:
                              anyOf:
                                - type: number
                                - type: "null"
                            sourceBasis:
                              anyOf:
                                - type: string
                                - type: "null"
                          required:
                            - attributeId
                            - state
                            - confidence
                            - sourceBasis
                          additionalProperties: false
                      notes:
                        type: array
                        items:
                          type: string
                    required:
                      - score
                      - matched
                      - notes
                    additionalProperties: false
                required:
                  - slotId
                  - slotName
                  - material
                  - reason
                additionalProperties: false
            objective:
              type: object
              properties:
                total:
                  type: number
                briefFit:
                  type: number
                coverage:
                  type: number
                coherence:
                  type: number
                novelty:
                  type: number
              required:
                - total
                - briefFit
                - coverage
                - coherence
                - novelty
              additionalProperties: false
          required:
            - schemeId
            - title
            - picks
            - objective
          additionalProperties: false
        provenance:
          type: object
          properties:
            axis:
              type: string
              enum:
                - palette
                - sustainability
                - brandDiversity
                - priceTier
              description: The constraint axis to vary along. 'palette' shifts colour cues
                (each unlocked slot retrieves against the programme palette
                rotated by one position). 'sustainability' adds a certification
                floor filter on top of the slot's required criteria.
                'brandDiversity' hard-excludes any brand used ≥2× in the source
                scheme and doubles the brand-repeat penalty. 'priceTier' is an
                HONEST brand-level prestige-tier proxy — there is NO per-SKU
                price data. It reads the source scheme's dominant `price_tier`
                band and boosts (never hard-gates) candidates in the OPPOSITE
                half (dominant luxury {$$$,$$$$} → prefer value {$,$$}, and
                vice-versa).
            sourceRunId:
              type: string
              minLength: 1
            sourceSchemeId:
              type: string
              minLength: 1
            lockedSlotIds:
              type: array
              items:
                type: string
            notes:
              type: array
              items:
                type: string
          required:
            - axis
            - sourceRunId
            - sourceSchemeId
            - lockedSlotIds
            - notes
          additionalProperties: false
        shortfall:
          anyOf:
            - type: object
              properties:
                requested:
                  type: integer
                  minimum: 1
                  maximum: 9007199254740991
                produced:
                  type: integer
                  minimum: 0
                  maximum: 9007199254740991
                reasons:
                  minItems: 1
                  type: array
                  items:
                    type: string
                    minLength: 1
              required:
                - requested
                - produced
                - reasons
              additionalProperties: false
            - type: "null"
      required:
        - version
        - scheme
        - provenance
        - shortfall
      additionalProperties: false
    SearchResponse:
      oneOf:
        - type: object
          properties:
            view:
              default: full
              description: Projection echo — always 'full' on this envelope variant.
              type: string
              const: full
            results:
              type: array
              items:
                type: object
                properties:
                  variantId:
                    type: string
                  brand:
                    type: string
                  product:
                    type: string
                  brandSlug:
                    anyOf:
                      - type: string
                      - type: "null"
                  productId:
                    anyOf:
                      - type: string
                      - type: "null"
                  color:
                    anyOf:
                      - type: string
                      - type: "null"
                  variantName:
                    anyOf:
                      - type: string
                      - type: "null"
                  imageUrl:
                    anyOf:
                      - type: string
                      - type: "null"
                  hasImage:
                    type: boolean
                  palette:
                    type: array
                    items:
                      type: object
                      properties:
                        hex:
                          type: string
                        proportion:
                          type: number
                        name:
                          anyOf:
                            - type: string
                            - type: "null"
                      required:
                        - hex
                        - proportion
                      additionalProperties: false
                  colourMeta:
                    type: object
                    properties:
                      mood:
                        type: string
                      dominantFamily:
                        type: string
                      dominantHex:
                        type: string
                    additionalProperties: false
                  score:
                    type: number
                  signals:
                    type: object
                    properties:
                      image:
                        anyOf:
                          - type: number
                          - type: "null"
                      category:
                        anyOf:
                          - type: number
                          - type: "null"
                      colour:
                        anyOf:
                          - type: number
                          - type: "null"
                      lexical:
                        anyOf:
                          - type: number
                          - type: "null"
                    required:
                      - image
                      - category
                      - colour
                      - lexical
                    additionalProperties: false
                  retired:
                    description: MB lists the SKU as not visible/retired; demoted in ranking.
                      Omitted when false.
                    type: boolean
                  siblingCount:
                    description: 'Additional variants of the same product hidden by `collapse:
                      "product"`. Present only when grouping hid at least one
                      sibling.'
                    type: integer
                    minimum: 1
                    maximum: 9007199254740991
                  visualMeanings:
                    description: Existing accepted/measured visual meanings — descriptive evidence,
                      not ranking causes. Present only when `explain` was set
                      and at least one meaning exists.
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          description: Registry attribute id, or a measured-meaning id like `colour_mood`.
                        label:
                          type: string
                          description: Human label — registry presentation label for canonical meanings.
                        values:
                          minItems: 1
                          type: array
                          items:
                            type: string
                          description: Human-readable value labels.
                        basis:
                          type: string
                          enum:
                            - canonical_attribute
                            - image_colour_analysis
                          description: "Where the meaning comes from: an accepted canonical attribute
                            value, or measured image colour analysis. NEVER
                            derived from vector similarity or scores."
                        confidence:
                          type: number
                        sourceBasis:
                          type: string
                        version:
                          description: Extraction version for measured meanings.
                          type: string
                        appliedAt:
                          description: ISO timestamp the accepted value was applied.
                          type: string
                      required:
                        - id
                        - label
                        - values
                        - basis
                      additionalProperties: false
                required:
                  - variantId
                  - brand
                  - product
                  - brandSlug
                  - productId
                  - color
                  - variantName
                  - imageUrl
                  - hasImage
                  - palette
                  - score
                additionalProperties: false
            suggestions:
              default: []
              description: Bounded structured refinements. Each request is a complete
                executable replacement for the current search request.
              maxItems: 6
              type: array
              items:
                type: object
                properties:
                  label:
                    type: string
                    minLength: 1
                    description: Human-readable description of the refinement.
                  request:
                    type: object
                    properties:
                      text:
                        description: Free-text query, matched VISUALLY against material imagery
                          ("striped wallpaper", "warm terrazzo"). A bare colour
                          word ("sage") automatically runs as a colour-only
                          search.
                        type: string
                        maxLength: 500
                      textModes:
                        description: How to interpret `text`. Default ["visual"] — a text→image VISUAL
                          search. "lexical" (opt-in) runs a BM25 ATTRIBUTE-lens
                          search over material names + canonical attribute terms
                          (find by what it's called / what it IS); detected
                          colour words are stripped from the lexical terms and
                          reranked instead. Requesting BOTH ["visual","lexical"]
                          runs the visual search and notes it — compose a
                          separate lexical search to blend them.
                        type: array
                        items:
                          type: string
                          enum:
                            - visual
                            - lexical
                      colours:
                        description: "Explicit colour palette to match. Each entry is EITHER a `hex` OR
                          a `name` (a descriptive or brand paint name resolved
                          server-side — never invent a hex for a name).
                          PRECEDENCE: when set, these override any colour word
                          detected in `text`. Unresolvable names fail with a
                          typed error listing them, so you can correct and
                          retry."
                        maxItems: 8
                        type: array
                        items:
                          type: object
                          properties:
                            hex:
                              description: The colour to match, as 3- or 6-digit hex. Provide EITHER `hex` OR
                                `name`, never both. Only pass `hex` when you
                                already hold an EXACT hex — for a colour NAME
                                use `name` and let the server resolve it.
                              type: string
                              pattern: ^#?(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$
                            name:
                              description: A colour NAME to match — a descriptive name ('sage green',
                                'terracotta', 'warm sand') or a brand paint name
                                ('Gauntlet Gray', 'Hale Navy', 'White Dove').
                                Resolved server-side to its exact library hex
                                (misspellings are fuzzy-matched). NEVER invent a
                                hex for a name — pass the name here. Provide
                                EITHER `name` OR `hex`, never both.
                              type: string
                              minLength: 1
                              maxLength: 120
                            weight:
                              description: Relative proportion of this colour in the desired palette (0–1).
                                Weights are normalised across the palette; omit
                                for equal parts.
                              type: number
                              exclusiveMinimum: 0
                              maximum: 1
                            role:
                              type: string
                              enum:
                                - any
                                - is
                                - accent
                              description: "Per-colour swatch eligibility (default 'any'). PRECEDENCE:
                                `colourLogic` governs cross-colour combination
                                (all/any); `role` governs which swatch of a
                                product may satisfy THIS colour, and ANDs on top
                                of the logic. 'is' = the colour must be the
                                product's DOMINANT swatch; 'accent' = the colour
                                must be a minor swatch (proportion 0.05–0.35)
                                and is excluded from the proportion-match term;
                                'any' = today's nearest-swatch match."
                            sourceName:
                              description: Human name the colour came from (e.g. "sage", "Elephant's Breath")
                                — echoed back for display; never affects
                                matching. (For a name you want RESOLVED to a
                                hex, use `name` instead.)
                              type: string
                              maxLength: 120
                          additionalProperties: false
                      colourLogic:
                        description: '"all" (default): results should contain the whole palette, ranked
                          by average distance + proportion match. "any": results
                          matching ANY palette colour, ranked by their best
                          single-colour distance.'
                        type: string
                        enum:
                          - all
                          - any
                      colourTolerance:
                        description: How close a colour must be to count as a match. 'normal' (default)
                          is today's behaviour; 'tight' narrows the OKLab match
                          threshold and prunes far single-colour matches;
                          'loose' widens the threshold.
                        type: string
                        enum:
                          - tight
                          - normal
                          - loose
                      colourAppearance:
                        type: object
                        properties:
                          lightness:
                            description: OKLab lightness window (L ∈ [0,1]); dark ≈ 0, light ≈ 1.
                            type: object
                            properties:
                              min:
                                type: number
                                minimum: 0
                                maximum: 1
                              max:
                                type: number
                                minimum: 0
                                maximum: 1
                            required:
                              - min
                              - max
                            additionalProperties: false
                          warmth:
                            description: OKLab warmth window (b axis); cool < 0 < warm, ≈ −0.1..0.1.
                            type: object
                            properties:
                              min:
                                type: number
                                minimum: -0.5
                                maximum: 0.5
                              max:
                                type: number
                                minimum: -0.5
                                maximum: 0.5
                            required:
                              - min
                              - max
                            additionalProperties: false
                        additionalProperties: false
                        description: HARD appearance gate on the material's DOMINANT colour, independent
                          of the colour palette — works on a text-only query
                          too. Variants without colour extraction are EXCLUDED
                          whenever any axis is set (NULL scalars never match).
                      mood:
                        type: string
                        enum:
                          - warm
                          - cool
                          - earthy
                          - moody
                          - bold
                          - light-airy
                          - natural
                          - neutral
                          - romantic
                          - coastal
                          - industrial
                          - luxe
                          - vintage
                          - serene
                          - scandi
                          - cosy
                          - heritage
                          - mid-century
                          - pastel
                        description: "Mood-language colour entry — resolves to a curated checked-in
                          colour palette. Use it for evocative mood queries
                          ('coastal', 'moody', 'earthy') rather than a single
                          colour. A mood ALONE runs as a colour search; a mood
                          WITH text reranks the text results by the mood palette
                          (a gentle tint). One of: warm (Warm — Reds, oranges,
                          yellows - inviting and cozy); cool (Cool — Blues,
                          teals, cool grays - calm and professional); earthy
                          (Earthy — Browns, tans, terracotta - natural and
                          grounded); moody (Moody — Deep, saturated, dramatic -
                          rich and atmospheric); bold (Bold — High chroma,
                          vibrant - energetic and eye-catching); light-airy
                          (Light + Airy — Pastels, pale tones - soft and
                          spacious); natural (Natural — Greens, wood tones,
                          stone - organic and balanced); neutral (Neutral —
                          Whites, grays, blacks - versatile and timeless);
                          romantic (Romantic — Soft pinks, mauves, blush -
                          tender and elegant); coastal (Coastal — Sandy beiges,
                          ocean blues, seafoam - relaxed seaside); industrial
                          (Industrial — Cool grays, concrete, metallic - urban
                          and modern); luxe (Luxe — Deep jewel tones, rich
                          emeralds, sapphires - opulent); vintage (Vintage —
                          Muted, dusty tones - nostalgic and timeworn); serene
                          (Serene — Soft blues, sage, muted lavender - calm and
                          peaceful); scandi (Scandi — Warm whites, pale woods,
                          cream - Scandinavian minimalism); cosy (Cosy — Deep
                          plaster, amber, tobacco - enveloping warmth); heritage
                          (Heritage — Rich historical tones with saturated depth
                          - classic and grounded); mid-century (Mid-Century —
                          Olive, mustard, teal, terracotta - retro modern);
                          pastel (Pastel — Sage, duck egg, blush - soft colors
                          with body)."
                      sort:
                        type: string
                        enum:
                          - relevance
                          - light
                          - dark
                          - vivid
                          - muted
                          - colourful
                        description: Re-order the final scored results by a colour-appearance axis
                          (default 'relevance' = the fused score).
                          'light'/'dark' by dominant lightness, 'vivid'/'muted'
                          by chroma, 'colourful' by overall vibrancy. With
                          search signals present this REORDERS the matches; for
                          pure colour browsing use the dedicated browse surface
                          instead. Variants without colour extraction sink to
                          the end.
                      similarTo:
                        description: "Find materials that LOOK like a seed image. Supply `variantId` to
                          seed from an existing variant's primary image, or
                          `vector` — a pre-computed halfvec literal (`[a,b,…]`)
                          from an externally-embedded image — to drive the image
                          arm directly. PRECEDENCE: wins over `text` (the
                          response carries a note when both were given)."
                        anyOf:
                          - type: object
                            properties:
                              variantId:
                                type: string
                            required:
                              - variantId
                            additionalProperties: false
                          - type: object
                            properties:
                              vector:
                                type: string
                            required:
                              - vector
                            additionalProperties: false
                      image:
                        description: "A raw incoming image to search by (URL or base64). The HTTP
                          adapter embeds it and sets `similarTo: { vector }`;
                          wins over `text`."
                        oneOf:
                          - type: object
                            properties:
                              type:
                                type: string
                                const: url
                              url:
                                type: string
                                format: uri
                                description: HTTP(S) URL of the query image. MUST be an https URL.
                            required:
                              - type
                              - url
                            additionalProperties: false
                          - type: object
                            properties:
                              type:
                                type: string
                                const: base64
                              data:
                                type: string
                                minLength: 1
                                description: Base64-encoded image bytes (or a data URI) the caller already holds
                                  — e.g. a form upload — so there is nothing to
                                  host.
                            required:
                              - type
                              - data
                            additionalProperties: false
                        type: object
                      category:
                        type: object
                        properties:
                          boost:
                            description: Soft category preference — results of this product type score
                              higher but others still appear.
                            type: string
                          filter:
                            description: "HARD gate: only these public taxonomy categories are returned.
                              Values are category-group or product-type display
                              names (case-insensitive), or canonical taxonomy
                              keys. A group expands to every child product type;
                              an unknown value errors listing every valid name."
                            type: array
                            items:
                              type: string
                        additionalProperties: false
                      weights:
                        type: object
                        properties:
                          image:
                            type: number
                            minimum: 0
                          category:
                            type: number
                            minimum: 0
                          colour:
                            type: number
                            minimum: 0
                          lexical:
                            type: number
                            minimum: 0
                        additionalProperties: false
                        description: "FREE multipliers over the per-signal scores (each signal ∈ [0,1]);
                          no sum-to-1 requirement. Omitted keys use the mode
                          defaults (text: image 1.0 / category 0.5, plus a
                          colour RERANK weight of 0.4 for a strong/explicit
                          colour or 0.25 for a weak/derived one; colour mode:
                          colour 1.0). Max achievable score = sum of active
                          weights."
                      filters:
                        description: "Typed attribute filters applied as a hard restriction before
                          ranking. Each entry is either a term match
                          (`attributeId` + `term`) or a presence check
                          (`attributeId` + `hasValue: true` — any accepted
                          value). Presence filters always route as hard spec
                          gates. Aesthetic/meaning term filters (pattern,
                          texture, directionality, and the other visual-meaning
                          attributes) are applied as a pool-bounded post-filter
                          over the visual candidate pool rather than a
                          whole-catalog restriction."
                        type: array
                        items:
                          anyOf:
                            - type: object
                              properties:
                                attributeId:
                                  type: string
                                  description: Canonical attribute id, e.g. "primary_material" or "fire_rating".
                                term:
                                  type: string
                                  description: Canonical term the attribute must equal, e.g. "textile".
                              required:
                                - attributeId
                                - term
                              additionalProperties: false
                            - type: object
                              properties:
                                attributeId:
                                  type: string
                                  description: Canonical attribute id that must have an accepted value, e.g.
                                    "sample_image_mm_per_px".
                                hasValue:
                                  type: boolean
                                  const: true
                                  description: Require any accepted value for this attribute.
                              required:
                                - attributeId
                                - hasValue
                              additionalProperties: false
                      limit:
                        description: Results per page (default 24, max 48).
                        type: integer
                        minimum: 1
                        maximum: 48
                      cursor:
                        description: "Opaque pagination cursor from a previous response. NOTE: ANN
                          search is approximate — tail items may reorder
                          slightly between pages."
                        type: string
                      explain:
                        description: Include per-signal scores on each result (`signals`).
                        type: boolean
                      collapse:
                        description: Group results by product before pagination. "product" returns one
                          representative (highest-ranked) variant per product,
                          with hidden siblings counted in `siblingCount`.
                          Default "none" returns every variant unchanged.
                        type: string
                        enum:
                          - none
                          - product
                      count:
                        default: false
                        description: 'Opt-in feed total. When true the response carries
                          `counts.variants` (and `counts.products` under
                          `collapse:"product"`). The variant total is EXACT up
                          to a cap of 1000 and reported as `basis:"lowerBound"`
                          beyond it — and ALWAYS `lowerBound` for ANN-ranked
                          pools (image / colour / visual-text / visual-meaning
                          post-filters), where the finite candidate pool is the
                          only honest figure. Counts are never silently
                          approximated: an exact count is labelled `exact`, a
                          capped or pool-bounded one `lowerBound`. Default false
                          (skip the count query entirely).'
                        type: boolean
                      hasImages:
                        default: false
                        description: When true, only return results that have at least one catalog image
                          (a servable card thumbnail). Off by default so search
                          stays honest about the FULL catalog, including
                          image-less records. It filters the LEXICAL attribute
                          lens — the one source that can surface an image-less
                          record; the visual arms (image / colour / text→image)
                          are inherently image-backed, so the flag is a no-op
                          there. The opt-in `counts.variants` feed total honours
                          it too (image-less records are excluded from the
                          count). Default false.
                        type: boolean
                      view:
                        default: full
                        description: Result projection. "full" (default) returns the complete
                          `searchResultItemSchema` (palette, colourMeta,
                          hasImage, siblings, explain signals/meanings). "card"
                          returns the lean `searchCardItemSchema` (identity +
                          name + thumbnail + score) for grid rendering, skipping
                          the per-page palette / appearance / meaning reads. The
                          response echoes the chosen `view`, which discriminates
                          the `results` union.
                        type: string
                        enum:
                          - full
                          - card
                    required:
                      - count
                      - hasImages
                      - view
                    additionalProperties: false
                    description: Complete replacement search request. It can be parsed and executed
                      directly; it is not a delta.
                required:
                  - label
                  - request
                additionalProperties: false
            mode:
              type: string
              enum:
                - image
                - colour
                - text
                - lexical
              description: "The single candidate source the query resolved to: 'image'
                (look-alike), 'colour' (the colour arm — pure), 'text' (visual
                text→image, the default), or 'lexical' (BM25 over the
                attribute/name projection). Colours/category rerank within this
                source, never admit."
            rerank:
              type: object
              properties:
                colour:
                  type: string
                  enum:
                    - ok
                    - failed
                    - skipped
                colourWeight:
                  type: number
                colourLabel:
                  type: string
              additionalProperties: false
            maxScore:
              type: number
            counts:
              description: 'Feed totals, present when `count:true` was requested OR when
                `collapse:"product"` grouped the results. `variants` is the
                total matching variants; `products` (only under `collapse`) the
                distinct product groups. Each total carries a `basis`: `exact`
                (true count, up to the cap of 1000) or `lowerBound` (a floor —
                the cap was hit, an ANN-ranked pool bounds it, or the count
                degraded). The lexical arm counts exactly via the tsvector
                predicate; a single accepted spec term reads the
                `mv_attribute_facet_counts` materialized view (refreshed
                post-ingest, so a hair stale by design); ANN pools (image /
                colour / visual-text) and `products` are always `lowerBound`.'
              type: object
              properties:
                variants:
                  type: object
                  properties:
                    value:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                    basis:
                      type: string
                      enum:
                        - exact
                        - lowerBound
                  required:
                    - value
                    - basis
                  additionalProperties: false
                products:
                  type: object
                  properties:
                    value:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                    basis:
                      type: string
                      enum:
                        - exact
                        - lowerBound
                  required:
                    - value
                    - basis
                  additionalProperties: false
              required:
                - variants
              additionalProperties: false
            note:
              type: string
            cursor:
              type: string
            timings:
              description: Per-stage wall-clock milliseconds (embed, retrieval, prune, rerank,
                finalize, total). Present only when `explain` is set —
                diagnostic, not part of the ranking contract.
              type: object
              propertyNames:
                type: string
              additionalProperties:
                type: number
            query:
              type: object
              propertyNames:
                type: string
              additionalProperties: {}
              description: Echo of the resolved query input (image bytes/vector are never
                echoed).
            seed:
              description: A `getVariantBrief` echo of the seed variant — GET `?variantId=`
                mode only.
              type: object
              propertyNames:
                type: string
              additionalProperties: {}
          required:
            - view
            - results
            - suggestions
            - mode
            - maxScore
            - query
          additionalProperties: false
        - type: object
          properties:
            view:
              type: string
              const: card
              description: Projection echo — always 'card' on this envelope variant.
            results:
              type: array
              items:
                type: object
                properties:
                  variantId:
                    type: string
                  productId:
                    anyOf:
                      - type: string
                      - type: "null"
                  brandSlug:
                    anyOf:
                      - type: string
                      - type: "null"
                  brand:
                    type: string
                  name:
                    type: string
                    description: Product name (the full item's `product`).
                  variantName:
                    anyOf:
                      - type: string
                      - type: "null"
                  imageUrl:
                    anyOf:
                      - type: string
                      - type: "null"
                  score:
                    type: number
                required:
                  - variantId
                  - productId
                  - brandSlug
                  - brand
                  - name
                  - variantName
                  - imageUrl
                  - score
                additionalProperties: false
            suggestions:
              default: []
              description: Bounded structured refinements. Each request is a complete
                executable replacement for the current search request.
              maxItems: 6
              type: array
              items:
                type: object
                properties:
                  label:
                    type: string
                    minLength: 1
                    description: Human-readable description of the refinement.
                  request:
                    type: object
                    properties:
                      text:
                        description: Free-text query, matched VISUALLY against material imagery
                          ("striped wallpaper", "warm terrazzo"). A bare colour
                          word ("sage") automatically runs as a colour-only
                          search.
                        type: string
                        maxLength: 500
                      textModes:
                        description: How to interpret `text`. Default ["visual"] — a text→image VISUAL
                          search. "lexical" (opt-in) runs a BM25 ATTRIBUTE-lens
                          search over material names + canonical attribute terms
                          (find by what it's called / what it IS); detected
                          colour words are stripped from the lexical terms and
                          reranked instead. Requesting BOTH ["visual","lexical"]
                          runs the visual search and notes it — compose a
                          separate lexical search to blend them.
                        type: array
                        items:
                          type: string
                          enum:
                            - visual
                            - lexical
                      colours:
                        description: "Explicit colour palette to match. Each entry is EITHER a `hex` OR
                          a `name` (a descriptive or brand paint name resolved
                          server-side — never invent a hex for a name).
                          PRECEDENCE: when set, these override any colour word
                          detected in `text`. Unresolvable names fail with a
                          typed error listing them, so you can correct and
                          retry."
                        maxItems: 8
                        type: array
                        items:
                          type: object
                          properties:
                            hex:
                              description: The colour to match, as 3- or 6-digit hex. Provide EITHER `hex` OR
                                `name`, never both. Only pass `hex` when you
                                already hold an EXACT hex — for a colour NAME
                                use `name` and let the server resolve it.
                              type: string
                              pattern: ^#?(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$
                            name:
                              description: A colour NAME to match — a descriptive name ('sage green',
                                'terracotta', 'warm sand') or a brand paint name
                                ('Gauntlet Gray', 'Hale Navy', 'White Dove').
                                Resolved server-side to its exact library hex
                                (misspellings are fuzzy-matched). NEVER invent a
                                hex for a name — pass the name here. Provide
                                EITHER `name` OR `hex`, never both.
                              type: string
                              minLength: 1
                              maxLength: 120
                            weight:
                              description: Relative proportion of this colour in the desired palette (0–1).
                                Weights are normalised across the palette; omit
                                for equal parts.
                              type: number
                              exclusiveMinimum: 0
                              maximum: 1
                            role:
                              type: string
                              enum:
                                - any
                                - is
                                - accent
                              description: "Per-colour swatch eligibility (default 'any'). PRECEDENCE:
                                `colourLogic` governs cross-colour combination
                                (all/any); `role` governs which swatch of a
                                product may satisfy THIS colour, and ANDs on top
                                of the logic. 'is' = the colour must be the
                                product's DOMINANT swatch; 'accent' = the colour
                                must be a minor swatch (proportion 0.05–0.35)
                                and is excluded from the proportion-match term;
                                'any' = today's nearest-swatch match."
                            sourceName:
                              description: Human name the colour came from (e.g. "sage", "Elephant's Breath")
                                — echoed back for display; never affects
                                matching. (For a name you want RESOLVED to a
                                hex, use `name` instead.)
                              type: string
                              maxLength: 120
                          additionalProperties: false
                      colourLogic:
                        description: '"all" (default): results should contain the whole palette, ranked
                          by average distance + proportion match. "any": results
                          matching ANY palette colour, ranked by their best
                          single-colour distance.'
                        type: string
                        enum:
                          - all
                          - any
                      colourTolerance:
                        description: How close a colour must be to count as a match. 'normal' (default)
                          is today's behaviour; 'tight' narrows the OKLab match
                          threshold and prunes far single-colour matches;
                          'loose' widens the threshold.
                        type: string
                        enum:
                          - tight
                          - normal
                          - loose
                      colourAppearance:
                        type: object
                        properties:
                          lightness:
                            description: OKLab lightness window (L ∈ [0,1]); dark ≈ 0, light ≈ 1.
                            type: object
                            properties:
                              min:
                                type: number
                                minimum: 0
                                maximum: 1
                              max:
                                type: number
                                minimum: 0
                                maximum: 1
                            required:
                              - min
                              - max
                            additionalProperties: false
                          warmth:
                            description: OKLab warmth window (b axis); cool < 0 < warm, ≈ −0.1..0.1.
                            type: object
                            properties:
                              min:
                                type: number
                                minimum: -0.5
                                maximum: 0.5
                              max:
                                type: number
                                minimum: -0.5
                                maximum: 0.5
                            required:
                              - min
                              - max
                            additionalProperties: false
                        additionalProperties: false
                        description: HARD appearance gate on the material's DOMINANT colour, independent
                          of the colour palette — works on a text-only query
                          too. Variants without colour extraction are EXCLUDED
                          whenever any axis is set (NULL scalars never match).
                      mood:
                        type: string
                        enum:
                          - warm
                          - cool
                          - earthy
                          - moody
                          - bold
                          - light-airy
                          - natural
                          - neutral
                          - romantic
                          - coastal
                          - industrial
                          - luxe
                          - vintage
                          - serene
                          - scandi
                          - cosy
                          - heritage
                          - mid-century
                          - pastel
                        description: "Mood-language colour entry — resolves to a curated checked-in
                          colour palette. Use it for evocative mood queries
                          ('coastal', 'moody', 'earthy') rather than a single
                          colour. A mood ALONE runs as a colour search; a mood
                          WITH text reranks the text results by the mood palette
                          (a gentle tint). One of: warm (Warm — Reds, oranges,
                          yellows - inviting and cozy); cool (Cool — Blues,
                          teals, cool grays - calm and professional); earthy
                          (Earthy — Browns, tans, terracotta - natural and
                          grounded); moody (Moody — Deep, saturated, dramatic -
                          rich and atmospheric); bold (Bold — High chroma,
                          vibrant - energetic and eye-catching); light-airy
                          (Light + Airy — Pastels, pale tones - soft and
                          spacious); natural (Natural — Greens, wood tones,
                          stone - organic and balanced); neutral (Neutral —
                          Whites, grays, blacks - versatile and timeless);
                          romantic (Romantic — Soft pinks, mauves, blush -
                          tender and elegant); coastal (Coastal — Sandy beiges,
                          ocean blues, seafoam - relaxed seaside); industrial
                          (Industrial — Cool grays, concrete, metallic - urban
                          and modern); luxe (Luxe — Deep jewel tones, rich
                          emeralds, sapphires - opulent); vintage (Vintage —
                          Muted, dusty tones - nostalgic and timeworn); serene
                          (Serene — Soft blues, sage, muted lavender - calm and
                          peaceful); scandi (Scandi — Warm whites, pale woods,
                          cream - Scandinavian minimalism); cosy (Cosy — Deep
                          plaster, amber, tobacco - enveloping warmth); heritage
                          (Heritage — Rich historical tones with saturated depth
                          - classic and grounded); mid-century (Mid-Century —
                          Olive, mustard, teal, terracotta - retro modern);
                          pastel (Pastel — Sage, duck egg, blush - soft colors
                          with body)."
                      sort:
                        type: string
                        enum:
                          - relevance
                          - light
                          - dark
                          - vivid
                          - muted
                          - colourful
                        description: Re-order the final scored results by a colour-appearance axis
                          (default 'relevance' = the fused score).
                          'light'/'dark' by dominant lightness, 'vivid'/'muted'
                          by chroma, 'colourful' by overall vibrancy. With
                          search signals present this REORDERS the matches; for
                          pure colour browsing use the dedicated browse surface
                          instead. Variants without colour extraction sink to
                          the end.
                      similarTo:
                        description: "Find materials that LOOK like a seed image. Supply `variantId` to
                          seed from an existing variant's primary image, or
                          `vector` — a pre-computed halfvec literal (`[a,b,…]`)
                          from an externally-embedded image — to drive the image
                          arm directly. PRECEDENCE: wins over `text` (the
                          response carries a note when both were given)."
                        anyOf:
                          - type: object
                            properties:
                              variantId:
                                type: string
                            required:
                              - variantId
                            additionalProperties: false
                          - type: object
                            properties:
                              vector:
                                type: string
                            required:
                              - vector
                            additionalProperties: false
                      image:
                        description: "A raw incoming image to search by (URL or base64). The HTTP
                          adapter embeds it and sets `similarTo: { vector }`;
                          wins over `text`."
                        oneOf:
                          - type: object
                            properties:
                              type:
                                type: string
                                const: url
                              url:
                                type: string
                                format: uri
                                description: HTTP(S) URL of the query image. MUST be an https URL.
                            required:
                              - type
                              - url
                            additionalProperties: false
                          - type: object
                            properties:
                              type:
                                type: string
                                const: base64
                              data:
                                type: string
                                minLength: 1
                                description: Base64-encoded image bytes (or a data URI) the caller already holds
                                  — e.g. a form upload — so there is nothing to
                                  host.
                            required:
                              - type
                              - data
                            additionalProperties: false
                        type: object
                      category:
                        type: object
                        properties:
                          boost:
                            description: Soft category preference — results of this product type score
                              higher but others still appear.
                            type: string
                          filter:
                            description: "HARD gate: only these public taxonomy categories are returned.
                              Values are category-group or product-type display
                              names (case-insensitive), or canonical taxonomy
                              keys. A group expands to every child product type;
                              an unknown value errors listing every valid name."
                            type: array
                            items:
                              type: string
                        additionalProperties: false
                      weights:
                        type: object
                        properties:
                          image:
                            type: number
                            minimum: 0
                          category:
                            type: number
                            minimum: 0
                          colour:
                            type: number
                            minimum: 0
                          lexical:
                            type: number
                            minimum: 0
                        additionalProperties: false
                        description: "FREE multipliers over the per-signal scores (each signal ∈ [0,1]);
                          no sum-to-1 requirement. Omitted keys use the mode
                          defaults (text: image 1.0 / category 0.5, plus a
                          colour RERANK weight of 0.4 for a strong/explicit
                          colour or 0.25 for a weak/derived one; colour mode:
                          colour 1.0). Max achievable score = sum of active
                          weights."
                      filters:
                        description: "Typed attribute filters applied as a hard restriction before
                          ranking. Each entry is either a term match
                          (`attributeId` + `term`) or a presence check
                          (`attributeId` + `hasValue: true` — any accepted
                          value). Presence filters always route as hard spec
                          gates. Aesthetic/meaning term filters (pattern,
                          texture, directionality, and the other visual-meaning
                          attributes) are applied as a pool-bounded post-filter
                          over the visual candidate pool rather than a
                          whole-catalog restriction."
                        type: array
                        items:
                          anyOf:
                            - type: object
                              properties:
                                attributeId:
                                  type: string
                                  description: Canonical attribute id, e.g. "primary_material" or "fire_rating".
                                term:
                                  type: string
                                  description: Canonical term the attribute must equal, e.g. "textile".
                              required:
                                - attributeId
                                - term
                              additionalProperties: false
                            - type: object
                              properties:
                                attributeId:
                                  type: string
                                  description: Canonical attribute id that must have an accepted value, e.g.
                                    "sample_image_mm_per_px".
                                hasValue:
                                  type: boolean
                                  const: true
                                  description: Require any accepted value for this attribute.
                              required:
                                - attributeId
                                - hasValue
                              additionalProperties: false
                      limit:
                        description: Results per page (default 24, max 48).
                        type: integer
                        minimum: 1
                        maximum: 48
                      cursor:
                        description: "Opaque pagination cursor from a previous response. NOTE: ANN
                          search is approximate — tail items may reorder
                          slightly between pages."
                        type: string
                      explain:
                        description: Include per-signal scores on each result (`signals`).
                        type: boolean
                      collapse:
                        description: Group results by product before pagination. "product" returns one
                          representative (highest-ranked) variant per product,
                          with hidden siblings counted in `siblingCount`.
                          Default "none" returns every variant unchanged.
                        type: string
                        enum:
                          - none
                          - product
                      count:
                        default: false
                        description: 'Opt-in feed total. When true the response carries
                          `counts.variants` (and `counts.products` under
                          `collapse:"product"`). The variant total is EXACT up
                          to a cap of 1000 and reported as `basis:"lowerBound"`
                          beyond it — and ALWAYS `lowerBound` for ANN-ranked
                          pools (image / colour / visual-text / visual-meaning
                          post-filters), where the finite candidate pool is the
                          only honest figure. Counts are never silently
                          approximated: an exact count is labelled `exact`, a
                          capped or pool-bounded one `lowerBound`. Default false
                          (skip the count query entirely).'
                        type: boolean
                      hasImages:
                        default: false
                        description: When true, only return results that have at least one catalog image
                          (a servable card thumbnail). Off by default so search
                          stays honest about the FULL catalog, including
                          image-less records. It filters the LEXICAL attribute
                          lens — the one source that can surface an image-less
                          record; the visual arms (image / colour / text→image)
                          are inherently image-backed, so the flag is a no-op
                          there. The opt-in `counts.variants` feed total honours
                          it too (image-less records are excluded from the
                          count). Default false.
                        type: boolean
                      view:
                        default: full
                        description: Result projection. "full" (default) returns the complete
                          `searchResultItemSchema` (palette, colourMeta,
                          hasImage, siblings, explain signals/meanings). "card"
                          returns the lean `searchCardItemSchema` (identity +
                          name + thumbnail + score) for grid rendering, skipping
                          the per-page palette / appearance / meaning reads. The
                          response echoes the chosen `view`, which discriminates
                          the `results` union.
                        type: string
                        enum:
                          - full
                          - card
                    required:
                      - count
                      - hasImages
                      - view
                    additionalProperties: false
                    description: Complete replacement search request. It can be parsed and executed
                      directly; it is not a delta.
                required:
                  - label
                  - request
                additionalProperties: false
            mode:
              type: string
              enum:
                - image
                - colour
                - text
                - lexical
              description: "The single candidate source the query resolved to: 'image'
                (look-alike), 'colour' (the colour arm — pure), 'text' (visual
                text→image, the default), or 'lexical' (BM25 over the
                attribute/name projection). Colours/category rerank within this
                source, never admit."
            rerank:
              type: object
              properties:
                colour:
                  type: string
                  enum:
                    - ok
                    - failed
                    - skipped
                colourWeight:
                  type: number
                colourLabel:
                  type: string
              additionalProperties: false
            maxScore:
              type: number
            counts:
              description: 'Feed totals, present when `count:true` was requested OR when
                `collapse:"product"` grouped the results. `variants` is the
                total matching variants; `products` (only under `collapse`) the
                distinct product groups. Each total carries a `basis`: `exact`
                (true count, up to the cap of 1000) or `lowerBound` (a floor —
                the cap was hit, an ANN-ranked pool bounds it, or the count
                degraded). The lexical arm counts exactly via the tsvector
                predicate; a single accepted spec term reads the
                `mv_attribute_facet_counts` materialized view (refreshed
                post-ingest, so a hair stale by design); ANN pools (image /
                colour / visual-text) and `products` are always `lowerBound`.'
              type: object
              properties:
                variants:
                  type: object
                  properties:
                    value:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                    basis:
                      type: string
                      enum:
                        - exact
                        - lowerBound
                  required:
                    - value
                    - basis
                  additionalProperties: false
                products:
                  type: object
                  properties:
                    value:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                    basis:
                      type: string
                      enum:
                        - exact
                        - lowerBound
                  required:
                    - value
                    - basis
                  additionalProperties: false
              required:
                - variants
              additionalProperties: false
            note:
              type: string
            cursor:
              type: string
            timings:
              description: Per-stage wall-clock milliseconds (embed, retrieval, prune, rerank,
                finalize, total). Present only when `explain` is set —
                diagnostic, not part of the ranking contract.
              type: object
              propertyNames:
                type: string
              additionalProperties:
                type: number
            query:
              type: object
              propertyNames:
                type: string
              additionalProperties: {}
              description: Echo of the resolved query input (image bytes/vector are never
                echoed).
            seed:
              description: A `getVariantBrief` echo of the seed variant — GET `?variantId=`
                mode only.
              type: object
              propertyNames:
                type: string
              additionalProperties: {}
          required:
            - view
            - results
            - suggestions
            - mode
            - maxScore
            - query
          additionalProperties: false
      type: object
    MatchMaterialResponse:
      type: object
      properties:
        results:
          type: array
          items:
            type: object
            properties:
              variantId:
                type: string
              brand:
                type: string
              product:
                type: string
              brandSlug:
                anyOf:
                  - type: string
                  - type: "null"
              productId:
                anyOf:
                  - type: string
                  - type: "null"
              color:
                anyOf:
                  - type: string
                  - type: "null"
              variantName:
                anyOf:
                  - type: string
                  - type: "null"
              imageUrl:
                anyOf:
                  - type: string
                  - type: "null"
              hasImage:
                type: boolean
              palette:
                type: array
                items:
                  type: object
                  properties:
                    hex:
                      type: string
                    proportion:
                      type: number
                    name:
                      anyOf:
                        - type: string
                        - type: "null"
                  required:
                    - hex
                    - proportion
                  additionalProperties: false
              colourMeta:
                type: object
                properties:
                  mood:
                    type: string
                  dominantFamily:
                    type: string
                  dominantHex:
                    type: string
                additionalProperties: false
              score:
                type: number
              signals:
                type: object
                properties:
                  image:
                    anyOf:
                      - type: number
                      - type: "null"
                  category:
                    anyOf:
                      - type: number
                      - type: "null"
                  colour:
                    anyOf:
                      - type: number
                      - type: "null"
                  lexical:
                    anyOf:
                      - type: number
                      - type: "null"
                required:
                  - image
                  - category
                  - colour
                  - lexical
                additionalProperties: false
              retired:
                description: MB lists the SKU as not visible/retired; demoted in ranking.
                  Omitted when false.
                type: boolean
              siblingCount:
                description: 'Additional variants of the same product hidden by `collapse:
                  "product"`. Present only when grouping hid at least one
                  sibling.'
                type: integer
                minimum: 1
                maximum: 9007199254740991
              visualMeanings:
                description: Existing accepted/measured visual meanings — descriptive evidence,
                  not ranking causes. Present only when `explain` was set and at
                  least one meaning exists.
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: string
                      description: Registry attribute id, or a measured-meaning id like `colour_mood`.
                    label:
                      type: string
                      description: Human label — registry presentation label for canonical meanings.
                    values:
                      minItems: 1
                      type: array
                      items:
                        type: string
                      description: Human-readable value labels.
                    basis:
                      type: string
                      enum:
                        - canonical_attribute
                        - image_colour_analysis
                      description: "Where the meaning comes from: an accepted canonical attribute
                        value, or measured image colour analysis. NEVER derived
                        from vector similarity or scores."
                    confidence:
                      type: number
                    sourceBasis:
                      type: string
                    version:
                      description: Extraction version for measured meanings.
                      type: string
                    appliedAt:
                      description: ISO timestamp the accepted value was applied.
                      type: string
                  required:
                    - id
                    - label
                    - values
                    - basis
                  additionalProperties: false
            required:
              - variantId
              - brand
              - product
              - brandSlug
              - productId
              - color
              - variantName
              - imageUrl
              - hasImage
              - palette
              - score
            additionalProperties: false
        inferredCategories:
          type: array
          items:
            type: object
            properties:
              productTypeId:
                type: string
              displayName:
                type: string
              boost:
                type: number
            required:
              - productTypeId
              - displayName
              - boost
            additionalProperties: false
        qualityFloor:
          type: number
        poolSize:
          type: number
        droppedBelowFloor:
          type: number
        droppedAsDuplicateProduct:
          type: number
        note:
          type: string
      required:
        - results
        - inferredCategories
        - qualityFloor
        - poolSize
        - droppedBelowFloor
        - droppedAsDuplicateProduct
      additionalProperties: false
    ByColourResponse:
      type: object
      properties:
        query:
          type: object
          properties:
            colours:
              type: array
              items:
                type: string
              description: The normalised `#rrggbb` palette the search ran against.
            categories:
              description: Echoed category filter, present only when one was applied.
              type: array
              items:
                type: string
            oklab:
              description: Echoed OKLab triple, present only when one was provided.
              type: object
              properties:
                l:
                  type: number
                a:
                  type: number
                b:
                  type: number
              required:
                - l
                - a
                - b
              additionalProperties: false
          required:
            - colours
          additionalProperties: false
          description: Echo of the resolved query input.
        count:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: Number of matches returned.
        matches:
          type: array
          items:
            type: object
            properties:
              variantId:
                type: string
              brand:
                type: string
              product:
                type: string
              color:
                anyOf:
                  - type: string
                  - type: "null"
              variantName:
                anyOf:
                  - type: string
                  - type: "null"
              imageUrl:
                anyOf:
                  - type: string
                  - type: "null"
              hex:
                anyOf:
                  - type: string
                  - type: "null"
              distance:
                type: number
              closeness:
                type: number
              category:
                anyOf:
                  - type: object
                    properties:
                      group:
                        anyOf:
                          - type: string
                          - type: "null"
                        description: Product-type GROUP display name (coarse), or null.
                      type:
                        anyOf:
                          - type: string
                          - type: "null"
                        description: Primary product-TYPE display name (fine), or null.
                    required:
                      - group
                      - type
                    additionalProperties: false
                  - type: "null"
                description: The match's primary product type (accepted classifications only),
                  or null when unclassified.
              oklab:
                anyOf:
                  - type: object
                    properties:
                      l:
                        type: number
                      a:
                        type: number
                      b:
                        type: number
                    required:
                      - l
                      - a
                      - b
                    additionalProperties: false
                  - type: "null"
                description: OKLab of the match hex, or null when the match carried no hex.
            required:
              - variantId
              - brand
              - product
              - color
              - variantName
              - imageUrl
              - hex
              - distance
              - closeness
              - category
              - oklab
            additionalProperties: false
          description: Colour matches, nearest palette first.
      required:
        - query
        - count
        - matches
      additionalProperties: false
    BrowseColourResponse:
      type: object
      properties:
        results:
          type: array
          items:
            type: object
            properties:
              variantId:
                type: string
              brand:
                type: string
              product:
                type: string
              brandSlug:
                anyOf:
                  - type: string
                  - type: "null"
              productId:
                anyOf:
                  - type: string
                  - type: "null"
              color:
                anyOf:
                  - type: string
                  - type: "null"
              variantName:
                anyOf:
                  - type: string
                  - type: "null"
              imageUrl:
                anyOf:
                  - type: string
                  - type: "null"
              scalars:
                type: object
                properties:
                  l:
                    anyOf:
                      - type: number
                      - type: "null"
                  chroma:
                    anyOf:
                      - type: number
                      - type: "null"
                  vibrancy:
                    anyOf:
                      - type: number
                      - type: "null"
                required:
                  - l
                  - chroma
                  - vibrancy
                additionalProperties: false
              retired:
                type: boolean
            required:
              - variantId
              - brand
              - product
              - brandSlug
              - productId
              - color
              - variantName
              - imageUrl
              - scalars
            additionalProperties: false
        cursor:
          type: string
      required:
        - results
      additionalProperties: false
    ExtractPaletteResponse:
      type: object
      properties:
        palette:
          anyOf:
            - type: object
              properties:
                colors:
                  type: array
                  items:
                    type: object
                    properties:
                      hex:
                        type: string
                      name:
                        type: string
                      proportion:
                        type: number
                    required:
                      - hex
                      - name
                      - proportion
                    additionalProperties: false
                  description: Named palette swatches, most dominant first.
                dominant:
                  anyOf:
                    - type: object
                      properties:
                        family:
                          type: string
                          description: Coarse hue family, e.g. 'green'.
                        hex:
                          type: string
                        name:
                          type: string
                        oklch:
                          type: string
                          description: OKLCH string, e.g. 'oklch(72% 0.040 145)'.
                      required:
                        - family
                        - hex
                        - name
                        - oklch
                      additionalProperties: false
                    - type: "null"
                  description: The dominant colour, or null when none could be derived.
                mood:
                  type: string
                  description: A one-word read of the palette, e.g. "muted", "vibrant", "dark".
              required:
                - colors
                - dominant
                - mood
              additionalProperties: false
            - type: "null"
          description: The extracted palette, or null when extraction found no usable
            colour.
      required:
        - palette
      additionalProperties: false
    ResolveColourResponse:
      type: object
      properties:
        match:
          anyOf:
            - type: object
              properties:
                hex:
                  type: string
                  description: The resolved `#rrggbb` hex.
                name:
                  type: string
                  description: The resolved display name (echoes the query when unnamed).
                code:
                  description: Vendor/standard code on a brand hit (e.g. "OC-17").
                  type: string
                libraryId:
                  description: Colorscope library id of a brand hit (e.g. "benjamin-moore").
                  type: string
              required:
                - hex
                - name
              additionalProperties: false
            - type: "null"
          description: The resolved colour, or null when nothing resolved.
      required:
        - match
      additionalProperties: false
    SuggestColourResponse:
      type: object
      properties:
        suggestions:
          type: array
          items:
            type: object
            properties:
              term:
                type: string
              hex:
                type: string
              libraryId:
                type: string
              kind:
                type: string
            required:
              - term
              - hex
              - kind
            additionalProperties: false
          description: Ranked colour-name suggestions, best match first.
      required:
        - suggestions
      additionalProperties: false
    MaterialDetail:
      type: object
      properties:
        variantId:
          type: string
        brand:
          type: string
        product:
          type: string
        variantName:
          anyOf:
            - type: string
            - type: "null"
        imageUrl:
          anyOf:
            - type: string
            - type: "null"
        imageSource:
          anyOf:
            - type: string
              enum:
                - r2
                - origin
            - type: "null"
        category:
          anyOf:
            - type: string
            - type: "null"
        colour:
          type: object
          properties:
            dominantHex:
              anyOf:
                - type: string
                - type: "null"
            hsl:
              anyOf:
                - type: object
                  properties:
                    h:
                      type: number
                      minimum: 0
                      exclusiveMaximum: 360
                    s:
                      type: number
                      minimum: 0
                      maximum: 1
                    l:
                      type: number
                      minimum: 0
                      maximum: 1
                  required:
                    - h
                    - s
                    - l
                  additionalProperties: false
                - type: "null"
            oklab:
              anyOf:
                - type: object
                  properties:
                    l:
                      type: number
                    a:
                      type: number
                    b:
                      type: number
                  required:
                    - l
                    - a
                    - b
                  additionalProperties: false
                - type: "null"
            chroma:
              anyOf:
                - type: number
                - type: "null"
            vibrancy:
              anyOf:
                - type: number
                - type: "null"
          required:
            - dominantHex
            - hsl
            - oklab
            - chroma
            - vibrancy
          additionalProperties: false
        brandSlug:
          type: string
        productId:
          type: string
        color:
          anyOf:
            - type: string
            - type: "null"
        materialBankId:
          anyOf:
            - type: string
            - type: "null"
        installationImageUrl:
          anyOf:
            - type: string
            - type: "null"
        displayCategory:
          anyOf:
            - type: string
            - type: "null"
        categoryPath:
          type: array
          items:
            type: string
        palette:
          type: array
          items:
            type: object
            properties:
              hex:
                type: string
              proportion:
                type: number
              name:
                anyOf:
                  - type: string
                  - type: "null"
            required:
              - hex
              - proportion
            additionalProperties: false
        facets:
          type: array
          items:
            type: object
            properties:
              attributeId:
                type: string
              term:
                type: string
            required:
              - attributeId
              - term
            additionalProperties: false
        dimensions:
          anyOf:
            - type: array
              items:
                type: object
                properties:
                  label:
                    description: Free-text descriptor of this configuration (e.g. "Plank").
                    type: string
                  tile_size:
                    description: Nominal tile size as the source states it (e.g. "3x8").
                    type: string
                  measurement_basis:
                    description: How the measurements were taken (e.g. "nominal", "actual").
                    type: string
                  width_mm:
                    description: Width in millimetres.
                    type: number
                  length_mm:
                    description: Length in millimetres.
                    type: number
                  height_mm:
                    description: Height in millimetres.
                    type: number
                  depth_mm:
                    description: Depth in millimetres.
                    type: number
                  thickness_mm:
                    description: Thickness in millimetres.
                    type: number
                  diameter_mm:
                    description: Diameter in millimetres.
                    type: number
                  visible_projection_mm:
                    description: Visible projection (relief depth) in millimetres.
                    type: number
                  weight_kg:
                    description: Weight in kilograms.
                    type: number
                additionalProperties: {}
                description: One dimensional configuration; numeric fields are
                  millimetres/kilograms.
            - type: "null"
      required:
        - variantId
        - brand
        - product
        - variantName
        - imageUrl
        - imageSource
        - category
        - colour
        - brandSlug
        - productId
        - color
        - materialBankId
        - installationImageUrl
        - displayCategory
        - categoryPath
        - palette
        - facets
        - dimensions
      additionalProperties: false
    MaterialTile:
      type: object
      properties:
        variantId:
          type: string
        brand:
          type: string
        product:
          type: string
        variantName:
          anyOf:
            - type: string
            - type: "null"
        imageUrl:
          anyOf:
            - type: string
            - type: "null"
        imageSource:
          anyOf:
            - type: string
              enum:
                - r2
                - origin
            - type: "null"
        category:
          anyOf:
            - type: string
            - type: "null"
        colour:
          type: object
          properties:
            dominantHex:
              anyOf:
                - type: string
                - type: "null"
            hsl:
              anyOf:
                - type: object
                  properties:
                    h:
                      type: number
                      minimum: 0
                      exclusiveMaximum: 360
                    s:
                      type: number
                      minimum: 0
                      maximum: 1
                    l:
                      type: number
                      minimum: 0
                      maximum: 1
                  required:
                    - h
                    - s
                    - l
                  additionalProperties: false
                - type: "null"
            oklab:
              anyOf:
                - type: object
                  properties:
                    l:
                      type: number
                    a:
                      type: number
                    b:
                      type: number
                  required:
                    - l
                    - a
                    - b
                  additionalProperties: false
                - type: "null"
            chroma:
              anyOf:
                - type: number
                - type: "null"
            vibrancy:
              anyOf:
                - type: number
                - type: "null"
          required:
            - dominantHex
            - hsl
            - oklab
            - chroma
            - vibrancy
          additionalProperties: false
      required:
        - variantId
        - brand
        - product
        - variantName
        - imageUrl
        - imageSource
        - category
        - colour
      additionalProperties: false
    PairingItem:
      oneOf:
        - type: object
          properties:
            variantId:
              type: string
            brand:
              type: string
            product:
              type: string
            color:
              anyOf:
                - type: string
                - type: "null"
            variantName:
              anyOf:
                - type: string
                - type: "null"
            imageUrl:
              anyOf:
                - type: string
                - type: "null"
            brandSlug:
              type: string
            productId:
              type: string
            category:
              anyOf:
                - type: string
                - type: "null"
            projectCount:
              type: number
            customerCount:
              type: number
            lift:
              type: number
            liftLabel:
              type: string
            demand:
              anyOf:
                - type: object
                  properties:
                    specCount:
                      type: number
                    projectCount:
                      type: number
                    momentum:
                      anyOf:
                        - type: object
                          properties:
                            state:
                              type: string
                              enum:
                                - rising
                                - fading
                                - stable
                                - emerging
                                - insufficient_volume
                            percentChange:
                              anyOf:
                                - type: number
                                - type: "null"
                            trailingSpecCount:
                              type: number
                            priorSpecCount:
                              type: number
                          required:
                            - state
                            - percentChange
                            - trailingSpecCount
                            - priorSpecCount
                          additionalProperties: false
                        - type: "null"
                  required:
                    - specCount
                    - projectCount
                    - momentum
                  additionalProperties: false
                - type: "null"
            topProjectTypes:
              type: array
              items:
                type: object
                properties:
                  projectType:
                    type: string
                  projectCount:
                    type: number
                required:
                  - projectType
                  - projectCount
                additionalProperties: false
            source:
              type: string
              const: cospec
          required:
            - variantId
            - brand
            - product
            - color
            - variantName
            - imageUrl
            - brandSlug
            - productId
            - category
            - projectCount
            - customerCount
            - lift
            - liftLabel
            - demand
            - topProjectTypes
            - source
          additionalProperties: false
        - type: object
          properties:
            variantId:
              type: string
            brand:
              type: string
            product:
              type: string
            color:
              anyOf:
                - type: string
                - type: "null"
            variantName:
              anyOf:
                - type: string
                - type: "null"
            imageUrl:
              anyOf:
                - type: string
                - type: "null"
            brandSlug:
              type: string
            productId:
              type: string
            category:
              anyOf:
                - type: string
                - type: "null"
            projectCount:
              type: number
            customerCount:
              type: number
            lift:
              type: number
            liftLabel:
              type: string
            demand:
              anyOf:
                - type: object
                  properties:
                    specCount:
                      type: number
                    projectCount:
                      type: number
                    momentum:
                      anyOf:
                        - type: object
                          properties:
                            state:
                              type: string
                              enum:
                                - rising
                                - fading
                                - stable
                                - emerging
                                - insufficient_volume
                            percentChange:
                              anyOf:
                                - type: number
                                - type: "null"
                            trailingSpecCount:
                              type: number
                            priorSpecCount:
                              type: number
                          required:
                            - state
                            - percentChange
                            - trailingSpecCount
                            - priorSpecCount
                          additionalProperties: false
                        - type: "null"
                  required:
                    - specCount
                    - projectCount
                    - momentum
                  additionalProperties: false
                - type: "null"
            topProjectTypes:
              type: array
              items:
                type: object
                properties:
                  projectType:
                    type: string
                  projectCount:
                    type: number
                required:
                  - projectType
                  - projectCount
                additionalProperties: false
            source:
              type: string
              const: considered
          required:
            - variantId
            - brand
            - product
            - color
            - variantName
            - imageUrl
            - brandSlug
            - productId
            - category
            - projectCount
            - customerCount
            - lift
            - liftLabel
            - demand
            - topProjectTypes
            - source
          additionalProperties: false
        - type: object
          properties:
            variantId:
              type: string
            brand:
              type: string
            product:
              type: string
            variantName:
              anyOf:
                - type: string
                - type: "null"
            imageUrl:
              anyOf:
                - type: string
                - type: "null"
            imageSource:
              anyOf:
                - type: string
                  enum:
                    - r2
                    - origin
                - type: "null"
            category:
              anyOf:
                - type: string
                - type: "null"
            colour:
              type: object
              properties:
                dominantHex:
                  anyOf:
                    - type: string
                    - type: "null"
                hsl:
                  anyOf:
                    - type: object
                      properties:
                        h:
                          type: number
                          minimum: 0
                          exclusiveMaximum: 360
                        s:
                          type: number
                          minimum: 0
                          maximum: 1
                        l:
                          type: number
                          minimum: 0
                          maximum: 1
                      required:
                        - h
                        - s
                        - l
                      additionalProperties: false
                    - type: "null"
                oklab:
                  anyOf:
                    - type: object
                      properties:
                        l:
                          type: number
                        a:
                          type: number
                        b:
                          type: number
                      required:
                        - l
                        - a
                        - b
                      additionalProperties: false
                    - type: "null"
                chroma:
                  anyOf:
                    - type: number
                    - type: "null"
                vibrancy:
                  anyOf:
                    - type: number
                    - type: "null"
              required:
                - dominantHex
                - hsl
                - oklab
                - chroma
                - vibrancy
              additionalProperties: false
            source:
              type: string
              const: colour
            score:
              type: number
          required:
            - variantId
            - brand
            - product
            - variantName
            - imageUrl
            - imageSource
            - category
            - colour
            - source
            - score
          additionalProperties: false
      type: object
    FindSubstitutesResponse:
      oneOf:
        - type: object
          properties:
            status:
              type: string
              const: available
            signatureUsed:
              type: object
              properties:
                family:
                  type: string
                  enum:
                    - textile
                    - tile
                    - coating
                    - lighting_electrical
                    - acoustic
                    - surface
                    - furniture
                    - sanitaryware
                    - fittings
                    - architectural_hardware
                form:
                  type: string
                  minLength: 1
                criteria:
                  type: array
                  prefixItems:
                    - type: object
                      properties:
                        role:
                          type: string
                          enum:
                            - primary_material
                            - performance
                        attributeId:
                          type: string
                          minLength: 1
                        term:
                          type: string
                          minLength: 1
                      required:
                        - role
                        - attributeId
                        - term
                      additionalProperties: false
                    - type: object
                      properties:
                        role:
                          type: string
                          enum:
                            - primary_material
                            - performance
                        attributeId:
                          type: string
                          minLength: 1
                        term:
                          type: string
                          minLength: 1
                      required:
                        - role
                        - attributeId
                        - term
                      additionalProperties: false
                  minItems: 2
                  maxItems: 2
              required:
                - family
                - form
                - criteria
              additionalProperties: false
            items:
              type: array
              items:
                type: object
                properties:
                  variantId:
                    type: string
                    minLength: 1
                  brandSlug:
                    type: string
                    minLength: 1
                    maxLength: 128
                    pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
                  brand:
                    type: string
                  product:
                    type: string
                  variant:
                    type: string
                  color:
                    anyOf:
                      - type: string
                      - type: "null"
                  imageUrl:
                    anyOf:
                      - type: string
                      - type: "null"
                  hex:
                    anyOf:
                      - type: string
                      - type: "null"
                  matched:
                    type: array
                    items:
                      type: object
                      properties:
                        attributeId:
                          type: string
                          minLength: 1
                        confidence:
                          anyOf:
                            - type: number
                              minimum: 0
                              maximum: 1
                            - type: "null"
                        sourceBasis:
                          anyOf:
                            - type: string
                            - type: "null"
                      required:
                        - attributeId
                        - confidence
                        - sourceBasis
                      additionalProperties: false
                  minConfidence:
                    anyOf:
                      - type: number
                        minimum: 0
                        maximum: 1
                      - type: "null"
                  baseScore:
                    type: number
                    minimum: 0
                    maximum: 1
                  diversityAdjustment:
                    type: number
                    minimum: 0
                    maximum: 1
                  score:
                    type: number
                    minimum: 0
                    maximum: 2
                required:
                  - variantId
                  - brandSlug
                  - brand
                  - product
                  - variant
                  - color
                  - imageUrl
                  - hex
                  - matched
                  - minConfidence
                  - baseScore
                  - diversityAdjustment
                  - score
                additionalProperties: false
            candidatePoolSize:
              type: integer
              minimum: 0
              maximum: 10000
            nextCursor:
              type: string
              minLength: 1
          required:
            - status
            - signatureUsed
            - items
            - candidatePoolSize
          additionalProperties: false
        - type: object
          properties:
            status:
              type: string
              const: unsupported
            signature:
              type: object
              properties:
                family:
                  type: string
                  enum:
                    - textile
                    - tile
                    - coating
                    - lighting_electrical
                    - acoustic
                    - surface
                    - furniture
                    - sanitaryware
                    - fittings
                    - architectural_hardware
                form:
                  type: string
                  minLength: 1
                reason:
                  type: string
                  minLength: 1
              required:
                - family
                - form
                - reason
              additionalProperties: false
            items:
              type: array
              prefixItems: []
              minItems: 0
              maxItems: 0
              items: false
          required:
            - status
            - signature
            - items
          additionalProperties: false
        - type: object
          properties:
            status:
              type: string
              const: insufficient_signature
            signature:
              type: object
              properties:
                family:
                  type: string
                  enum:
                    - textile
                    - tile
                    - coating
                    - lighting_electrical
                    - acoustic
                    - surface
                    - furniture
                    - sanitaryware
                    - fittings
                    - architectural_hardware
                form:
                  type: string
                  minLength: 1
                missingRoles:
                  minItems: 1
                  type: array
                  items:
                    type: string
                    enum:
                      - primary_material
                      - performance
              required:
                - family
                - form
                - missingRoles
              additionalProperties: false
            items:
              type: array
              prefixItems: []
              minItems: 0
              maxItems: 0
              items: false
          required:
            - status
            - signature
            - items
          additionalProperties: false
      type: object
    CoordinateMaterialsResponse:
      type: object
      properties:
        seeds:
          type: array
          items:
            type: object
            properties:
              variantId:
                type: string
              brand:
                type: string
              product:
                type: string
              color:
                anyOf:
                  - type: string
                  - type: "null"
              variantName:
                anyOf:
                  - type: string
                  - type: "null"
                description: The variant's own true name (paint colourway names live here).
              imageUrl:
                anyOf:
                  - type: string
                  - type: "null"
              brandSlug:
                type: string
              productId:
                type: string
              resolvedFrom:
                type: string
                description: The input reference this seed resolved from.
            required:
              - variantId
              - brand
              - product
              - color
              - variantName
              - imageUrl
              - brandSlug
              - productId
              - resolvedFrom
            additionalProperties: false
        unresolved:
          type: array
          items:
            type: string
          description: References that did not resolve to any variant.
        window:
          type: string
          const: trailing_36mo
          description: The demand window this endpoint serves.
        companions:
          type: array
          items:
            type: object
            properties:
              variantId:
                type: string
              brand:
                type: string
              product:
                type: string
              color:
                anyOf:
                  - type: string
                  - type: "null"
              variantName:
                anyOf:
                  - type: string
                  - type: "null"
                description: The variant's own true name (paint colourway names live here).
              imageUrl:
                anyOf:
                  - type: string
                  - type: "null"
              brandSlug:
                type: string
              productId:
                type: string
              totalLift:
                type: number
                description: Summed lift across the seeds this companion co-specifies with.
              liftLabel:
                type: string
              seedMatchCount:
                type: integer
                minimum: -9007199254740991
                maximum: 9007199254740991
                description: Number of the input seeds this companion co-specifies with.
              seedContributions:
                type: array
                items:
                  type: object
                  properties:
                    seedVariantId:
                      type: string
                    lift:
                      type: number
                  required:
                    - seedVariantId
                    - lift
                  additionalProperties: false
              demand:
                anyOf:
                  - type: object
                    properties:
                      specCount:
                        type: number
                      projectCount:
                        type: number
                      momentum:
                        anyOf:
                          - type: object
                            properties:
                              state:
                                type: string
                                enum:
                                  - rising
                                  - fading
                                  - stable
                                  - emerging
                                  - insufficient_volume
                              percentChange:
                                anyOf:
                                  - type: number
                                  - type: "null"
                                description: Null for `emerging` (no prior base) and `insufficient_volume`.
                              trailingSpecCount:
                                type: number
                              priorSpecCount:
                                type: number
                            required:
                              - state
                              - percentChange
                              - trailingSpecCount
                              - priorSpecCount
                            additionalProperties: false
                          - type: "null"
                    required:
                      - specCount
                      - projectCount
                      - momentum
                    additionalProperties: false
                  - type: "null"
              swatches:
                type: array
                items:
                  type: object
                  properties:
                    hex:
                      type: string
                    proportion:
                      type: number
                  required:
                    - hex
                    - proportion
                  additionalProperties: false
              colourFit:
                anyOf:
                  - type: number
                  - type: "null"
                description: Anchor-relative appearance similarity between the seed and
                  companion palettes. v1 is annotate-only — never reorders.
            required:
              - variantId
              - brand
              - product
              - color
              - variantName
              - imageUrl
              - brandSlug
              - productId
              - totalLift
              - liftLabel
              - seedMatchCount
              - seedContributions
              - demand
              - swatches
              - colourFit
            additionalProperties: false
      required:
        - seeds
        - unresolved
        - window
        - companions
      additionalProperties: false
    MaterialDemandResult:
      type: object
      properties:
        found:
          type: boolean
        query:
          type: string
        seed:
          anyOf:
            - type: object
              properties:
                variantId:
                  type: string
                brand:
                  type: string
                product:
                  type: string
                color:
                  anyOf:
                    - type: string
                    - type: "null"
                variantName:
                  anyOf:
                    - type: string
                    - type: "null"
                  description: The variant's own true name (holds the paint colour name); null
                    when unnamed.
                imageUrl:
                  anyOf:
                    - type: string
                    - type: "null"
                brandSlug:
                  type: string
                productId:
                  type: string
              required:
                - variantId
                - brand
                - product
                - color
                - variantName
                - imageUrl
                - brandSlug
                - productId
              additionalProperties: false
            - type: "null"
        window:
          type: string
          const: trailing_36mo
          description: The demand window v1 serves (trailing 36 months).
        demand:
          anyOf:
            - type: object
              properties:
                specCount:
                  type: integer
                  minimum: -9007199254740991
                  maximum: 9007199254740991
                projectCount:
                  type: integer
                  minimum: -9007199254740991
                  maximum: 9007199254740991
                customerCount:
                  type: integer
                  minimum: -9007199254740991
                  maximum: 9007199254740991
                byQuarter:
                  type: array
                  items:
                    type: object
                    properties:
                      quarter:
                        type: string
                      specCount:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                      projectCount:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                    required:
                      - quarter
                      - specCount
                      - projectCount
                    additionalProperties: false
                momentum:
                  type: object
                  properties:
                    state:
                      type: string
                      enum:
                        - rising
                        - fading
                        - stable
                        - emerging
                        - insufficient_volume
                    percentChange:
                      anyOf:
                        - type: number
                        - type: "null"
                      description: Null for `emerging` (no prior base) and `insufficient_volume` (no
                        comparison).
                    trailingSpecCount:
                      type: integer
                      minimum: -9007199254740991
                      maximum: 9007199254740991
                    priorSpecCount:
                      type: integer
                      minimum: -9007199254740991
                      maximum: 9007199254740991
                  required:
                    - state
                    - percentChange
                    - trailingSpecCount
                    - priorSpecCount
                  additionalProperties: false
              required:
                - specCount
                - projectCount
                - customerCount
                - byQuarter
                - momentum
              additionalProperties: false
            - type: "null"
          description: Headline counts + quarterly series + momentum; null when no stats
            are tracked.
      required:
        - found
        - query
        - seed
        - window
        - demand
      additionalProperties: false
    CoSpecifiedResult:
      type: object
      properties:
        found:
          type: boolean
        resolution:
          type: string
          enum:
            - resolved
            - ambiguous
            - not_found
          description: Three-valued seed resolution; distinct from `found`.
        query:
          type: string
        seed:
          anyOf:
            - type: object
              properties:
                variantId:
                  type: string
                brand:
                  type: string
                product:
                  type: string
                color:
                  anyOf:
                    - type: string
                    - type: "null"
                variantName:
                  anyOf:
                    - type: string
                    - type: "null"
                  description: The variant's own true name (holds the paint colour name); null
                    when unnamed.
                imageUrl:
                  anyOf:
                    - type: string
                    - type: "null"
                brandSlug:
                  type: string
                productId:
                  type: string
              required:
                - variantId
                - brand
                - product
                - color
                - variantName
                - imageUrl
                - brandSlug
                - productId
              additionalProperties: false
            - type: "null"
        seedCategory:
          anyOf:
            - type: string
            - type: "null"
        seedAlternatives:
          type: array
          items:
            type: object
            properties:
              variantId:
                type: string
              brand:
                type: string
              product:
                type: string
              color:
                anyOf:
                  - type: string
                  - type: "null"
              variantName:
                anyOf:
                  - type: string
                  - type: "null"
                description: The variant's own true name (holds the paint colour name); null
                  when unnamed.
              imageUrl:
                anyOf:
                  - type: string
                  - type: "null"
              brandSlug:
                type: string
              productId:
                type: string
            required:
              - variantId
              - brand
              - product
              - color
              - variantName
              - imageUrl
              - brandSlug
              - productId
            additionalProperties: false
          description: Best-per-other-brand seeds for the same colour name when steered to
            one brand.
        candidates:
          type: array
          items:
            type: object
            properties:
              variantId:
                type: string
              brand:
                type: string
              product:
                type: string
              color:
                anyOf:
                  - type: string
                  - type: "null"
              variantName:
                anyOf:
                  - type: string
                  - type: "null"
                description: The variant's own true name (holds the paint colour name); null
                  when unnamed.
              imageUrl:
                anyOf:
                  - type: string
                  - type: "null"
              brandSlug:
                type: string
              productId:
                type: string
            required:
              - variantId
              - brand
              - product
              - color
              - variantName
              - imageUrl
              - brandSlug
              - productId
            additionalProperties: false
          description: Best-per-brand candidates when `resolution === "ambiguous"`; empty
            otherwise.
        window:
          type: string
          const: trailing_36mo
          description: The demand window v1 serves (trailing 36 months).
        companions:
          type: array
          items:
            type: object
            properties:
              variantId:
                type: string
              brand:
                type: string
              product:
                type: string
              color:
                anyOf:
                  - type: string
                  - type: "null"
              variantName:
                anyOf:
                  - type: string
                  - type: "null"
                description: The variant's own true name (holds the paint colour name); null
                  when unnamed.
              imageUrl:
                anyOf:
                  - type: string
                  - type: "null"
              brandSlug:
                type: string
              productId:
                type: string
              category:
                anyOf:
                  - type: string
                  - type: "null"
                description: Coarse material category (product-type group), for filtering.
              projectCount:
                type: integer
                minimum: -9007199254740991
                maximum: 9007199254740991
                description: Distinct projects specifying both the seed and this.
              customerCount:
                type: integer
                minimum: -9007199254740991
                maximum: 9007199254740991
              lift:
                type: number
                description: PMI-style co-specification lift.
              liftLabel:
                type: string
                description: Human framing of `lift`, e.g. "4.2× more than average".
              demand:
                anyOf:
                  - type: object
                    properties:
                      specCount:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                      projectCount:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                      momentum:
                        anyOf:
                          - type: object
                            properties:
                              state:
                                type: string
                                enum:
                                  - rising
                                  - fading
                                  - stable
                                  - emerging
                                  - insufficient_volume
                              percentChange:
                                anyOf:
                                  - type: number
                                  - type: "null"
                                description: Null for `emerging` (no prior base) and `insufficient_volume` (no
                                  comparison).
                              trailingSpecCount:
                                type: integer
                                minimum: -9007199254740991
                                maximum: 9007199254740991
                              priorSpecCount:
                                type: integer
                                minimum: -9007199254740991
                                maximum: 9007199254740991
                            required:
                              - state
                              - percentChange
                              - trailingSpecCount
                              - priorSpecCount
                            additionalProperties: false
                          - type: "null"
                    required:
                      - specCount
                      - projectCount
                      - momentum
                    additionalProperties: false
                  - type: "null"
              topProjectTypes:
                type: array
                items:
                  type: object
                  properties:
                    projectType:
                      type: string
                    projectCount:
                      type: integer
                      minimum: -9007199254740991
                      maximum: 9007199254740991
                  required:
                    - projectType
                    - projectCount
                  additionalProperties: false
            required:
              - variantId
              - brand
              - product
              - color
              - variantName
              - imageUrl
              - brandSlug
              - productId
              - category
              - projectCount
              - customerCount
              - lift
              - liftLabel
              - demand
              - topProjectTypes
            additionalProperties: false
      required:
        - found
        - resolution
        - query
        - seed
        - seedCategory
        - seedAlternatives
        - candidates
        - window
        - companions
      additionalProperties: false
    EnrichCandidateDemandResponse:
      type: object
      properties:
        generationId:
          type: string
          description: The completed ingestion-session id these counts were mined in (the
            dataset generation).
        metric:
          type: string
          const: specification_demand
        universe:
          type: object
          properties:
            window:
              type: string
              const: trailing_36mo
            signal:
              type: string
              enum:
                - sampled
                - considered
                - specified
            description:
              type: string
          required:
            - window
            - signal
            - description
          additionalProperties: false
          description: The metric universe — window and signal the counts are scoped to.
        guardrail:
          type: object
          properties:
            audience:
              type: string
              enum:
                - public
                - internal
            minProjectCount:
              type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
              description: Quality floor applied for this signal.
            minCustomerCount:
              type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
              description: Privacy floor applied for this signal.
          required:
            - audience
            - minProjectCount
            - minCustomerCount
          additionalProperties: false
          description: The guardrail status under which `results` were projected.
        coverage:
          type: object
          properties:
            requested:
              type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
            available:
              type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
            insufficientData:
              type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
            noSignal:
              type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
            unknownVariant:
              type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
          required:
            - requested
            - available
            - insufficientData
            - noSignal
            - unknownVariant
          additionalProperties: false
          description: Outcome tallies over `results` — never a denominator for the metric.
        context:
          anyOf:
            - type: object
              propertyNames:
                type: string
              additionalProperties:
                type: string
            - type: "null"
          description: The echoed declared context, or null when none was supplied.
        results:
          type: array
          items:
            oneOf:
              - type: object
                properties:
                  variantId:
                    type: string
                  state:
                    type: string
                    const: available
                  population:
                    type: object
                    properties:
                      specCount:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                        description: Order lines (master-order grain) specifying this variant in the
                          window.
                      projectCount:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                        description: Distinct projects specifying this variant.
                      customerCount:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                        description: Distinct customers specifying this variant.
                    required:
                      - specCount
                      - projectCount
                      - customerCount
                    additionalProperties: false
                  intervals:
                    type: array
                    items:
                      type: object
                      properties:
                        quarter:
                          type: string
                          description: "`YYYY-Q#` quarter label."
                        specCount:
                          type: integer
                          minimum: -9007199254740991
                          maximum: 9007199254740991
                        projectCount:
                          type: integer
                          minimum: -9007199254740991
                          maximum: 9007199254740991
                      required:
                        - quarter
                        - specCount
                        - projectCount
                      additionalProperties: false
                    description: Exact per-quarter counts (sparse — zero-spec quarters are absent).
                  momentum:
                    type: object
                    properties:
                      state:
                        type: string
                        enum:
                          - rising
                          - fading
                          - stable
                          - emerging
                          - insufficient_volume
                      percentChange:
                        anyOf:
                          - type: number
                          - type: "null"
                        description: Null for `emerging` (no prior base) and `insufficient_volume` (no
                          comparison).
                      trailingSpecCount:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                      priorSpecCount:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                    required:
                      - state
                      - percentChange
                      - trailingSpecCount
                      - priorSpecCount
                    additionalProperties: false
                  funnel:
                    anyOf:
                      - type: object
                        properties:
                          consideredProjects:
                            type: integer
                            minimum: -9007199254740991
                            maximum: 9007199254740991
                          specifiedProjects:
                            type: integer
                            minimum: -9007199254740991
                            maximum: 9007199254740991
                          excludedCount:
                            type: integer
                            minimum: -9007199254740991
                            maximum: 9007199254740991
                          exclusionRate:
                            type: number
                        required:
                          - consideredProjects
                          - specifiedProjects
                          - excludedCount
                          - exclusionRate
                        additionalProperties: false
                      - type: "null"
                    description: The consideration→specification funnel (project-grain, both
                      audiences); null when the signal is not `sampled` or no
                      `considered` row/session exists.
                  evidence:
                    anyOf:
                      - type: object
                        properties:
                          belowPrivacyFloor:
                            type: boolean
                            description: Fewer distinct customers than the privacy floor — hidden from
                              public callers.
                          belowQualityFloor:
                            type: boolean
                            description: Fewer distinct projects than the quality floor — too thin for a
                              public metric.
                          suppressedForPublic:
                            type: boolean
                            description: True when a public caller would receive `no_signal` for this
                              variant (privacy suppression).
                        required:
                          - belowPrivacyFloor
                          - belowQualityFloor
                          - suppressedForPublic
                        additionalProperties: false
                      - type: "null"
                    description: Exact guardrail evidence for `internal` callers; null for public.
                required:
                  - variantId
                  - state
                  - population
                  - intervals
                  - momentum
                  - funnel
                  - evidence
                additionalProperties: false
              - type: object
                properties:
                  variantId:
                    type: string
                  state:
                    type: string
                    const: insufficient_data
                required:
                  - variantId
                  - state
                additionalProperties: false
              - type: object
                properties:
                  variantId:
                    type: string
                  state:
                    type: string
                    const: no_signal
                required:
                  - variantId
                  - state
                additionalProperties: false
              - type: object
                properties:
                  variantId:
                    type: string
                  state:
                    type: string
                    const: unknown_variant
                required:
                  - variantId
                  - state
                additionalProperties: false
            type: object
      required:
        - generationId
        - metric
        - universe
        - guardrail
        - coverage
        - context
        - results
      additionalProperties: false
    CodeAdvisoryResponse:
      type: object
      properties:
        jurisdiction:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
            codeFamily:
              type: string
            edition:
              type: string
            adoptionNote:
              type: string
          required:
            - id
            - name
            - codeFamily
            - edition
            - adoptionNote
          additionalProperties: false
        context:
          type: object
          properties:
            occupancyGroup:
              type: string
            spaceType:
              type: string
            sprinklered:
              type: boolean
          required:
            - occupancyGroup
            - spaceType
            - sprinklered
          additionalProperties: false
        caveat:
          type: string
          const: Advisory — model code as adopted, not a legal determination. Always
            verify with the authority having jurisdiction.
        results:
          type: array
          items:
            type: object
            properties:
              variantId:
                type: string
              state:
                type: string
                enum:
                  - evaluated
                  - unknown_variant
              requirements:
                type: array
                items:
                  type: object
                  properties:
                    requirementId:
                      type: string
                    dimension:
                      type: string
                      enum:
                        - fire
                        - floor
                        - slip
                        - decorative
                    section:
                      type: string
                    requirement:
                      type: object
                      properties:
                        scale:
                          type: string
                          enum:
                            - astm_e84
                            - astm_e648
                            - nfpa_701
                            - doc_ff1
                        minClass:
                          anyOf:
                            - type: string
                            - type: "null"
                        numericFloor:
                          anyOf:
                            - type: object
                              properties:
                                metric:
                                  type: string
                                value:
                                  type: number
                                unit:
                                  type: string
                              required:
                                - metric
                                - value
                                - unit
                              additionalProperties: false
                            - type: "null"
                      required:
                        - scale
                        - minClass
                        - numericFloor
                      additionalProperties: false
                    verdict:
                      type: string
                      enum:
                        - meets
                        - shortfall
                        - unknown
                    evidence:
                      anyOf:
                        - type: object
                          properties:
                            attributeId:
                              type: string
                              enum:
                                - fire_classifications
                                - slip_resistance_dcof
                            value:
                              type: string
                            confidence:
                              anyOf:
                                - type: number
                                - type: "null"
                            sourceBasis:
                              anyOf:
                                - type: string
                                - type: "null"
                          required:
                            - attributeId
                            - value
                            - confidence
                            - sourceBasis
                          additionalProperties: false
                        - type: "null"
                    caveat:
                      type: string
                      const: Advisory — model code as adopted, not a legal determination. Always
                        verify with the authority having jurisdiction.
                  required:
                    - requirementId
                    - dimension
                    - section
                    - requirement
                    - verdict
                    - evidence
                    - caveat
                  additionalProperties: false
            required:
              - variantId
              - state
              - requirements
            additionalProperties: false
      required:
        - jurisdiction
        - context
        - caveat
        - results
      additionalProperties: false
    CatalogBySpecResponse:
      type: object
      properties:
        criteria:
          type: array
          items:
            type: object
            properties:
              attribute:
                type: string
              value:
                type: string
            required:
              - attribute
              - value
            additionalProperties: false
          description: The parsed criteria the query ran against.
        totalMatches:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: Size of the returned (limited) sample, not the true corpus total.
        brandsSpanned:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: Distinct brands in the returned sample.
        matches:
          type: array
          items:
            type: object
            properties:
              variantId:
                type: string
              brand:
                type: string
              product:
                type: string
              variant:
                type: string
              color:
                anyOf:
                  - type: string
                  - type: "null"
              imageUrl:
                anyOf:
                  - type: string
                  - type: "null"
                description: R2 thumbnail of the main image, when one exists.
              hex:
                anyOf:
                  - type: string
                  - type: "null"
                description: Dominant colour hex (swatch fallback when there's no image).
              matched:
                type: array
                items:
                  type: object
                  properties:
                    attributeId:
                      type: string
                    confidence:
                      anyOf:
                        - type: number
                        - type: "null"
                    sourceBasis:
                      anyOf:
                        - type: string
                        - type: "null"
                  required:
                    - attributeId
                    - confidence
                    - sourceBasis
                  additionalProperties: false
                description: The cells that satisfied each criterion, with their per-cell trust.
              minConfidence:
                anyOf:
                  - type: number
                  - type: "null"
                description: Weakest-link confidence across the matched cells (0..1), or null.
            required:
              - variantId
              - brand
              - product
              - variant
              - color
              - imageUrl
              - hex
              - matched
              - minConfidence
            additionalProperties: false
          description: Matching variants, photo-first.
        unresolvedCriteria:
          description: Present only when a criterion's value matched no canonical term —
            the query is NOT run. Each carries ranked `resolution.candidates`;
            retry with a candidate term id.
          type: array
          items:
            type: object
            properties:
              attributeId:
                type: string
              value:
                type: string
              resolution:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - resolved
                      - ambiguous
                      - unresolved
                  query:
                    type: string
                  best:
                    anyOf:
                      - type: object
                        properties:
                          id:
                            type: string
                          label:
                            type: string
                          dictionaryId:
                            type: string
                          score:
                            type: number
                          tier:
                            type: string
                            enum:
                              - exact_id
                              - exact_alias
                              - lexical
                              - semantic
                          via:
                            type: string
                            const: colorscope
                        required:
                          - id
                          - label
                          - score
                          - tier
                        additionalProperties: false
                      - type: "null"
                  candidates:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        label:
                          type: string
                        dictionaryId:
                          type: string
                        score:
                          type: number
                        tier:
                          type: string
                          enum:
                            - exact_id
                            - exact_alias
                            - lexical
                            - semantic
                        via:
                          type: string
                          const: colorscope
                      required:
                        - id
                        - label
                        - score
                        - tier
                      additionalProperties: false
                  recommendedAction:
                    type: string
                    enum:
                      - use_best
                      - choose_from_candidates
                      - ask_user
                      - none_found
                required:
                  - status
                  - query
                  - best
                  - candidates
                  - recommendedAction
                additionalProperties: false
            required:
              - attributeId
              - value
              - resolution
            additionalProperties: false
      required:
        - criteria
        - totalMatches
        - brandsSpanned
        - matches
      additionalProperties: false
    ResolveAttributeResponse:
      type: object
      properties:
        query:
          type: string
          description: Echo of the resolved (trimmed) query phrase.
        candidates:
          type: array
          items:
            type: object
            properties:
              attributeId:
                type: string
                description: Canonical registry attribute id (e.g. abrasion_classifications).
              name:
                type: string
                description: Human display name from the registry.
              description:
                type: string
                description: Registry description of the attribute.
              score:
                type: number
                description: Semantic similarity to the query in [0,1] (1 = nearest). Higher is
                  a better match.
              facetable:
                type: boolean
                description: Whether this attribute is a queryable spec facet for
                  search/specifications.
              blockIds:
                type: array
                items:
                  type: string
                description: The spec blocks this attribute appears in.
            required:
              - attributeId
              - name
              - description
              - score
              - facetable
              - blockIds
            additionalProperties: false
          description: Matching attributes, best (nearest) first.
      required:
        - query
        - candidates
      additionalProperties: false
    ColourFacetsResponse:
      type: object
      properties:
        count:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: The total number of terms across all three colour vocabularies.
        dictionaries:
          type: array
          items:
            type: object
            properties:
              attributeId:
                type: string
                enum:
                  - colour_family
                  - colour_mood
                  - colour_undertone
                description: Which colour vocabulary this dictionary describes.
              name:
                type: string
                description: The display name of the colour vocabulary.
              description:
                type: string
                description: A short explanation of what this colour vocabulary captures.
              terms:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: string
                      description: The canonical term identifier used when filtering.
                    label:
                      type: string
                      description: The human-readable name shown in the filter UI.
                    aliases:
                      type: array
                      items:
                        type: string
                      description: Alternative names that mean the same term (e.g. "grey" for gray).
                    variants:
                      type: integer
                      minimum: -9007199254740991
                      maximum: 9007199254740991
                      description: How many material variants are tagged with this term.
                    brands:
                      type: integer
                      minimum: -9007199254740991
                      maximum: 9007199254740991
                      description: How many distinct brands have a material tagged with this term.
                  required:
                    - id
                    - label
                    - aliases
                    - variants
                    - brands
                  additionalProperties: false
                description: The canonical terms, most-used first (by variant count, then term
                  id).
            required:
              - attributeId
              - name
              - description
              - terms
            additionalProperties: false
          description: The colour vocabularies, in the order family, mood, undertone.
      required:
        - count
        - dictionaries
      additionalProperties: false
    CategoryTaxonomyResponse:
      type: object
      properties:
        count:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: Total number of category nodes returned.
        categories:
          type: array
          items:
            type: object
            properties:
              key:
                type: string
                description: Stable identifier for this category, e.g. "tile_surfaces" or
                  "ceramic_porcelain_tile".
              name:
                type: string
                description: Human-readable display name, e.g. "Ceramic and Porcelain Tile".
              parentKey:
                anyOf:
                  - type: string
                  - type: "null"
                description: The key of this node's parent group, or null for a top-level group.
              path:
                type: string
                description: Display-name path from the root, " / " separated, e.g. "Tile
                  Surfaces / Ceramic and Porcelain Tile".
              level:
                type: string
                enum:
                  - group
                  - type
                description: 'Tier of this node: "group" for a top-level category, "type" for a
                  product type within a group.'
            required:
              - key
              - name
              - parentKey
              - path
              - level
            additionalProperties: false
          description: Flat list of every group and product-type node.
      required:
        - count
        - categories
      additionalProperties: false
    ApplicableSpecsResponse:
      type: object
      properties:
        found:
          type: boolean
        productType:
          anyOf:
            - type: object
              properties:
                id:
                  type: string
                name:
                  type: string
              required:
                - id
                - name
              additionalProperties: false
            - type: "null"
        blocks:
          type: array
          items:
            type: object
            properties:
              blockId:
                type: string
              name:
                type: string
              applicability:
                type: string
                enum:
                  - required
                  - recommended
                  - optional
                  - unusual
              attributes:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: string
                    name:
                      type: string
                    valueKind:
                      type: string
                    required:
                      type: boolean
                  required:
                    - id
                    - name
                    - valueKind
                    - required
                  additionalProperties: false
            required:
              - blockId
              - name
              - applicability
              - attributes
            additionalProperties: false
        hint:
          type: string
      required:
        - found
        - productType
        - blocks
      additionalProperties: false
    GlossaryTermResponse:
      type: object
      properties:
        found:
          type: boolean
        term:
          type: string
        match:
          anyOf:
            - oneOf:
                - type: object
                  properties:
                    kind:
                      type: string
                      const: attribute
                    id:
                      type: string
                    name:
                      type: string
                    description:
                      type: string
                    valueKind:
                      type: string
                    unit:
                      type: string
                    facetable:
                      type: boolean
                    comparable:
                      type: boolean
                    dictionaryId:
                      type: string
                    usedInBlocks:
                      type: array
                      items:
                        type: string
                  required:
                    - kind
                    - id
                    - name
                    - description
                    - valueKind
                    - facetable
                    - comparable
                    - usedInBlocks
                  additionalProperties: false
                - type: object
                  properties:
                    kind:
                      type: string
                      const: value
                    id:
                      type: string
                    label:
                      type: string
                    description:
                      type: string
                    aliases:
                      type: array
                      items:
                        type: string
                    dictionaryId:
                      type: string
                    dictionaryName:
                      type: string
                    attributeIds:
                      type: array
                      items:
                        type: string
                  required:
                    - kind
                    - id
                    - label
                    - aliases
                    - dictionaryId
                    - dictionaryName
                    - attributeIds
                  additionalProperties: false
              type: object
            - type: "null"
        alternates:
          type: array
          items:
            type: object
            properties:
              kind:
                type: string
                enum:
                  - attribute
                  - value
              id:
                type: string
              label:
                type: string
            required:
              - kind
              - id
              - label
            additionalProperties: false
      required:
        - found
        - term
        - match
      additionalProperties: false
    BrandProfileResponse:
      type: object
      properties:
        found:
          type: boolean
          description: Whether the name resolved to a brand.
        query:
          type: string
          description: The name that was looked up.
        brand:
          anyOf:
            - type: object
              properties:
                id:
                  type: string
                slug:
                  type: string
                name:
                  type: string
                marketSegment:
                  type: string
                  description: The brand's primary market segment (e.g. textiles, tile).
                headquartersCity:
                  anyOf:
                    - type: string
                    - type: "null"
                  description: HQ city, when on record.
                headquartersCountry:
                  anyOf:
                    - type: string
                    - type: "null"
                  description: HQ country in canonical short form (US, UK, Italy), when on record.
                parentBrand:
                  anyOf:
                    - type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                      required:
                        - id
                        - name
                      additionalProperties: false
                    - type: "null"
                  description: Parent brand / holding company, when this brand has one.
                positioning:
                  anyOf:
                    - type: string
                    - type: "null"
                  description: Price/prestige tier (luxury, premium, mid-market, value,
                    trade-only).
                ownershipType:
                  anyOf:
                    - type: string
                    - type: "null"
                  description: Ownership structure (private, public, family, cooperative,
                    subsidiary).
                foundedYear:
                  anyOf:
                    - type: integer
                      minimum: -9007199254740991
                      maximum: 9007199254740991
                    - type: "null"
                description:
                  anyOf:
                    - type: string
                    - type: "null"
                website:
                  anyOf:
                    - type: string
                    - type: "null"
                  description: The brand's primary website URL, when on record.
              required:
                - id
                - slug
                - name
                - marketSegment
                - headquartersCity
                - headquartersCountry
                - parentBrand
                - positioning
                - ownershipType
                - foundedYear
                - description
                - website
              additionalProperties: false
            - type: "null"
          description: Brand identity, or null when the name did not resolve.
        trade:
          type: object
          propertyNames:
            type: string
          additionalProperties:
            anyOf:
              - type: boolean
              - type: string
          description: Trade / specification signals that ARE on record, keyed by
            canonical attribute id (material_bank_presence, bim_library_url,
            sample_program_url, trade_program_url, cpd_ceu_offerings). Absent
            keys mean we hold no value — do not infer false/absent as a claim.
        links:
          type: array
          items:
            type: object
            properties:
              label:
                type: string
              url:
                type: string
            required:
              - label
              - url
            additionalProperties: false
          description: Labelled resource links on record (press kit, brand guidelines),
            when present.
        alternates:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
            required:
              - id
              - name
            additionalProperties: false
          description: Other brands the name also matched, when ambiguous — the best match
            is in `brand`.
      required:
        - found
        - query
        - brand
        - trade
        - links
        - alternates
      additionalProperties: false
    WhoamiResponse:
      type: object
      properties:
        principal:
          type: string
          description: The resolved principal for the credential (e.g. `apikey:read`).
        scopes:
          type: array
          items:
            type: string
          description: The scopes granted to this credential (e.g. `mg:read`).
      required:
        - principal
        - scopes
      additionalProperties: false
    ResolveValueResponse:
      type: object
      properties:
        attribute:
          type: object
          properties:
            id:
              type: string
              description: The resolved registry attribute id (echo of the path segment).
            name:
              type: string
              description: Human display name from the registry.
            dictionaryId:
              type: string
              description: The attribute's value dictionary id.
          required:
            - id
            - name
            - dictionaryId
          additionalProperties: false
        resolution:
          type: object
          properties:
            status:
              type: string
              enum:
                - resolved
                - ambiguous
                - unresolved
              description: "Caller verdict: `resolved` (act on `best`), `ambiguous`
                (disambiguate), `unresolved` (stop)."
            query:
              type: string
              description: Echo of the resolved (trimmed) query phrase.
            best:
              anyOf:
                - type: object
                  properties:
                    id:
                      type: string
                      description: Canonical term id within the attribute's value dictionary.
                    label:
                      type: string
                      description: Human-readable label for the term.
                    dictionaryId:
                      description: The owning value dictionary's id.
                      type: string
                    score:
                      type: number
                      description: Match confidence in [0,1] (1 = exact). Higher is a better match.
                    tier:
                      type: string
                      enum:
                        - exact_id
                        - exact_alias
                        - lexical
                        - semantic
                      description: Which resolution rung produced the winning score.
                    via:
                      description: Present only for a colour_family candidate resolved via the
                        colorscope fallback.
                      type: string
                      const: colorscope
                  required:
                    - id
                    - label
                    - score
                    - tier
                  additionalProperties: false
                - type: "null"
              description: The leading candidate, or null when nothing was found.
            candidates:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                    description: Canonical term id within the attribute's value dictionary.
                  label:
                    type: string
                    description: Human-readable label for the term.
                  dictionaryId:
                    description: The owning value dictionary's id.
                    type: string
                  score:
                    type: number
                    description: Match confidence in [0,1] (1 = exact). Higher is a better match.
                  tier:
                    type: string
                    enum:
                      - exact_id
                      - exact_alias
                      - lexical
                      - semantic
                    description: Which resolution rung produced the winning score.
                  via:
                    description: Present only for a colour_family candidate resolved via the
                      colorscope fallback.
                    type: string
                    const: colorscope
                required:
                  - id
                  - label
                  - score
                  - tier
                additionalProperties: false
              description: Candidates ranked best-first, capped at `limit`.
            recommendedAction:
              type: string
              enum:
                - use_best
                - choose_from_candidates
                - ask_user
                - none_found
              description: The action the resolver recommends the caller take.
          required:
            - status
            - query
            - best
            - candidates
            - recommendedAction
          additionalProperties: false
      required:
        - attribute
        - resolution
      additionalProperties: false
    VocabularySearchResponse:
      type: object
      properties:
        query:
          type: string
          description: Echo of the resolved (trimmed) query phrase.
        results:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                description: Canonical token — a registry attribute id or a value-dictionary
                  term id.
              label:
                type: string
                description: Human-readable label for the token.
              dictionaryId:
                description: "For a term: the owning value dictionary's id. Absent for an
                  attribute."
                type: string
              score:
                type: number
                description: Match confidence in [0,1] (1 = exact). Higher is a better match.
              tier:
                type: string
                enum:
                  - exact_id
                  - exact_alias
                  - lexical
                  - semantic
                description: Which resolution rung produced the winning score.
              via:
                description: Present only for a colour candidate resolved via the colorscope
                  fallback.
                type: string
                const: colorscope
              kind:
                type: string
                enum:
                  - attribute
                  - term
                description: Which controlled vocabulary the candidate came from.
              attributeIds:
                description: "For a term: the ids of the registry attributes whose value
                  dictionary owns it — resolve values against them via
                  attributes/{attributeId}/values/resolve. Omitted for an
                  attribute."
                type: array
                items:
                  type: string
            required:
              - id
              - label
              - score
              - tier
              - kind
            additionalProperties: false
          description: Matching vocabulary tokens, best (nearest) first.
      required:
        - query
        - results
      additionalProperties: false
    SpecFacetsListResponse:
      type: object
      properties:
        mode:
          type: string
          const: facets
          description: The complete live specification-facet collection.
        count:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: Number of facets returned.
        facets:
          type: array
          items:
            type: object
            properties:
              attributeId:
                type: string
                description: Canonical attribute id — query it in search/specifications as
                  `attribute:value`.
              name:
                type: string
                description: Human-readable attribute name.
              isArray:
                type: boolean
                description: Whether the attribute holds multiple values per variant.
              brands:
                type: integer
                minimum: -9007199254740991
                maximum: 9007199254740991
                description: Brands carrying any value for this attribute.
              values:
                type: array
                items:
                  type: object
                  properties:
                    value:
                      type: string
                      description: A canonical value token queryable in search/specifications.
                    variants:
                      type: integer
                      minimum: -9007199254740991
                      maximum: 9007199254740991
                      description: Variants carrying this value.
                    brands:
                      type: integer
                      minimum: -9007199254740991
                      maximum: 9007199254740991
                      description: Distinct brands carrying this value.
                    label:
                      description: Human label for the value, when one is known (e.g. certification
                        programmes).
                      type: string
                  required:
                    - value
                    - variants
                    - brands
                  additionalProperties: false
                description: Top canonical values, most-cross-brand first.
              description:
                description: How the facet is queried (set for virtual facets like certification
                  programmes).
                type: string
            required:
              - attributeId
              - name
              - isArray
              - brands
              - values
            additionalProperties: false
          description: Queryable specification facets, most-cross-brand first.
      required:
        - mode
        - count
        - facets
      additionalProperties: false
    SpecFacetsDistributionResponse:
      type: object
      properties:
        mode:
          type: string
          const: distribution
          description: A single attribute's live value distribution.
        attributeId:
          type: string
          description: The canonical attribute id named by the path.
        count:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: Number of values returned.
        values:
          type: array
          items:
            type: object
            properties:
              value:
                type: string
                description: A canonical value token queryable in search/specifications.
              variants:
                type: integer
                minimum: -9007199254740991
                maximum: 9007199254740991
                description: Variants carrying this value.
              brands:
                type: integer
                minimum: -9007199254740991
                maximum: 9007199254740991
                description: Distinct brands carrying this value.
              label:
                description: Human label for the value, when one is known (e.g. certification
                  programmes).
                type: string
            required:
              - value
              - variants
              - brands
            additionalProperties: false
          description: The attribute's top values in the product graph, most-cross-brand
            first.
      required:
        - mode
        - attributeId
        - count
        - values
      additionalProperties: false
    BrandCatalogListPage:
      type: object
      properties:
        items:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              slug:
                type: string
              name:
                type: string
              primaryMarketSegment:
                type: string
              homepageUrl:
                anyOf:
                  - type: string
                  - type: "null"
              productCount:
                type: integer
                minimum: 0
                maximum: 9007199254740991
              variantCount:
                type: integer
                minimum: 0
                maximum: 9007199254740991
            required:
              - id
              - slug
              - name
              - primaryMarketSegment
              - homepageUrl
              - productCount
              - variantCount
            additionalProperties: false
        meta:
          type: object
          properties:
            count:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            total:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            limit:
              type: integer
              minimum: 1
              maximum: 250
            offset:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            hasMore:
              type: boolean
          required:
            - count
            - total
            - limit
            - offset
            - hasMore
          additionalProperties: false
      required:
        - items
        - meta
      additionalProperties: false
    BrandDiscoveryRunStreamFrame:
      anyOf:
        - type: object
          properties:
            kind:
              type: string
              const: run
            runId:
              type: string
            href:
              type: string
          required:
            - kind
            - runId
            - href
          additionalProperties: false
        - oneOf:
            - type: object
              properties:
                kind:
                  type: string
                  const: stage
                stepId:
                  type: string
                label:
                  type: string
                status:
                  type: string
                  enum:
                    - started
                    - progress
                    - completed
                    - skipped
                    - failed
                detail:
                  type: string
                counts:
                  type: object
                  propertyNames:
                    type: string
                  additionalProperties:
                    anyOf:
                      - type: number
                      - type: string
                      - type: "null"
              required:
                - kind
                - stepId
                - label
                - status
              additionalProperties: false
            - type: object
              properties:
                homepageUrl:
                  type: string
                summary:
                  type: object
                  properties:
                    totalCandidates:
                      type: number
                    groupedCandidates:
                      type: number
                    catalogRootCount:
                      type: number
                    productCount:
                      type: number
                    documentCount:
                      type: number
                    contextCount:
                      type: number
                  required:
                    - totalCandidates
                    - groupedCandidates
                    - catalogRootCount
                    - productCount
                    - documentCount
                    - contextCount
                  additionalProperties: false
                groups:
                  type: array
                  items:
                    type: object
                    properties:
                      kind:
                        type: string
                      label:
                        type: string
                      count:
                        type: number
                      items:
                        type: array
                        items:
                          type: object
                          properties:
                            url:
                              type: string
                            title:
                              type: string
                          required:
                            - url
                          additionalProperties: false
                    required:
                      - kind
                      - label
                      - count
                      - items
                    additionalProperties: false
                kind:
                  type: string
                  const: structure
              required:
                - homepageUrl
                - summary
                - groups
                - kind
              additionalProperties: false
            - type: object
              properties:
                kind:
                  type: string
                  const: error
                message:
                  type: string
              required:
                - kind
                - message
              additionalProperties: false
          type: object
    BrandDiscoveryRunSnapshot:
      type: object
      properties:
        completedAt:
          anyOf:
            - type: string
            - type: "null"
        createdAt:
          type: string
        errorMessage:
          anyOf:
            - type: string
            - type: "null"
        events:
          type: array
          items:
            oneOf:
              - type: object
                properties:
                  kind:
                    type: string
                    const: stage
                  stepId:
                    type: string
                  label:
                    type: string
                  status:
                    type: string
                    enum:
                      - started
                      - progress
                      - completed
                      - skipped
                      - failed
                  detail:
                    type: string
                  counts:
                    type: object
                    propertyNames:
                      type: string
                    additionalProperties:
                      anyOf:
                        - type: number
                        - type: string
                        - type: "null"
                required:
                  - kind
                  - stepId
                  - label
                  - status
                additionalProperties: false
              - type: object
                properties:
                  homepageUrl:
                    type: string
                  summary:
                    type: object
                    properties:
                      totalCandidates:
                        type: number
                      groupedCandidates:
                        type: number
                      catalogRootCount:
                        type: number
                      productCount:
                        type: number
                      documentCount:
                        type: number
                      contextCount:
                        type: number
                    required:
                      - totalCandidates
                      - groupedCandidates
                      - catalogRootCount
                      - productCount
                      - documentCount
                      - contextCount
                    additionalProperties: false
                  groups:
                    type: array
                    items:
                      type: object
                      properties:
                        kind:
                          type: string
                        label:
                          type: string
                        count:
                          type: number
                        items:
                          type: array
                          items:
                            type: object
                            properties:
                              url:
                                type: string
                              title:
                                type: string
                            required:
                              - url
                            additionalProperties: false
                      required:
                        - kind
                        - label
                        - count
                        - items
                      additionalProperties: false
                  kind:
                    type: string
                    const: structure
                required:
                  - homepageUrl
                  - summary
                  - groups
                  - kind
                additionalProperties: false
              - type: object
                properties:
                  kind:
                    type: string
                    const: error
                  message:
                    type: string
                required:
                  - kind
                  - message
                additionalProperties: false
            type: object
        homepageUrl:
          type: string
        href:
          type: string
        runId:
          type: string
        startedAt:
          anyOf:
            - type: string
            - type: "null"
        status:
          type: string
          enum:
            - running
            - completed
            - failed
        structure:
          anyOf:
            - type: object
              properties:
                homepageUrl:
                  type: string
                summary:
                  type: object
                  properties:
                    totalCandidates:
                      type: number
                    groupedCandidates:
                      type: number
                    catalogRootCount:
                      type: number
                    productCount:
                      type: number
                    documentCount:
                      type: number
                    contextCount:
                      type: number
                  required:
                    - totalCandidates
                    - groupedCandidates
                    - catalogRootCount
                    - productCount
                    - documentCount
                    - contextCount
                  additionalProperties: false
                groups:
                  type: array
                  items:
                    type: object
                    properties:
                      kind:
                        type: string
                      label:
                        type: string
                      count:
                        type: number
                      items:
                        type: array
                        items:
                          type: object
                          properties:
                            url:
                              type: string
                            title:
                              type: string
                          required:
                            - url
                          additionalProperties: false
                    required:
                      - kind
                      - label
                      - count
                      - items
                    additionalProperties: false
              required:
                - homepageUrl
                - summary
                - groups
              additionalProperties: false
            - type: "null"
      required:
        - completedAt
        - createdAt
        - errorMessage
        - events
        - homepageUrl
        - href
        - runId
        - startedAt
        - status
        - structure
      additionalProperties: false
    BrandEnrichmentStreamEvent:
      oneOf:
        - type: object
          properties:
            kind:
              type: string
              const: step
            id:
              type: string
            label:
              type: string
            detail:
              type: string
            status:
              type: string
              enum:
                - running
                - done
                - failed
          required:
            - kind
            - id
            - label
            - status
          additionalProperties: false
        - type: object
          properties:
            kind:
              type: string
              const: reasoning
            delta:
              type: string
          required:
            - kind
            - delta
          additionalProperties: false
        - type: object
          properties:
            kind:
              type: string
              const: result
            result:
              oneOf:
                - type: object
                  properties:
                    ok:
                      type: boolean
                      const: true
                    value:
                      anyOf:
                        - type: string
                        - type: "null"
                    source:
                      type: string
                    confidence:
                      type: number
                    rationale:
                      type: string
                  required:
                    - ok
                    - value
                    - source
                    - confidence
                    - rationale
                  additionalProperties: false
                - type: object
                  properties:
                    ok:
                      type: boolean
                      const: false
                    error:
                      type: string
                  required:
                    - ok
                    - error
                  additionalProperties: false
              type: object
          required:
            - kind
            - result
          additionalProperties: false
        - type: object
          properties:
            kind:
              type: string
              const: error
            message:
              type: string
          required:
            - kind
            - message
          additionalProperties: false
      type: object
    MaterialBlockProjection:
      type: object
      properties:
        variantId:
          type: string
        blocks:
          type: array
          items:
            type: object
            properties:
              canonicalBlockId:
                type: string
              canonicalBlockName:
                type: string
              status:
                type: string
                enum:
                  - populated
                  - partial
                  - pending
                  - not_applicable
              fields:
                type: array
                items:
                  type: object
                  properties:
                    attributeId:
                      type: string
                    label:
                      type: string
                    value:
                      anyOf:
                        - type: string
                        - type: number
                        - type: boolean
                        - type: array
                          items: {}
                        - type: object
                          propertyNames:
                            type: string
                          additionalProperties: {}
                        - type: "null"
                    unit:
                      type: string
                    status:
                      type: string
                      enum:
                        - verified
                        - partial
                        - missing
                        - not_applicable
                  required:
                    - attributeId
                    - label
                    - value
                    - status
                  additionalProperties: false
              missingAttributeIds:
                type: array
                items:
                  type: string
            required:
              - canonicalBlockId
              - canonicalBlockName
              - status
              - fields
              - missingAttributeIds
            additionalProperties: false
      required:
        - variantId
        - blocks
      additionalProperties: false
    MaterialAssetProjection:
      type: object
      properties:
        variantId:
          type: string
        images:
          type: array
          items:
            type: object
            properties:
              key:
                type: string
              ordinal:
                type: integer
                minimum: -9007199254740991
                maximum: 9007199254740991
              proxyPath:
                type: string
              visualRole:
                type: string
                enum:
                  - grid
                  - detail
                  - scene
                  - swatch
                  - technical
                  - unknown
              status:
                type: string
                enum:
                  - downloaded
                  - linked_not_downloaded
                  - failed
              contentType:
                anyOf:
                  - type: string
                  - type: "null"
              fileExtension:
                anyOf:
                  - type: string
                  - type: "null"
              filename:
                anyOf:
                  - type: string
                  - type: "null"
              byteSize:
                anyOf:
                  - type: number
                  - type: "null"
            required:
              - key
              - ordinal
              - proxyPath
              - visualRole
              - status
              - contentType
              - fileExtension
              - filename
              - byteSize
            additionalProperties: false
        downloads:
          type: array
          items:
            type: object
            properties:
              ordinal:
                type: integer
                minimum: -9007199254740991
                maximum: 9007199254740991
              url:
                type: string
              label:
                anyOf:
                  - type: string
                  - type: "null"
              assetKind:
                type: string
              status:
                type: string
              contentType:
                anyOf:
                  - type: string
                  - type: "null"
              fileExtension:
                anyOf:
                  - type: string
                  - type: "null"
              filename:
                anyOf:
                  - type: string
                  - type: "null"
            required:
              - ordinal
              - url
              - label
              - assetKind
              - status
              - contentType
              - fileExtension
              - filename
            additionalProperties: false
        summary:
          type: object
          properties:
            imageCount:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            downloadCount:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            downloadedImageCount:
              type: integer
              minimum: 0
              maximum: 9007199254740991
          required:
            - imageCount
            - downloadCount
            - downloadedImageCount
          additionalProperties: false
      required:
        - variantId
        - images
        - downloads
        - summary
      additionalProperties: false
    SchemaBlocksResponse:
      type: object
      properties:
        kind:
          type: string
          const: blocks
        schemaVersion:
          type: string
        items:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              semanticId:
                type: string
              lifecycle:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - draft
                      - active
                      - deprecated
                  introducedIn:
                    type: string
                  deprecatedIn:
                    type: string
                  supersededBy:
                    type: string
                required:
                  - status
                  - introducedIn
                additionalProperties: false
              name:
                type: string
              description:
                type: string
              attributeIds:
                type: array
                items:
                  type: string
            required:
              - id
              - semanticId
              - lifecycle
              - name
              - description
              - attributeIds
            additionalProperties: false
        totalCount:
          type: integer
          minimum: 0
          maximum: 9007199254740991
      required:
        - kind
        - schemaVersion
        - items
        - totalCount
      additionalProperties: false
    SchemaAttributesResponse:
      type: object
      properties:
        kind:
          type: string
          const: attributes
        schemaVersion:
          type: string
        items:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              semanticId:
                type: string
              lifecycle:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - draft
                      - active
                      - deprecated
                  introducedIn:
                    type: string
                  deprecatedIn:
                    type: string
                  supersededBy:
                    type: string
                required:
                  - status
                  - introducedIn
                additionalProperties: false
              name:
                type: string
              description:
                type: string
              valueKind:
                type: string
                enum:
                  - string
                  - text
                  - integer
                  - number
                  - boolean
                  - enum
                  - object
                  - reference
              quantityKind:
                anyOf:
                  - type: string
                    enum:
                      - length
                      - area
                      - volume
                      - mass
                      - time
                      - temperature
                      - currency
                      - ratio
                      - density
                      - coverage
                      - emissions
                      - force
                      - power
                      - voltage
                      - current
                      - frequency
                      - luminous_flux
                      - color_temperature
                      - count
                      - unitless
                  - type: "null"
              canonicalUnitId:
                anyOf:
                  - type: string
                  - type: "null"
              acceptedUnitIds:
                type: array
                items:
                  type: string
              valueDictionaryId:
                anyOf:
                  - type: string
                  - type: "null"
              defaultSourceRequirementId:
                anyOf:
                  - type: string
                  - type: "null"
              externalMappings:
                type: array
                items:
                  type: object
                  properties:
                    target:
                      type: string
                      enum:
                        - schema_org
                        - ifc_pset
                        - bsdd
                        - iso_23387
                        - eclass
                        - gs1_gdm
                        - gdsn
                        - etim
                        - uniclass
                    targetPath:
                      type: string
                    mappingType:
                      type: string
                      enum:
                        - direct
                        - transform
                        - derived
                        - classification
                        - semantic
                    notes:
                      type: string
                  required:
                    - target
                    - targetPath
                    - mappingType
                  additionalProperties: false
              searchFacet:
                type: boolean
              comparable:
                type: boolean
              usedInBlocks:
                type: array
                items:
                  type: string
              enumValues:
                anyOf:
                  - type: array
                    items:
                      type: string
                  - type: "null"
            required:
              - id
              - semanticId
              - lifecycle
              - name
              - description
              - valueKind
              - quantityKind
              - canonicalUnitId
              - acceptedUnitIds
              - valueDictionaryId
              - defaultSourceRequirementId
              - externalMappings
              - searchFacet
              - comparable
              - usedInBlocks
              - enumValues
            additionalProperties: false
        totalCount:
          type: integer
          minimum: 0
          maximum: 9007199254740991
      required:
        - kind
        - schemaVersion
        - items
        - totalCount
      additionalProperties: false
    SchemaVerticalsResponse:
      type: object
      properties:
        kind:
          type: string
          const: verticals
        schemaVersion:
          type: string
        items:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              displayName:
                type: string
              description:
                type: string
            required:
              - id
              - displayName
              - description
            additionalProperties: false
        totalCount:
          type: integer
          minimum: 0
          maximum: 9007199254740991
      required:
        - kind
        - schemaVersion
        - items
        - totalCount
      additionalProperties: false
    SchemaProductTypesResponse:
      type: object
      properties:
        kind:
          type: string
          const: productTypes
        schemaVersion:
          type: string
        items:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              description:
                type: string
              verticalId:
                type: string
              parentProductTypeGroupId:
                type: string
              blocks:
                type: array
                items:
                  type: object
                  properties:
                    blockId:
                      type: string
                    applicability:
                      type: string
                      enum:
                        - required
                        - recommended
                        - optional
                        - unusual
                    requiredAttributes:
                      type: array
                      items:
                        type: string
                    optionalAttributes:
                      type: array
                      items:
                        type: string
                    sourceRequirementId:
                      type: string
                    completenessWeight:
                      type: number
                    notes:
                      type: string
                    optional:
                      type: boolean
                  required:
                    - blockId
                    - applicability
                  additionalProperties: false
              productTypeSpecificAttributes:
                type: array
                items:
                  type: string
              semanticId:
                type: string
              lifecycle:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - draft
                      - active
                      - deprecated
                  introducedIn:
                    type: string
                  deprecatedIn:
                    type: string
                  supersededBy:
                    type: string
                required:
                  - status
                  - introducedIn
                additionalProperties: false
            required:
              - id
              - name
              - verticalId
              - blocks
              - semanticId
              - lifecycle
            additionalProperties: false
        totalCount:
          type: integer
          minimum: 0
          maximum: 9007199254740991
      required:
        - kind
        - schemaVersion
        - items
        - totalCount
      additionalProperties: false
    SchemaUnitsResponse:
      type: object
      properties:
        kind:
          type: string
          const: units
        schemaVersion:
          type: string
        items:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              symbol:
                type: string
              quantityKind:
                type: string
                enum:
                  - length
                  - area
                  - volume
                  - mass
                  - time
                  - temperature
                  - currency
                  - ratio
                  - density
                  - coverage
                  - emissions
                  - force
                  - power
                  - voltage
                  - current
                  - frequency
                  - luminous_flux
                  - color_temperature
                  - count
                  - unitless
              acceptedSourceUnits:
                type: array
                items:
                  type: string
              conversionToCanonical:
                type: object
                properties:
                  factor:
                    type: number
                  offset:
                    type: number
                required:
                  - factor
                additionalProperties: false
              notes:
                type: string
              semanticId:
                type: string
              lifecycle:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - draft
                      - active
                      - deprecated
                  introducedIn:
                    type: string
                  deprecatedIn:
                    type: string
                  supersededBy:
                    type: string
                required:
                  - status
                  - introducedIn
                additionalProperties: false
            required:
              - id
              - name
              - symbol
              - quantityKind
              - semanticId
              - lifecycle
            additionalProperties: false
        totalCount:
          type: integer
          minimum: 0
          maximum: 9007199254740991
      required:
        - kind
        - schemaVersion
        - items
        - totalCount
      additionalProperties: false
    SchemaValueDictionariesResponse:
      type: object
      properties:
        kind:
          type: string
          const: valueDictionaries
        schemaVersion:
          type: string
        items:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              description:
                type: string
              terms:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: string
                    label:
                      type: string
                    aliases:
                      type: array
                      items:
                        type: string
                    broaderTermId:
                      type: string
                    externalMappings:
                      type: array
                      items:
                        type: object
                        properties:
                          target:
                            type: string
                            enum:
                              - schema_org
                              - ifc_pset
                              - bsdd
                              - iso_23387
                              - eclass
                              - gs1_gdm
                              - gdsn
                              - etim
                              - uniclass
                          targetPath:
                            type: string
                          mappingType:
                            type: string
                            enum:
                              - direct
                              - transform
                              - derived
                              - classification
                              - semantic
                          notes:
                            type: string
                        required:
                          - target
                          - targetPath
                          - mappingType
                        additionalProperties: false
                    metadata:
                      type: object
                      propertyNames:
                        type: string
                      additionalProperties:
                        anyOf:
                          - type: string
                          - type: number
                          - type: boolean
                          - type: "null"
                    semanticId:
                      type: string
                    lifecycle:
                      type: object
                      properties:
                        status:
                          type: string
                          enum:
                            - draft
                            - active
                            - deprecated
                        introducedIn:
                          type: string
                        deprecatedIn:
                          type: string
                        supersededBy:
                          type: string
                      required:
                        - status
                        - introducedIn
                      additionalProperties: false
                  required:
                    - id
                    - label
                    - semanticId
                    - lifecycle
                  additionalProperties: false
              semanticId:
                type: string
              lifecycle:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - draft
                      - active
                      - deprecated
                  introducedIn:
                    type: string
                  deprecatedIn:
                    type: string
                  supersededBy:
                    type: string
                required:
                  - status
                  - introducedIn
                additionalProperties: false
            required:
              - id
              - name
              - description
              - terms
              - semanticId
              - lifecycle
            additionalProperties: false
        totalCount:
          type: integer
          minimum: 0
          maximum: 9007199254740991
      required:
        - kind
        - schemaVersion
        - items
        - totalCount
      additionalProperties: false
    SchemaSourceRequirementsResponse:
      type: object
      properties:
        kind:
          type: string
          const: sourceRequirements
        schemaVersion:
          type: string
        items:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              description:
                type: string
              allowedSourceTypes:
                type: array
                items:
                  type: string
                  enum:
                    - manufacturer_page
                    - technical_data_sheet
                    - safety_data_sheet
                    - test_report
                    - certificate
                    - image
                    - commerce_page
                    - catalog_pdf
                    - derived_analysis
              allowedMethods:
                type: array
                items:
                  type: string
                  enum:
                    - manual_review
                    - agent_extract
                    - pdf_extract
                    - image_analysis
                    - html_scrape
                    - structured_feed
                    - calculation
              minConfidence:
                type: number
              reviewRequired:
                type: boolean
              freshnessDays:
                type: number
              sourceBasis:
                type: string
                enum:
                  - declared
                  - derived
                  - tested
              semanticId:
                type: string
              lifecycle:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - draft
                      - active
                      - deprecated
                  introducedIn:
                    type: string
                  deprecatedIn:
                    type: string
                  supersededBy:
                    type: string
                required:
                  - status
                  - introducedIn
                additionalProperties: false
            required:
              - id
              - name
              - description
              - allowedSourceTypes
              - allowedMethods
              - minConfidence
              - reviewRequired
              - semanticId
              - lifecycle
            additionalProperties: false
        totalCount:
          type: integer
          minimum: 0
          maximum: 9007199254740991
      required:
        - kind
        - schemaVersion
        - items
        - totalCount
      additionalProperties: false
    SchemaProductTypeTemplatesResponse:
      type: object
      properties:
        kind:
          type: string
          const: productTypeTemplates
        schemaVersion:
          type: string
        items:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              productTypeId:
                type: string
              blocks:
                type: array
                items:
                  type: object
                  properties:
                    blockId:
                      type: string
                    applicability:
                      type: string
                      enum:
                        - required
                        - recommended
                        - optional
                        - unusual
                    requiredAttributes:
                      type: array
                      items:
                        type: string
                    optionalAttributes:
                      type: array
                      items:
                        type: string
                    sourceRequirementId:
                      type: string
                    completenessWeight:
                      type: number
                    notes:
                      type: string
                  required:
                    - blockId
                    - applicability
                  additionalProperties: false
              defaultSourceRequirementId:
                type: string
              notes:
                type: string
              semanticId:
                type: string
              lifecycle:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                      - draft
                      - active
                      - deprecated
                  introducedIn:
                    type: string
                  deprecatedIn:
                    type: string
                  supersededBy:
                    type: string
                required:
                  - status
                  - introducedIn
                additionalProperties: false
            required:
              - id
              - name
              - productTypeId
              - blocks
              - semanticId
              - lifecycle
            additionalProperties: false
        totalCount:
          type: integer
          minimum: 0
          maximum: 9007199254740991
      required:
        - kind
        - schemaVersion
        - items
        - totalCount
      additionalProperties: false
    SchemaMappingsResponse:
      type: object
      properties:
        kind:
          type: string
          const: mappings
        schemaVersion:
          type: string
        items:
          type: array
          items:
            type: object
            properties:
              target:
                type: string
                enum:
                  - schema_org
                  - ifc_pset
                  - bsdd
                  - iso_23387
                  - eclass
                  - gs1_gdm
                  - gdsn
                  - etim
                  - uniclass
              subjectKind:
                type: string
                enum:
                  - attribute
                  - block
                  - product_type
                  - template
                  - unit
                  - value_dictionary
                  - value_dictionary_term
              subjectId:
                type: string
              canonicalPath:
                type: string
              targetPath:
                type: string
              mappingType:
                type: string
                enum:
                  - direct
                  - transform
                  - derived
                  - classification
                  - semantic
              notes:
                type: string
            required:
              - target
              - canonicalPath
              - targetPath
              - mappingType
            additionalProperties: false
        totalCount:
          type: integer
          minimum: 0
          maximum: 9007199254740991
      required:
        - kind
        - schemaVersion
        - items
        - totalCount
      additionalProperties: false
    ExtractRunSnapshot:
      type: object
      properties:
        runId:
          type: string
          minLength: 1
        href:
          type: string
          minLength: 1
        url:
          type: string
          format: uri
        status:
          type: string
          enum:
            - running
            - completed
            - failed
        events:
          type: array
          items:
            type: object
            propertyNames:
              type: string
            additionalProperties: {}
          description: Persisted extract progress events (stage / attribute / media /
            error, …).
        createdAt:
          type: string
        startedAt:
          anyOf:
            - type: string
            - type: "null"
        completedAt:
          anyOf:
            - type: string
            - type: "null"
        errorMessage:
          anyOf:
            - type: string
            - type: "null"
      required:
        - runId
        - href
        - url
        - status
        - events
        - createdAt
        - startedAt
        - completedAt
        - errorMessage
      additionalProperties: false
    VerifyRunSnapshot:
      type: object
      properties:
        runId:
          type: string
          minLength: 1
        status:
          type: string
          enum:
            - running
            - completed
            - failed
        targetId:
          type: string
          minLength: 1
        events:
          type: array
          items:
            oneOf:
              - type: object
                properties:
                  type:
                    type: string
                    const: run
                  runId:
                    type: string
                    minLength: 1
                  scopeLabel:
                    type: string
                required:
                  - type
                  - runId
                additionalProperties: false
              - type: object
                properties:
                  type:
                    type: string
                    const: stage
                  stage:
                    type: string
                    minLength: 1
                  status:
                    type: string
                    enum:
                      - started
                      - done
                      - error
                  detail:
                    type: string
                required:
                  - type
                  - stage
                  - status
                additionalProperties: false
              - type: object
                properties:
                  type:
                    type: string
                    const: attribute
                  attributeId:
                    type: string
                    minLength: 1
                  blockId:
                    type: string
                    minLength: 1
                  blockName:
                    type: string
                required:
                  - type
                  - attributeId
                  - blockId
                additionalProperties: false
              - type: object
                properties:
                  type:
                    type: string
                    const: verdict
                  verdict:
                    type: object
                    properties:
                      attributeId:
                        type: string
                        minLength: 1
                      blockId:
                        type: string
                        minLength: 1
                      storedValue: {}
                      storedConfidence:
                        anyOf:
                          - type: number
                          - type: "null"
                        description: Confidence recorded with the stored value, or null.
                      sourceExcerpt:
                        anyOf:
                          - type: string
                          - type: "null"
                        description: The evidence snippet the verdict is based on, or null.
                      sourceLabel:
                        anyOf:
                          - type: string
                          - type: "null"
                        description: The on-page label the evidence was found under, or null.
                      verdict:
                        type: string
                        enum:
                          - match
                          - discrepancy
                          - unverifiable
                      unverifiableReason:
                        description: Why an 'unverifiable' verdict could not be checked; null/absent for
                          match & discrepancy.
                        anyOf:
                          - type: string
                            enum:
                              - no_source_url
                              - no_match_on_page
                              - value_derived
                          - type: "null"
                      proposedValue:
                        description: A corrected value when verdict is 'discrepancy', else null.
                      confidence:
                        type: number
                        minimum: 0
                        maximum: 1
                        description: Calibrated confidence 0-1 in this verdict.
                    required:
                      - attributeId
                      - blockId
                      - storedValue
                      - storedConfidence
                      - sourceExcerpt
                      - sourceLabel
                      - verdict
                      - proposedValue
                      - confidence
                    additionalProperties: false
                required:
                  - type
                  - verdict
                additionalProperties: false
              - type: object
                properties:
                  type:
                    type: string
                    const: proposed
                  attributeId:
                    type: string
                    minLength: 1
                  blockId:
                    type: string
                    minLength: 1
                  proposedValue: {}
                  confidence:
                    type: number
                    minimum: 0
                    maximum: 1
                  taskId:
                    description: The runtime review task created for this discrepancy, for
                      deep-linking to /ops/reviews/[taskId]. Optional for
                      back-compat.
                    type: string
                required:
                  - type
                  - attributeId
                  - blockId
                  - proposedValue
                  - confidence
                additionalProperties: false
              - type: object
                properties:
                  type:
                    type: string
                    const: error
                  message:
                    type: string
                required:
                  - type
                  - message
                additionalProperties: false
            type: object
          description: Persisted verify stream events.
        inputJson: {}
        output:
          anyOf:
            - type: object
              propertyNames:
                type: string
              additionalProperties: {}
            - type: "null"
        summary:
          anyOf:
            - {}
            - type: "null"
        createdAt:
          type: string
        startedAt:
          anyOf:
            - type: string
            - type: "null"
        completedAt:
          anyOf:
            - type: string
            - type: "null"
        errorMessage:
          anyOf:
            - type: string
            - type: "null"
      required:
        - runId
        - status
        - targetId
        - events
        - inputJson
        - output
        - summary
        - createdAt
        - startedAt
        - completedAt
        - errorMessage
      additionalProperties: false
    VerifyStreamEvent:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              const: run
            runId:
              type: string
              minLength: 1
            scopeLabel:
              type: string
          required:
            - type
            - runId
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: stage
            stage:
              type: string
              minLength: 1
            status:
              type: string
              enum:
                - started
                - done
                - error
            detail:
              type: string
          required:
            - type
            - stage
            - status
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: attribute
            attributeId:
              type: string
              minLength: 1
            blockId:
              type: string
              minLength: 1
            blockName:
              type: string
          required:
            - type
            - attributeId
            - blockId
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: verdict
            verdict:
              type: object
              properties:
                attributeId:
                  type: string
                  minLength: 1
                blockId:
                  type: string
                  minLength: 1
                storedValue: {}
                storedConfidence:
                  anyOf:
                    - type: number
                    - type: "null"
                  description: Confidence recorded with the stored value, or null.
                sourceExcerpt:
                  anyOf:
                    - type: string
                    - type: "null"
                  description: The evidence snippet the verdict is based on, or null.
                sourceLabel:
                  anyOf:
                    - type: string
                    - type: "null"
                  description: The on-page label the evidence was found under, or null.
                verdict:
                  type: string
                  enum:
                    - match
                    - discrepancy
                    - unverifiable
                unverifiableReason:
                  description: Why an 'unverifiable' verdict could not be checked; null/absent for
                    match & discrepancy.
                  anyOf:
                    - type: string
                      enum:
                        - no_source_url
                        - no_match_on_page
                        - value_derived
                    - type: "null"
                proposedValue:
                  description: A corrected value when verdict is 'discrepancy', else null.
                confidence:
                  type: number
                  minimum: 0
                  maximum: 1
                  description: Calibrated confidence 0-1 in this verdict.
              required:
                - attributeId
                - blockId
                - storedValue
                - storedConfidence
                - sourceExcerpt
                - sourceLabel
                - verdict
                - proposedValue
                - confidence
              additionalProperties: false
          required:
            - type
            - verdict
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: proposed
            attributeId:
              type: string
              minLength: 1
            blockId:
              type: string
              minLength: 1
            proposedValue: {}
            confidence:
              type: number
              minimum: 0
              maximum: 1
            taskId:
              description: The runtime review task created for this discrepancy, for
                deep-linking to /ops/reviews/[taskId]. Optional for back-compat.
              type: string
          required:
            - type
            - attributeId
            - blockId
            - proposedValue
            - confidence
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: error
            message:
              type: string
          required:
            - type
            - message
          additionalProperties: false
      type: object
    ContentCompareResponse:
      type: object
      properties:
        url:
          type: string
          format: uri
        results:
          type: array
          items:
            $ref: "#/components/schemas/ContentProviderResult"
      required:
        - url
        - results
      additionalProperties: false
    ContentProviderResult:
      type: object
      properties:
        provider:
          type: string
          enum:
            - firecrawl
            - exa
            - tavily
        ok:
          type: boolean
        ms:
          type: number
        chars:
          type: number
        excerpt:
          type: string
        title:
          anyOf:
            - type: string
            - type: "null"
        answer:
          anyOf:
            - type: string
            - type: "null"
        error:
          anyOf:
            - type: string
            - type: "null"
      required:
        - provider
        - ok
        - ms
        - chars
        - excerpt
        - title
        - answer
        - error
      additionalProperties: false
    NormalisationProofResponse:
      type: object
      properties:
        entries:
          type: array
          items:
            $ref: "#/components/schemas/NormalisationProofEntry"
      required:
        - entries
      additionalProperties: false
    NormalisationProofEntry:
      type: object
      properties:
        label:
          type: string
        rawAttribute:
          type: string
        canonicalAttribute:
          type: string
        sourceSpellings:
          type: integer
          minimum: 0
          maximum: 9007199254740991
        canonicalTokens:
          type: integer
          minimum: 0
          maximum: 9007199254740991
        spellingSample:
          type: array
          items:
            type: string
        computedAt:
          type: string
          description: ISO timestamp when the underlying stats were measured.
        headline:
          type: object
          properties:
            token:
              type: string
            rawSpelling:
              type: string
            rawMatches:
              type: object
              properties:
                variants:
                  type: integer
                  minimum: 0
                  maximum: 9007199254740991
                brands:
                  type: integer
                  minimum: 0
                  maximum: 9007199254740991
              required:
                - variants
                - brands
              additionalProperties: false
            canonicalMatches:
              type: object
              properties:
                variants:
                  type: integer
                  minimum: 0
                  maximum: 9007199254740991
                brands:
                  type: integer
                  minimum: 0
                  maximum: 9007199254740991
              required:
                - variants
                - brands
              additionalProperties: false
            variantLift:
              type: number
            brandLift:
              type: number
          required:
            - token
            - rawSpelling
            - rawMatches
            - canonicalMatches
            - variantLift
            - brandLift
          additionalProperties: false
      required:
        - label
        - rawAttribute
        - canonicalAttribute
        - sourceSpellings
        - canonicalTokens
        - spellingSample
        - computedAt
        - headline
      additionalProperties: false
    DemandSignalsResponse:
      type: object
      properties:
        query:
          type: string
          minLength: 3
        signal:
          anyOf:
            - $ref: "#/components/schemas/ContextualSignal"
            - type: "null"
      required:
        - query
        - signal
      additionalProperties: false
    ContextualSignal:
      type: object
      properties:
        signal:
          oneOf:
            - type: object
              properties:
                state:
                  type: string
                  const: eligible
                dimension:
                  type: string
                  enum:
                    - category
                    - brand
                    - colour
                key:
                  type: string
                label:
                  type: string
                slug:
                  anyOf:
                    - type: string
                    - type: "null"
                groupKey:
                  anyOf:
                    - type: string
                    - type: "null"
                groupLabel:
                  anyOf:
                    - type: string
                    - type: "null"
                evidence:
                  type: object
                  properties:
                    specLines:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                    materialsCount:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                    projectsLowerBound:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                    customersLowerBound:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                  required:
                    - specLines
                    - materialsCount
                    - projectsLowerBound
                    - customersLowerBound
                  additionalProperties: false
                currentShare:
                  type: number
                priorShare:
                  type: number
                deltaPp:
                  type: number
                direction:
                  type: string
                  enum:
                    - rising
                    - falling
                series:
                  type: array
                  items:
                    type: object
                    properties:
                      quarter:
                        type: string
                      specCount:
                        type: integer
                        minimum: 0
                        maximum: 9007199254740991
                    required:
                      - quarter
                      - specCount
                    additionalProperties: false
              required:
                - state
                - dimension
                - key
                - label
                - slug
                - groupKey
                - groupLabel
                - evidence
                - currentShare
                - priorShare
                - deltaPp
                - direction
                - series
              additionalProperties: false
            - type: object
              properties:
                state:
                  type: string
                  const: insufficient_volume
                dimension:
                  type: string
                  enum:
                    - category
                    - brand
                    - colour
                key:
                  type: string
                label:
                  type: string
                slug:
                  anyOf:
                    - type: string
                    - type: "null"
                groupKey:
                  anyOf:
                    - type: string
                    - type: "null"
                groupLabel:
                  anyOf:
                    - type: string
                    - type: "null"
                evidence:
                  type: object
                  properties:
                    specLines:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                    materialsCount:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                    projectsLowerBound:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                    customersLowerBound:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                  required:
                    - specLines
                    - materialsCount
                    - projectsLowerBound
                    - customersLowerBound
                  additionalProperties: false
                reason:
                  type: string
                  enum:
                    - below_min_spec_lines
                    - below_min_variants
                    - below_customer_floor
                    - below_min_persistence
                    - below_min_effect
              required:
                - state
                - dimension
                - key
                - label
                - slug
                - groupKey
                - groupLabel
                - evidence
                - reason
              additionalProperties: false
            - type: object
              properties:
                state:
                  type: string
                  const: no_persistence
                dimension:
                  type: string
                  enum:
                    - category
                    - brand
                    - colour
                key:
                  type: string
                label:
                  type: string
                slug:
                  anyOf:
                    - type: string
                    - type: "null"
                groupKey:
                  anyOf:
                    - type: string
                    - type: "null"
                groupLabel:
                  anyOf:
                    - type: string
                    - type: "null"
                evidence:
                  type: object
                  properties:
                    specLines:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                    materialsCount:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                    projectsLowerBound:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                    customersLowerBound:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                  required:
                    - specLines
                    - materialsCount
                    - projectsLowerBound
                    - customersLowerBound
                  additionalProperties: false
                reason:
                  type: string
                  enum:
                    - below_min_spec_lines
                    - below_min_variants
                    - below_customer_floor
                    - below_min_persistence
                    - below_min_effect
              required:
                - state
                - dimension
                - key
                - label
                - slug
                - groupKey
                - groupLabel
                - evidence
                - reason
              additionalProperties: false
            - type: object
              properties:
                state:
                  type: string
                  const: below_min_effect
                dimension:
                  type: string
                  enum:
                    - category
                    - brand
                    - colour
                key:
                  type: string
                label:
                  type: string
                slug:
                  anyOf:
                    - type: string
                    - type: "null"
                groupKey:
                  anyOf:
                    - type: string
                    - type: "null"
                groupLabel:
                  anyOf:
                    - type: string
                    - type: "null"
                evidence:
                  type: object
                  properties:
                    specLines:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                    materialsCount:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                    projectsLowerBound:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                    customersLowerBound:
                      type: integer
                      minimum: 0
                      maximum: 9007199254740991
                  required:
                    - specLines
                    - materialsCount
                    - projectsLowerBound
                    - customersLowerBound
                  additionalProperties: false
                reason:
                  type: string
                  enum:
                    - below_min_spec_lines
                    - below_min_variants
                    - below_customer_floor
                    - below_min_persistence
                    - below_min_effect
              required:
                - state
                - dimension
                - key
                - label
                - slug
                - groupKey
                - groupLabel
                - evidence
                - reason
              additionalProperties: false
          type: object
        matchedOn:
          type: string
          enum:
            - category_leaf
            - category_group
            - brand
            - colour
      required:
        - signal
        - matchedOn
      additionalProperties: false
    SubjectChatStreamEvent:
      oneOf:
        - type: object
          properties:
            kind:
              type: string
              const: token
            text:
              type: string
          required:
            - kind
            - text
          additionalProperties: false
        - type: object
          properties:
            kind:
              type: string
              const: activity
            label:
              type: string
            phase:
              type: string
            staged: {}
          required:
            - kind
            - label
            - phase
          additionalProperties: false
        - type: object
          properties:
            kind:
              type: string
              const: done
            text:
              type: string
            finishReason:
              type: string
          required:
            - kind
            - text
          additionalProperties: false
        - type: object
          properties:
            kind:
              type: string
              const: error
            message:
              type: string
          required:
            - kind
            - message
          additionalProperties: false
      type: object
    SubjectOverrideResponse:
      type: object
      properties:
        success:
          type: boolean
          const: true
        status:
          type: string
          enum:
            - proposed
            - accepted
      required:
        - success
        - status
      additionalProperties: false
