{
  "openapi": "3.1.0",
  "info": {
    "title": "UXXRAY API",
    "version": "1.0.0",
    "description": "0-hallucination funnel-teardown API: measured facts + evidence, a deterministic Evidence Coverage Score (patterns.js), never an LLM opinion."
  },
  "servers": [
    {
      "url": "https://api.uxxray.com"
    }
  ],
  "paths": {
    "/api/v1": {
      "get": {
        "summary": "This self-describing API root — endpoint catalog, corpus counts, links",
        "operationId": "getApiRoot",
        "responses": {
          "200": {
            "description": "API descriptor",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/openapi.json": {
      "get": {
        "summary": "OpenAPI 3.1 document for this API",
        "operationId": "getOpenApi",
        "responses": {
          "200": {
            "description": "OpenAPI 3.1 document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/league": {
      "get": {
        "summary": "Ranked list of teardowns (?funnel_type=, ?limit=, ?offset=). Paginated: a full page does NOT mean the whole corpus — check the x-uxxray-has-more response header and follow Link rel=\"next\" until it reads false",
        "operationId": "getLeague",
        "parameters": [
          {
            "name": "funnel_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "ecommerce",
                "lander"
              ]
            },
            "description": "Filter to one funnel type"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5000,
              "default": 50
            },
            "description": "Rows per page, highest score first. This is a PAGE SIZE, not the corpus size"
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            },
            "description": "Rows to skip — use with limit to page through the full corpus"
          }
        ],
        "responses": {
          "200": {
            "description": "League rows, highest score first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/LeagueRow"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/corpus/stats": {
      "get": {
        "summary": "Corpus-wide aggregates (pct_lack, avg_tactics, evidence)",
        "operationId": "getCorpusStats",
        "responses": {
          "200": {
            "description": "Aggregate stats",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CorpusStats"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/patterns": {
      "get": {
        "summary": "The Conversion Pattern Library — every detected tactic, prevalence, quoted evidence, examples (?funnel_type=)",
        "operationId": "getPatterns",
        "parameters": [
          {
            "name": "funnel_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter examples/prevalence to one funnel type"
          }
        ],
        "responses": {
          "200": {
            "description": "The Conversion Pattern Library — every detected tactic, prevalence, quoted evidence, examples (?funnel_type=)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/patterns/{pattern_id}": {
      "get": {
        "summary": "One conversion pattern, machine-readable — up to 60 verbatim on-page examples",
        "operationId": "getPattern",
        "parameters": [
          {
            "name": "pattern_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Pattern id, e.g. \"review_wall\""
          },
          {
            "name": "funnel_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter examples to one funnel type"
          }
        ],
        "responses": {
          "200": {
            "description": "One conversion pattern, machine-readable — up to 60 verbatim on-page examples",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/templates": {
      "get": {
        "summary": "Page Template Library — recurring section-skeletons real pages are built from, clustered by exact structure",
        "operationId": "getTemplates",
        "responses": {
          "200": {
            "description": "Page Template Library — recurring section-skeletons real pages are built from, clustered by exact structure",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sections": {
      "get": {
        "summary": "Section-archetype prevalence across fully-crawled pages — how common each band is, where it typically sits, example sites; ?archetype=X returns every real instance with screenshot crop coords (the SECTION layer)",
        "operationId": "getSections",
        "parameters": [
          {
            "name": "archetype",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Return every real instance of this section archetype (e.g. \"hero\") with crop coords instead of the summary"
          }
        ],
        "responses": {
          "200": {
            "description": "Section-archetype prevalence across fully-crawled pages — how common each band is, where it typically sits, example sites; ?archetype=X returns every real instance with screenshot crop coords (the SECTION layer)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/library/index": {
      "get": {
        "summary": "THE WHOLE SHELF — every facet, every legal value, real counts, no specimens. Call this FIRST. The inventory is lumpy (hero is in the hundreds, stat_proof is single digits) and these counts are how you find that out before asking for 20 and getting 7",
        "operationId": "getLibraryIndex",
        "responses": {
          "200": {
            "description": "THE WHOLE SHELF — every facet, every legal value, real counts, no specimens. Call this FIRST. The inventory is lumpy (hero is in the hundreds, stat_proof is single digits) and these counts are how you find that out before asking for 20 and getting 7",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/library/specimens": {
      "get": {
        "summary": "FACETED SPECIMEN RETRIEVAL — you choose the breadth. scope=narrow (5, full receipts incl. verbatim evidence) | broad (20, the band's cropped image + tactic ids — the DEFAULT) | full (the whole roster for the facet: identity + score only). Select on ONE axis: kind (+archetype/template) OR playbook; funnel_type filters either. Every response publishes how many exist, so \"showing 7 of 7 available\" can never be misread as a page of many. exclude_seen_project stops a later round re-serving what you already built from. A ranked response is ONE specimen per domain by default (score is funnel-scoped, so a 4-page walk would otherwise take 4 slots) and does not rank bands with no possible crop; both reductions are disclosed and per_domain= lifts the cap",
        "operationId": "getLibrarySpecimens",
        "parameters": [
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "narrow",
                "broad",
                "full"
              ],
              "default": "broad"
            },
            "description": "How much of the shelf to see. narrow=5 with full receipts, broad=20 compact (default), full=the roster of every match"
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "page",
                "section",
                "element"
              ],
              "default": "section"
            },
            "description": "What a specimen IS. element is measured but not yet served here — it reports that as a missing capability, never as an empty corpus"
          },
          {
            "name": "archetype",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Required for kind=section — hero | faq | logo_wall | pricing | cta_band | feature_grid | comparison | email_capture | reviews | stat_proof. Legal values + counts at /api/v1/library/index"
          },
          {
            "name": "template",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "kind=page only — an exact section-skeleton signature such as hero>logo_wall>faq"
          },
          {
            "name": "playbook",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The OTHER axis: funnels measurably running one named play, served as page specimens with the play's quoted evidence. Cannot be combined with kind/archetype/template — that returns 400 rather than silently honouring one half"
          },
          {
            "name": "funnel_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Industry filter — composes with either axis. saas | ecommerce | affiliate | lead_gen | content"
          },
          {
            "name": "exclude_seen",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated slug:archetype refs you have already been served"
          },
          {
            "name": "exclude_seen_session",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "A Design Factory session id — excludes every specimen that session recorded in source_specimens"
          },
          {
            "name": "exclude_seen_project",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Excludes every specimen ANY of your sessions used for that project. Requires the x-uxxray-visitor header. This is the one that stops round 3 re-serving round 1"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Override the tier's COUNT without changing its SHAPE (max 500)"
          },
          {
            "name": "per_domain",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "1"
            },
            "description": "How many specimens ONE domain may occupy in the ranked list. Default 1, so N results are N different sites; a positive integer raises it and \"all\" lifts it entirely, for studying a single funnel's full set. An unparseable value is a 400, not a silent default"
          }
        ],
        "responses": {
          "200": {
            "description": "FACETED SPECIMEN RETRIEVAL — you choose the breadth. scope=narrow (5, full receipts incl. verbatim evidence) | broad (20, the band's cropped image + tactic ids — the DEFAULT) | full (the whole roster for the facet: identity + score only). Select on ONE axis: kind (+archetype/template) OR playbook; funnel_type filters either. Every response publishes how many exist, so \"showing 7 of 7 available\" can never be misread as a page of many. exclude_seen_project stops a later round re-serving what you already built from. A ranked response is ONE specimen per domain by default (score is funnel-scoped, so a 4-page walk would otherwise take 4 slots) and does not rank bands with no possible crop; both reductions are disclosed and per_domain= lifts the cap",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/library/resolve": {
      "get": {
        "summary": "DESIGN TASK → CORPUS VOCABULARY. \"build_review_section\" → the section archetypes, measured patterns and ready-made queries to use. Exact-match lookup: an unsupported task is reported as unsupported, never guessed",
        "operationId": "resolveDesignTask",
        "parameters": [
          {
            "name": "task",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "e.g. build_review_section, build_pricing_section, build_hero_with_proof, build_faq_section, build_comparison_section (aliases accepted)"
          },
          {
            "name": "funnel_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "saas | ecommerce | affiliate | lead_gen | content — threaded into the returned queries"
          }
        ],
        "responses": {
          "200": {
            "description": "DESIGN TASK → CORPUS VOCABULARY. \"build_review_section\" → the section archetypes, measured patterns and ready-made queries to use. Exact-match lookup: an unsupported task is reported as unsupported, never guessed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/library/sections": {
      "get": {
        "summary": "SECTION SPECIMENS with receipts — real bands from real funnels, each with crop_url (a real ~1440px webp of the band, or null with a stated reason) plus crop coords onto its full-page screenshot, the funnel's measured /100, and the verbatim on-page evidence for every tactic it runs. Ranked by score. The crops are cut at BUILD time over the listed corpus; scanning a site does not produce them (see steps[].crops_note on a bundle)",
        "operationId": "getSectionSpecimens",
        "parameters": [
          {
            "name": "task",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Resolve a design task and use its primary archetype"
          },
          {
            "name": "archetype",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "hero | reviews | pricing | faq | comparison | logo_wall | stat_proof — required unless ?task= is given"
          },
          {
            "name": "funnel_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Restrict to one measured funnel type"
          },
          {
            "name": "top",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "How many specimens (default 20, max 100)"
          }
        ],
        "responses": {
          "200": {
            "description": "SECTION SPECIMENS with receipts — real bands from real funnels, each with crop_url (a real ~1440px webp of the band, or null with a stated reason) plus crop coords onto its full-page screenshot, the funnel's measured /100, and the verbatim on-page evidence for every tactic it runs. Ranked by score. The crops are cut at BUILD time over the listed corpus; scanning a site does not produce them (see steps[].crops_note on a bundle)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/library/elements": {
      "get": {
        "summary": "ELEMENT SPECIMENS — the atomic UI pieces real funnels are built from (price anchors, ratings, guarantees, no-card notes, countdowns), each a REAL CROPPED IMAGE cut at BUILD time from the funnel's own full-page screenshot, plus the verbatim on-page text of that element. Scanning a site does not produce this material: a walk cuts at most two regions per step and they are a different tier (see steps[].crops_note on a bundle). Unlike section receipts, the text is element-scoped: the detector recorded the rect and the string from the same DOM node on the same crawl. Ranked by the funnel's measured /100",
        "operationId": "getElementSpecimens",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "strikethrough_price | rating | guarantee | no_credit_card | countdown — an unknown type is rejected with the vocabulary, never answered with an empty list"
          },
          {
            "name": "funnel_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "saas | ecommerce | affiliate | lead_gen — restrict to one measured funnel type"
          },
          {
            "name": "top",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "How many specimens (default 20, max 100)"
          }
        ],
        "responses": {
          "200": {
            "description": "ELEMENT SPECIMENS — the atomic UI pieces real funnels are built from (price anchors, ratings, guarantees, no-card notes, countdowns), each a REAL CROPPED IMAGE cut at BUILD time from the funnel's own full-page screenshot, plus the verbatim on-page text of that element. Scanning a site does not produce this material: a walk cuts at most two regions per step and they are a different tier (see steps[].crops_note on a bundle). Unlike section receipts, the text is element-scoped: the detector recorded the rect and the string from the same DOM node on the same crawl. Ranked by the funnel's measured /100",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/library/pages": {
      "get": {
        "summary": "PAGE SPECIMENS — one real crawled page per specimen, grouped by the MEASURED section sequence it shares with others (the shape). Carries the above-the-fold crop, the whole-page screenshot, the section skeleton the shape was clustered on, and the funnel's measured /100. Ranked by score",
        "operationId": "getPageSpecimens",
        "parameters": [
          {
            "name": "shape",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "A clustered page shape, e.g. the-trust-wall, the-pricing-page, hero-+-features — GET without it to list every shape"
          },
          {
            "name": "funnel_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Restrict to one measured funnel type"
          },
          {
            "name": "min_score",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Only pages whose funnel scored at least this"
          },
          {
            "name": "top",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "How many specimens (default 20, max 100)"
          }
        ],
        "responses": {
          "200": {
            "description": "PAGE SPECIMENS — one real crawled page per specimen, grouped by the MEASURED section sequence it shares with others (the shape). Carries the above-the-fold crop, the whole-page screenshot, the section skeleton the shape was clustered on, and the funnel's measured /100. Ranked by score",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/visitors": {
      "post": {
        "summary": "Issue a VISITOR CAPABILITY TOKEN — the continuity layer. It powers YOUR work (factory projects, likes) and is deliberately NOT an analytics identity: view counts deduplicate on a day-scoped IP hash and never read it. Returned ONCE and stored only as a SHA-256 hash, so it cannot be re-read. Send it back as the x-uxxray-visitor header — a header rather than a cookie so agents, which hold config but not cookies, use the same primitive as browsers",
        "operationId": "createVisitor",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Issue a VISITOR CAPABILITY TOKEN — the continuity layer. It powers YOUR work (factory projects, likes) and is deliberately NOT an analytics identity: view counts deduplicate on a day-scoped IP hash and never read it. Returned ONCE and stored only as a SHA-256 hash, so it cannot be re-read. Send it back as the x-uxxray-visitor header — a header rather than a cookie so agents, which hold config but not cookies, use the same primitive as browsers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/visitors/me": {
      "get": {
        "summary": "Your factory projects, resolved from the x-uxxray-visitor header. Returns a completeness note stating what is NOT listed — work from another device, from before the token existed, or created after clearing it is unreachable here and recoverable only by its own review link. Claiming full history would be a fabrication",
        "operationId": "getVisitorMe",
        "responses": {
          "200": {
            "description": "Your factory projects, resolved from the x-uxxray-visitor header. Returns a completeness note stating what is NOT listed — work from another device, from before the token existed, or created after clearing it is unreachable here and recoverable only by its own review link. Claiming full history would be a fabrication",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/library/engagement": {
      "get": {
        "summary": "Batch engagement counts for the specimens on one page. Fetched at RUNTIME and never baked into the static build, because a build-time number is a claim about when the page was generated dressed up as a current one. A ref with no row is OMITTED rather than returned as 0 — never counted and measured-zero are different facts. Carries the definition of the number",
        "operationId": "getEngagementCounts",
        "parameters": [
          {
            "name": "refs",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated slug:archetype refs"
          }
        ],
        "responses": {
          "200": {
            "description": "Batch engagement counts for the specimens on one page. Fetched at RUNTIME and never baked into the static build, because a build-time number is a claim about when the page was generated dressed up as a current one. A ref with no row is OMITTED rather than returned as 0 — never counted and measured-zero are different facts. Carries the definition of the number",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Record ONE deduped engagement event for a specimen. Deduplicated per visitor per day on a one-way, day-scoped IP hash — no cookie, no device storage, no identity. Agent visits COUNT (this is an agent-oriented product); 2000 pings in five seconds count once. Build, canary and internal traffic are excluded. The response carries the exact definition of the number, and returns recorded:false when the visitor was already counted today — that is a normal outcome, not an error",
        "operationId": "recordEngagement",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "kind",
                  "ref"
                ],
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "view",
                      "like"
                    ],
                    "description": "view is open; like requires the visitor token from #84 and currently returns 501"
                  },
                  "ref": {
                    "type": "string",
                    "description": "slug:archetype — the STABLE specimen identity. Never a crawl or job id: re-crawls re-point which crawl is primary and would orphan the count"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Record ONE deduped engagement event for a specimen. Deduplicated per visitor per day on a one-way, day-scoped IP hash — no cookie, no device storage, no identity. Agent visits COUNT (this is an agent-oriented product); 2000 pings in five seconds count once. Build, canary and internal traffic are excluded. The response carries the exact definition of the number, and returns recorded:false when the visitor was already counted today — that is a normal outcome, not an error",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/factory/sessions/{session_id}": {
      "patch": {
        "summary": "Change a session's visibility and/or record sources discovered mid-work (#177) — body needs privacy, source_specimens, or both. REQUIRES the write credential (x-uxxray-session-token header): editing is an owner action, and the review link alone must not authorise it. private means UNLISTED, not access-controlled — the id is still a capability. source_specimens APPENDS to what is already stored, it never replaces — a worker that discovers a specimen it used in round 2 does not need to resend round 1's list",
        "operationId": "patchFactorySession",
        "parameters": [
          {
            "name": "session_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Session id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "privacy": {
                    "type": "string",
                    "enum": [
                      "public",
                      "private"
                    ]
                  },
                  "source_specimens": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Specimen refs to APPEND to the session's recorded sources (deduped, capped at 80 total) — not a replacement of what is already stored"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Change a session's visibility and/or record sources discovered mid-work (#177) — body needs privacy, source_specimens, or both. REQUIRES the write credential (x-uxxray-session-token header): editing is an owner action, and the review link alone must not authorise it. private means UNLISTED, not access-controlled — the id is still a capability. source_specimens APPENDS to what is already stored, it never replaces — a worker that discovers a specimen it used in round 2 does not need to resend round 1's list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "get": {
        "summary": "Get one Design Factory session, its registered concepts, latest feedback, and agent contract",
        "operationId": "getFactorySession",
        "parameters": [
          {
            "name": "session_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Get one Design Factory session, its registered concepts, latest feedback, and agent contract",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/library/search": {
      "get": {
        "summary": "SPECIMEN SEARCH — free text resolved into explicit MEASURED filters (archetype, tactic, funnel type, min score), then real specimens with receipts. Deterministic, not semantic: any term it does not understand is REPORTED as unmatched rather than silently ignored",
        "operationId": "searchSpecimens",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "e.g. \"high-scoring saas heroes with proof\", \"pricing with guarantee\", \"ecommerce reviews with rating\""
          },
          {
            "name": "funnel_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Overrides any funnel type inferred from q"
          },
          {
            "name": "min_score",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Overrides any score floor inferred from q"
          },
          {
            "name": "top",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "How many specimens (default 20, max 100)"
          }
        ],
        "responses": {
          "200": {
            "description": "SPECIMEN SEARCH — free text resolved into explicit MEASURED filters (archetype, tactic, funnel type, min score), then real specimens with receipts. Deterministic, not semantic: any term it does not understand is REPORTED as unmatched rather than silently ignored",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/library/brief": {
      "get": {
        "summary": "THE INSPIRATION BRIEF — a SHORTLIST for one design task: resolved taxonomy, 2-3 selected specimens with receipts, what to read them for, and the originality instruction. Deterministic, so asking twice returns the same specimens; the response names its own pool size and links the full roster. For the whole library start at /api/v1/library/index instead",
        "operationId": "getInspirationBrief",
        "parameters": [
          {
            "name": "task",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The design task to brief for"
          },
          {
            "name": "funnel_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Restrict to one measured funnel type"
          },
          {
            "name": "top",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "How many specimens (default 3, max 10)"
          }
        ],
        "responses": {
          "200": {
            "description": "THE INSPIRATION BRIEF — a SHORTLIST for one design task: resolved taxonomy, 2-3 selected specimens with receipts, what to read them for, and the originality instruction. Deterministic, so asking twice returns the same specimens; the response names its own pool size and links the full roster. For the whole library start at /api/v1/library/index instead",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/playbooks": {
      "get": {
        "summary": "Funnel-playbook registry + which crawled funnels match each (measured surface matches, quoted evidence; bundles carry per-funnel matches as bundle.playbooks)",
        "operationId": "getPlaybooks",
        "responses": {
          "200": {
            "description": "Funnel-playbook registry + which crawled funnels match each (measured surface matches, quoted evidence; bundles carry per-funnel matches as bundle.playbooks)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/playbooks/{id}": {
      "get": {
        "summary": "One funnel playbook, machine-readable: model summary + cheat sheet + how to unlock the prompt templates",
        "operationId": "getPlaybook",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Playbook id (\"pre_sold\") or slug (\"pre-sold-funnel\"). Both are returned by GET /api/v1/playbooks; either resolves here."
          }
        ],
        "responses": {
          "200": {
            "description": "One funnel playbook, machine-readable: model summary + cheat sheet + how to unlock the prompt templates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/curriculum": {
      "get": {
        "summary": "THE AGENT STUDY PATH (markdown) — how to learn conversion design from this corpus: top scorers → bundles → screenshots → playbook → page → section → copy",
        "operationId": "getCurriculum",
        "responses": {
          "200": {
            "description": "THE AGENT STUDY PATH (markdown) — how to learn conversion design from this corpus: top scorers → bundles → screenshots → playbook → page → section → copy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/rate": {
      "post": {
        "summary": "★ Rate any result 1-5 — body {rating, context?, via: \"user\"(relayed human)|\"agent\", comment?}. Terminal responses carry the ask as _rate",
        "operationId": "postRate",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "rating"
                ],
                "properties": {
                  "rating": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5
                  },
                  "context": {
                    "type": "string",
                    "description": "What was rated, e.g. \"bundle:acme.com\""
                  },
                  "via": {
                    "type": "string",
                    "enum": [
                      "user",
                      "agent"
                    ],
                    "description": "\"user\" = a relayed human answer, \"agent\" = the agent's own"
                  },
                  "comment": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "★ Rate any result 1-5 — body {rating, context?, via: \"user\"(relayed human)|\"agent\", comment?}. Terminal responses carry the ask as _rate",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "get": {
        "summary": "Rating summary — overall count/avg + breakdown by context",
        "operationId": "getRateSummary",
        "responses": {
          "200": {
            "description": "Rating summary — overall count/avg + breakdown by context",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scan-request": {
      "post": {
        "summary": "Request a new site be scanned (queued for manual/batch capture) — body {url, email?}",
        "operationId": "postScanRequest",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded (idempotent on url+email)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing/invalid body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scan": {
      "post": {
        "summary": "Live on-demand x-ray — real Browser Rendering capture, async. Body {url, mode?, steps?, maxSteps?, force?}. mode=\"auto\" (default) auto-discovers the funnel (entry → pricing/product → signup/checkout); \"single\" scans just the entry page; \"defined\" walks the exact same-domain URLs in steps[]. maxSteps caps auto/defined (default 4, max 6). Returns 202 {jobId,status} or 200 {status:done,slug} if cached within 24h; {\"force\":true} bypasses the cache.",
        "operationId": "postScan",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Entry / first-step URL"
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "auto",
                      "single",
                      "defined"
                    ],
                    "default": "auto",
                    "description": "auto = discover the funnel; single = entry page only; defined = walk the URLs in steps[]"
                  },
                  "steps": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uri"
                    },
                    "description": "For mode=\"defined\": the exact funnel step URLs to capture after the entry page (same domain only)"
                  },
                  "maxSteps": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 6,
                    "default": 4,
                    "description": "Cap on total steps captured (auto/defined)"
                  },
                  "force": {
                    "type": "boolean",
                    "description": "Bypass the 24h cache and re-capture/re-score"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Already scanned within 24h — cached result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "done"
                      ]
                    },
                    "slug": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Queued — poll GET /api/v1/scan/{jobId}",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid URL / blocked host (SSRF guard)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (100/day per IP; our own IPs are allowlisted)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scan/{jobId}": {
      "get": {
        "summary": "Poll a live scan job — {status, slug?, score?, band?, blocked?, error?}",
        "operationId": "getScanStatus",
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Job status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued",
                        "running",
                        "done",
                        "error"
                      ]
                    },
                    "slug": {
                      "type": "string"
                    },
                    "score": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "band": {
                      "type": "string"
                    },
                    "stars": {
                      "type": "integer"
                    },
                    "blocked": {
                      "type": "boolean"
                    },
                    "error": {
                      "type": "string"
                    },
                    "reason": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "enum": [
                        "robots_denied",
                        "no_browser",
                        "exception",
                        "max_attempts",
                        "stale_reap",
                        "content_rejected",
                        null
                      ]
                    },
                    "detail": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "wrote_teardown": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No job with this id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/vs/{a}/{b}": {
      "get": {
        "summary": "Deterministic side-by-side compare page (HTML) for two teardowns",
        "operationId": "getVs",
        "parameters": [
          {
            "name": "a",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Slug of side A (must already be scored)"
          },
          {
            "name": "b",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Slug of side B (may still be scanning — renders a \"developing\" state)"
          }
        ],
        "responses": {
          "200": {
            "description": "Deterministic side-by-side compare page (HTML) for two teardowns",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/playbooks/unlock": {
      "post": {
        "summary": "Unlock a playbook's full prompt templates — body {email, playbook, source?}; returns {playbook:{prompts_md, cheat_md}}",
        "operationId": "postPlaybookUnlock",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "playbook"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "playbook": {
                    "type": "string"
                  },
                  "source": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Unlock a playbook's full prompt templates — body {email, playbook, source?}; returns {playbook:{prompts_md, cheat_md}}",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/playbooks/feedback": {
      "post": {
        "summary": "Report whether a playbook was useful — body {playbook, useful, comment?, email?, via?}",
        "operationId": "postPlaybookFeedback",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "playbook"
                ],
                "properties": {
                  "playbook": {
                    "type": "string"
                  },
                  "useful": {
                    "type": "boolean"
                  },
                  "comment": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "via": {
                    "type": "string",
                    "enum": [
                      "agent",
                      "widget"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Report whether a playbook was useful — body {playbook, useful, comment?, email?, via?}",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/factory/sessions": {
      "get": {
        "summary": "List Design Factory sessions. Anonymous callers see ONLY sessions explicitly created with privacy:\"public\" — private sessions are reachable solely by someone who already holds the id",
        "operationId": "getFactorySessions",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 20
            },
            "description": "Max sessions to return, capped at 100"
          }
        ],
        "responses": {
          "200": {
            "description": "List Design Factory sessions. Anonymous callers see ONLY sessions explicitly created with privacy:\"public\" — private sessions are reachable solely by someone who already holds the id",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a Design Factory session — PRIVATE by default; returns a write credential ONCE that is required to register concepts (header x-uxxray-session-token). The session_id alone is the review link: safe to hand a human, allows reading and feedback, not writing. Create — body {task, project?, target?, source_specimens?, agent?, privacy?}; external AI agents register concepts into it",
        "operationId": "postFactorySession",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "task"
                ],
                "properties": {
                  "task": {
                    "type": "string",
                    "description": "Human design task, e.g. build_review_section"
                  },
                  "project": {
                    "type": "string",
                    "description": "Optional project or repo name"
                  },
                  "target": {
                    "type": "string",
                    "description": "Target stack hint, e.g. html, react, astro, laravel, unknown"
                  },
                  "source_specimens": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Measured UXXRAY specimen ids/URLs the agent plans to use"
                  },
                  "agent": {
                    "type": "string",
                    "description": "Calling agent/client, e.g. claude-code"
                  },
                  "privacy": {
                    "type": "string",
                    "enum": [
                      "private",
                      "unlisted"
                    ],
                    "default": "private"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Create a Design Factory session — PRIVATE by default; returns a write credential ONCE that is required to register concepts (header x-uxxray-session-token). The session_id alone is the review link: safe to hand a human, allows reading and feedback, not writing. Create — body {task, project?, target?, source_specimens?, agent?, privacy?}; external AI agents register concepts into it",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/factory/sessions/{session_id}/concepts": {
      "post": {
        "summary": "Register an original concept into a session. REQUIRES the write credential from session creation, sent as the x-uxxray-session-token header; the review link alone will not authorise it. artifact must carry non-empty html or css",
        "operationId": "postFactoryConcept",
        "parameters": [
          {
            "name": "session_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "concept_id"
                ],
                "properties": {
                  "concept_id": {
                    "type": "string",
                    "description": "Stable concept slug inside the session"
                  },
                  "title": {
                    "type": "string"
                  },
                  "group": {
                    "type": "string",
                    "description": "Plain group slug, e.g. sections, pricing, proof"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "draft",
                      "refined",
                      "selected",
                      "archived"
                    ]
                  },
                  "path": {
                    "type": "string",
                    "description": "Optional hosted/review path if already known"
                  },
                  "brief_md": {
                    "type": "string",
                    "description": "Markdown generation brief / notes"
                  },
                  "sources": {
                    "type": "object",
                    "description": "Measured UXXRAY sources + user-provided sources"
                  },
                  "artifact": {
                    "type": "object",
                    "description": "Portable generated artifact contract"
                  },
                  "parent_concept_id": {
                    "type": "string",
                    "description": "Concept this refines, if any"
                  },
                  "version_label": {
                    "type": "string",
                    "maxLength": 40,
                    "description": "Optional NAME for this version, e.g. \"tighter proof\". Printed on the version toggle. It is a label only — the version NUMBER (1.0, 1.1, 2.0) is always derived from parent_concept_id and cannot be set here."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Register an original concept into a session. REQUIRES the write credential from session creation, sent as the x-uxxray-session-token header; the review link alone will not authorise it. artifact must carry non-empty html or css",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/factory/sessions/{session_id}/feedback": {
      "get": {
        "summary": "Get latest session-scoped feedback as both raw verdicts and an agent-ready refinement summary",
        "operationId": "getFactorySessionFeedback",
        "parameters": [
          {
            "name": "session_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Get latest session-scoped feedback as both raw verdicts and an agent-ready refinement summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Record session-scoped concept feedback — body {concept_id, starred?, rating?, comment?, tags?}",
        "operationId": "postFactorySessionFeedback",
        "parameters": [
          {
            "name": "session_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "concept_id"
                ],
                "properties": {
                  "concept_id": {
                    "type": "string"
                  },
                  "starred": {
                    "type": "boolean"
                  },
                  "rating": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 5
                  },
                  "comment": {
                    "type": "string"
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Record session-scoped concept feedback — body {concept_id, starred?, rating?, comment?, tags?}",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/factory/sessions/{session_id}/concepts/{concept_id}": {
      "patch": {
        "summary": "Commit a round's winner (#159 PART B) — body {status:\"selected\"}. REQUIRES the write credential (x-uxxray-session-token header), same as concept registration: the review link alone must not decide a session. Marks this concept selected, archives every OTHER direction's root, stamps the session's decided_concept_id/decided_at, and returns round_two_seed — the winner's brief + comment, every REJECTED concept's comment labelled as such, the source specimens already used, and both round-two branches (evolve the winner vs fresh alternatives). The human chooses the branch, never the agent",
        "operationId": "patchFactoryConcept",
        "parameters": [
          {
            "name": "session_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Session id"
          },
          {
            "name": "concept_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The concept to commit as this round's winner"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "selected"
                    ],
                    "description": "Only \"selected\" is accepted — this route commits a winner, nothing else"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Commit a round's winner (#159 PART B) — body {status:\"selected\"}. REQUIRES the write credential (x-uxxray-session-token header), same as concept registration: the review link alone must not decide a session. Marks this concept selected, archives every OTHER direction's root, stamps the session's decided_concept_id/decided_at, and returns round_two_seed — the winner's brief + comment, every REJECTED concept's comment labelled as such, the source specimens already used, and both round-two branches (evolve the winner vs fresh alternatives). The human chooses the branch, never the agent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/factory/sessions/{session_id}/concepts/{concept_id}/artifact": {
      "get": {
        "summary": "Get the portable artifact bundle for one generated concept: HTML/CSS/tokens/adapters/source provenance when registered",
        "operationId": "getFactoryArtifact",
        "parameters": [
          {
            "name": "session_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "concept_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Get the portable artifact bundle for one generated concept: HTML/CSS/tokens/adapters/source provenance when registered",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/report/dispute": {
      "post": {
        "summary": "Report a tactic we missed on a report — body {slug, tactic_id, url?, note?, email?, via?}; logged for detector review, not auto-applied",
        "operationId": "postReportDispute",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "slug",
                  "tactic_id"
                ],
                "properties": {
                  "slug": {
                    "type": "string"
                  },
                  "tactic_id": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string"
                  },
                  "note": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "via": {
                    "type": "string",
                    "enum": [
                      "agent",
                      "widget"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Report a tactic we missed on a report — body {slug, tactic_id, url?, note?, email?, via?}; logged for detector review, not auto-applied",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/teardown/{slug}": {
      "get": {
        "summary": "Full teardown — all device variants, score, funnel steps",
        "operationId": "getTeardown",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Teardown",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Teardown"
                }
              }
            }
          },
          "404": {
            "description": "No teardown for this slug",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/teardown/{slug}/bundle": {
      "get": {
        "summary": "One self-contained JSON with everything for one site (?include=, ?exclude=) — the recommended single fetch for agents",
        "operationId": "getBundle",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated section allowlist, e.g. score,facts"
          },
          {
            "name": "exclude",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated sections to drop, e.g. screenshots"
          }
        ],
        "responses": {
          "200": {
            "description": "Bundle",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Bundle"
                }
              }
            }
          },
          "404": {
            "description": "No teardown for this slug",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/teardown/{slug}/tokens": {
      "get": {
        "summary": "Design tokens (?format=json|w3c|tailwind|css). The json form carries the five-slot `tokens` export UNCHANGED plus a `design` object holding what those slots were derived from: the full palette ordered by usage with usage_pct, weighted ink candidates, the real type scale and weights, EVERY radius, and the measured roles with any collisions NAMED rather than resolved by inventing a colour. Page-scoped. No spacing field — nothing measures spacing",
        "operationId": "getTokens",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "w3c",
                "tailwind",
                "css"
              ],
              "default": "json"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tokens",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Unsupported format",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No export for this capture",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/teardown/{slug}/brief": {
      "get": {
        "summary": "Machine-generated build-brief markdown",
        "operationId": "getBrief",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Markdown brief",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "No brief for this capture",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/teardown/{slug}/favicon": {
      "get": {
        "summary": "Favicon image, streamed from R2",
        "operationId": "getFavicon",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Image",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/x-icon": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/svg+xml": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "No favicon for this capture",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/teardown/{slug}/report": {
      "get": {
        "summary": "Pre-rendered shareable report HTML (self-heals from D1/R2 if not yet rendered)",
        "operationId": "getReport",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pre-rendered shareable report HTML (self-heals from D1/R2 if not yet rendered)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/teardown/{slug}/og.png": {
      "get": {
        "summary": "Branded 1200x630 social card image, composed on demand from the latest hero screenshot",
        "operationId": "getOgImage",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "force",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Bypass the R2 cache and re-render"
          }
        ],
        "responses": {
          "200": {
            "description": "Branded 1200x630 social card image, composed on demand from the latest hero screenshot",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/teardown/{slug}/screenshot/{path}": {
      "get": {
        "summary": "One step screenshot/crop, streamed from R2 (e.g. path=\"01-home/hero.png\")",
        "operationId": "getScreenshot",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "path",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Relative R2 path within the teardown, e.g. \"01-home/hero.png\""
          }
        ],
        "responses": {
          "200": {
            "description": "One step screenshot/crop, streamed from R2 (e.g. path=\"01-home/hero.png\")",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "allowed": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "error"
        ]
      },
      "LeagueRow": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "funnel_type": {
            "type": "string"
          },
          "score": {
            "type": [
              "integer",
              "null"
            ]
          },
          "band": {
            "type": "string"
          },
          "stars": {
            "type": "integer"
          },
          "favicon_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "tactics_detected": {
            "type": "integer"
          },
          "proof_points": {
            "type": "integer"
          }
        }
      },
      "CorpusStats": {
        "type": "object",
        "properties": {
          "sites": {
            "type": "integer"
          },
          "tactics_total": {
            "type": "integer"
          },
          "evidence": {
            "type": "integer"
          },
          "avg_tactics": {
            "type": "number"
          },
          "pct_lack": {
            "type": "object",
            "additionalProperties": {
              "type": [
                "integer",
                "null"
              ]
            }
          },
          "pattern_rates": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "present": {
                  "type": "integer"
                },
                "evaluated_n": {
                  "type": "integer"
                },
                "pct_present": {
                  "type": [
                    "integer",
                    "null"
                  ]
                },
                "pct_absent": {
                  "type": [
                    "integer",
                    "null"
                  ]
                },
                "corpus_n": {
                  "type": "integer"
                },
                "applies_to": {
                  "type": [
                    "array",
                    "null"
                  ],
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      },
      "Teardown": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "date": {
            "type": "string"
          },
          "funnel_type": {
            "type": "string"
          },
          "blocked": {
            "type": "boolean"
          },
          "favicon_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "score": {
            "type": "object",
            "properties": {
              "score": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "band": {
                "type": "string"
              },
              "stars": {
                "type": "integer"
              }
            }
          },
          "devices_available": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "devices": {
            "type": "object",
            "additionalProperties": true,
            "description": "Raw teardown.json per device, keyed by device"
          }
        }
      },
      "Bundle": {
        "type": "object",
        "description": "Self-contained per-site payload. See `provenance` for which sections are measured vs. derived vs. a rendered summary.",
        "properties": {
          "schema_version": {
            "type": "string"
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "slug": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "funnel_type": {
            "type": "string"
          },
          "provenance": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "enum": [
                "measured",
                "derived_deterministic",
                "rendered_summary"
              ]
            }
          },
          "score": {
            "type": "object",
            "additionalProperties": true
          },
          "devices_available": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "facts": {
            "type": "object",
            "additionalProperties": true
          },
          "design_tokens": {
            "type": "object",
            "additionalProperties": true,
            "description": "Compact summary inline; full exports linked in _links"
          },
          "proof_language": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "steps": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "favicon_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "agent_brief": {
            "type": "object",
            "properties": {
              "source_type": {
                "type": "string"
              },
              "note": {
                "type": "string"
              },
              "markdown": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "devices": {
            "type": "object",
            "additionalProperties": true
          },
          "_links": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          }
        }
      }
    }
  }
}