﻿{
  "openapi": "3.0.3",
  "info": {
    "title": "ByteWaveNetwork API",
    "description": "REST API for ByteWaveNetwork tools: **Link Checker** (BFS site crawl for broken links, redirects, and slow pages), **SEO Analyzer** (BFS site crawl for SEO health scores, missing tags, and structural issues; optional standalone Lighthouse audit for performance/accessibility/Core Web Vitals), **Redirect Tracer** (full redirect chain inspection with per-hop timing), **Page Speed Inspector** (server-side performance metrics: TTFB, DNS/TCP/TLS, compression, caching), **Sitemap Validator** (fetch and validate XML sitemaps — broken links, redirects, noindex, canonical mismatches), **Schema Markup Tester** (extract and validate JSON-LD, Microdata, and RDFa against 18 schema.org types), **SEO Page Checker** (single-page 14-point on-page SEO audit with 0–100 score; sitemap-based multi-page scan mode), and **Context Eval** (benchmark any LLM's long-context retrieval accuracy — inject a needle fact at configurable positions and score responses with matched/missed keyword transparency; supports up to 4 simultaneous model comparisons and custom document haystacks). All scan-based tools run asynchronously; real-time progress is delivered over WebSocket.\n\n**Base URL:** `http://localhost:3000/api/v1`\n\n**Rate limit:** 10 POST `/scan` or `/check` requests per minute per IP (`429 Too Many Requests` when exceeded).\n\n**WebSocket:** Connect to `ws://host/ws?scanId=<id>` immediately after starting a scan to receive per-URL progress frames.\n\n**Contact:** [support@bytewavenetwork.com](mailto:support@bytewavenetwork.com) · [Contact page](https://www.bytewavenetwork.com/contact/)",
    "version": "1.7.0",
    "contact": {
      "name": "ByteWaveNetwork Support",
      "url": "https://www.bytewavenetwork.com/contact/",
      "email": "support@bytewavenetwork.com"
    },
    "license": {
      "name": "MIT"
    }
  },
  "servers": [
    {
      "url": "/api/v1",
      "description": "Local development server"
    }
  ],
  "tags": [
    { "name": "Link Checker — Scans", "description": "Start, monitor, and retrieve link checker scans" },
    { "name": "Link Checker — Results", "description": "Paginated link results and exports" },
    { "name": "SEO Analyzer — Scans", "description": "Start, monitor, and retrieve SEO analyzer scans" },
    { "name": "SEO Analyzer — Results", "description": "Paginated page results and exports" },
    { "name": "Redirect Tracer", "description": "Trace full redirect chains for any URL — every hop, status code, and response time" },
    { "name": "Page Speed Inspector", "description": "Server-side performance metrics: TTFB, DNS/TCP/TLS timing, compression, caching, HTTP/2 detection" },
    { "name": "Sitemap Validator", "description": "Fetch and validate XML sitemaps — check every URL for broken links, redirects, noindex, and canonical mismatches" },
    { "name": "Schema Markup Tester", "description": "Extract and validate JSON-LD, Microdata, and RDFa structured data against 18 schema.org types" },
    { "name": "Schema Markup Tester — Scans", "description": "Start, monitor, and cancel sitemap-based multi-page schema scans" },
    { "name": "Context Eval", "description": "Benchmark LLM long-context retrieval — inject a needle fact at configurable positions, score responses with keyword matching, compare up to 4 models side-by-side" },
    { "name": "Instruction Eval", "description": "Test multi-step instruction following across easy/medium/hard difficulty levels — measures whether models obey layered constraints" },
    { "name": "Agentic Loop Eval", "description": "Run 5-step research chains using real APIs (Wikipedia, Open-Meteo, REST Countries) to test loop reliability and tool abandonment" },
    { "name": "Thinking Mode Eval", "description": "Compare output quality and cost with reasoning/thinking mode ON vs OFF across math, coding, factual, and creative tasks" },
    { "name": "Prompt Sensitivity Eval", "description": "Measure how output quality changes across 10 phrasing variants of the same task — tracks score std-dev as the brittleness metric" },
    { "name": "SEO Page Checker", "description": "Audit a single page against 14 on-page SEO checks (title, meta description, canonical, headings, Open Graph, schema, author E-E-A-T, content depth, internal links, disclosure) with a 0–100 score. Supports both single-page (sync) and sitemap-based multi-page (async + WebSocket) modes." },
    { "name": "Newsletter", "description": "Email newsletter subscription with double opt-in confirmation" },
    { "name": "System", "description": "Health check and server configuration" },
    { "name": "MCP", "description": "Model Context Protocol 2024-11-05 server — JSON-RPC 2.0 over HTTP POST. Exposes all ByteWaveNetwork tools (SEO/performance + AI eval runners) to Claude Desktop and MCP-compatible AI agents. Endpoint: POST /api/mcp (base is /api, not /api/v1)." }
  ],
  "paths": {
    "/link-checker/scan": {
      "post": {
        "tags": ["Link Checker — Scans"],
        "summary": "Start a new scan",
        "description": "Starts an asynchronous BFS crawl of the given URL. Returns immediately with a `scanId` and WebSocket URL. Connect to the WebSocket to receive per-URL progress frames in real time.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/LinkScanRequest" },
              "examples": {
                "minimal": {
                  "summary": "Minimal — URL only",
                  "value": { "url": "https://example.com" }
                },
                "full": {
                  "summary": "All options",
                  "value": {
                    "url": "https://example.com",
                    "depth": 3,
                    "concurrency": 5,
                    "respectRobots": true,
                    "slowThresholdInternalMs": 2000,
                    "slowThresholdExternalMs": 5000,
                    "webhookUrl": "https://webhook.site/your-id",
                    "excludePatterns": ["*/admin/*", "/staging"],
                    "bypassHeader": {
                      "name": "x-vercel-protection-bypass",
                      "value": "your-secret"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Scan accepted and queued",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ScanStarted" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/link-checker/scan/{scanId}": {
      "get": {
        "tags": ["Link Checker — Scans"],
        "summary": "Get scan metadata and stats",
        "description": "Returns the scan configuration, current status, and aggregate link statistics. Available both during and after a scan.",
        "parameters": [
          { "$ref": "#/components/parameters/scanId" }
        ],
        "responses": {
          "200": {
            "description": "Scan found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/LinkScanDetail" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "delete": {
        "tags": ["Link Checker — Scans"],
        "summary": "Stop a running scan",
        "description": "Signals the BFS crawler to stop after in-flight requests complete. Broadcasts a `scan:cancelled` WebSocket frame with partial stats. Returns 404 if the scan is not currently running.",
        "parameters": [
          { "$ref": "#/components/parameters/scanId" }
        ],
        "responses": {
          "200": {
            "description": "Stop signal sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "example": true },
                    "message": { "type": "string", "example": "Scan stop requested" }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Scan not currently running",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": { "code": "NOT_FOUND", "message": "Scan not running" } }
              }
            }
          }
        }
      }
    },
    "/link-checker/scan/{scanId}/links": {
      "get": {
        "tags": ["Link Checker — Results"],
        "summary": "Get paginated link results",
        "description": "Returns links found during the scan with optional filtering by status or type. Results are ordered by `is_broken DESC, response_ms DESC` (broken and slowest links first).\n\nFor large scans, paginate using `page` and `limit`. The `total` field tells you the total matching count.",
        "parameters": [
          { "$ref": "#/components/parameters/scanId" },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by link status. `slow` matches both slow-internal and slow-external.",
            "schema": {
              "type": "string",
              "enum": ["broken", "blocked", "redirect", "slow", "slow-internal", "slow-external", "skipped", "ok"]
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "Filter by link type",
            "schema": {
              "type": "string",
              "enum": ["internal", "external", "image", "stylesheet", "script"]
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (1-indexed)",
            "schema": { "type": "integer", "minimum": 1, "default": 1 }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Results per page (max 500)",
            "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 100 }
          }
        ],
        "responses": {
          "200": {
            "description": "Link results",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/LinksPage" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/link-checker/scan/{scanId}/heal": {
      "get": {
        "tags": ["Link Checker — Results"],
        "summary": "Get redirect suggestions for 404 links (Pro)",
        "description": "Fetches all 404 links from the scan and cross-references them against the site's `sitemap.xml` to suggest redirect targets using path similarity scoring (Levenshtein distance). Only suggestions with confidence ≥ 0.3 are returned.\n\n**Requires** the `x-pro-key` header matching the `DEVUTILS_PRO_KEY` environment variable.",
        "parameters": [
          { "$ref": "#/components/parameters/scanId" },
          {
            "name": "x-pro-key",
            "in": "header",
            "required": true,
            "description": "Pro tier API key. Set via `DEVUTILS_PRO_KEY` environment variable on the server.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Redirect suggestions (may be empty if no 404s or no sitemap found)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "suggestions": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/HealSuggestion" }
                    },
                    "note": {
                      "type": "string",
                      "nullable": true,
                      "description": "Informational message when no sitemap was found",
                      "example": "No sitemap.xml found — cannot generate suggestions"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Missing or invalid Pro key",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": { "code": "FORBIDDEN", "message": "Valid x-pro-key header required for Healer mode" } }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/link-checker/scan/{scanId}/export": {
      "get": {
        "tags": ["Link Checker — Results"],
        "summary": "Export scan results as CSV or JSON",
        "description": "Downloads all links for the scan (up to 10,000) as a file attachment. When a `filter` query param is provided, only matching links are exported — the same filter used by the UI filter tabs.\n\nUse `format=json` for machine-readable output with full scan metadata included.",
        "parameters": [
          { "$ref": "#/components/parameters/scanId" },
          {
            "name": "format",
            "in": "query",
            "description": "Export format",
            "schema": { "type": "string", "enum": ["csv", "json"], "default": "csv" }
          },
          {
            "name": "filter",
            "in": "query",
            "description": "Optionally restrict export to links matching this status filter (same values as the `/links` status param)",
            "schema": {
              "type": "string",
              "enum": ["broken", "blocked", "redirect", "slow", "slow-internal", "slow-external", "skipped", "ok"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "File download",
            "headers": {
              "Content-Disposition": {
                "schema": { "type": "string" },
                "example": "attachment; filename=\"scan-<id>.csv\""
              }
            },
            "content": {
              "text/csv": {
                "schema": { "type": "string" },
                "example": "URL,Source URL,Type,Status,Redirect URL,Response (ms),Broken,Redirect,Slow,SSL Error,Error,Canonical URL,Noindex,Depth\nhttps://example.com/,https://example.com/,internal,200,,210,false,false,false,,,https://example.com/,false,0"
              },
              "application/json": {
                "schema": { "$ref": "#/components/schemas/LinkExportJson" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/link-checker/scans": {
      "get": {
        "tags": ["Link Checker — Scans"],
        "summary": "List recent scans",
        "description": "Returns the most recent link checker scans ordered by creation time descending.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Number of scans to return (max 100)",
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 30 }
          }
        ],
        "responses": {
          "200": {
            "description": "Recent scans",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "scans": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/LinkScanSummary" }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/link-checker/config": {
      "get": {
        "tags": ["System"],
        "summary": "Get link checker configuration defaults",
        "description": "Returns the server's default link checker settings read from environment variables. The UI uses this to pre-fill form fields.",
        "responses": {
          "200": {
            "description": "Server defaults",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/LinkConfig" }
              }
            }
          }
        }
      }
    },
    "/seo-analyzer/scan": {
      "post": {
        "tags": ["SEO Analyzer — Scans"],
        "summary": "Start a new SEO analysis",
        "description": "Starts an asynchronous BFS crawl that audits each internal HTML page for SEO health. Returns immediately with a `scanId` and WebSocket URL. Connect to the WebSocket to receive per-page `seo:progress` frames in real time.\n\nEach crawled page is scored 0–100 based on the issues found:\n- **Errors** (−15 pts): missing title, duplicate title, missing H1, multiple H1s, missing canonical\n- **Warnings** (−5 pts): title too short/long, missing/short/long meta description, duplicate meta description, canonical mismatch, missing OG title/image, images missing alt, heading hierarchy skip, missing JSON-LD, noindex detected\n- **Info** (−2 pts): missing OG description/URL, missing Twitter Card, missing viewport",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SeoScanRequest" },
              "examples": {
                "minimal": {
                  "summary": "Minimal — URL only",
                  "value": { "url": "https://example.com" }
                },
                "full": {
                  "summary": "All options",
                  "value": {
                    "url": "https://example.com",
                    "depth": 3,
                    "concurrency": 5,
                    "respectRobots": false,
                    "webhookUrl": "https://webhook.site/your-id"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Scan accepted and queued",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ScanStarted" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/seo-analyzer/scan/{scanId}": {
      "get": {
        "tags": ["SEO Analyzer — Scans"],
        "summary": "Get SEO scan metadata and stats",
        "description": "Returns the scan configuration, current status, and aggregate page statistics including counts for error pages, warning pages, noindex pages, and missing titles/descriptions.",
        "parameters": [
          { "$ref": "#/components/parameters/scanId" }
        ],
        "responses": {
          "200": {
            "description": "Scan found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SeoScanDetail" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "delete": {
        "tags": ["SEO Analyzer — Scans"],
        "summary": "Stop a running SEO scan",
        "description": "Signals the SEO BFS crawler to stop after in-flight requests complete. Broadcasts a `seo:cancelled` WebSocket frame with partial stats. Returns 404 if the scan is not currently running.",
        "parameters": [
          { "$ref": "#/components/parameters/scanId" }
        ],
        "responses": {
          "200": {
            "description": "Stop signal sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "example": true },
                    "message": { "type": "string", "example": "Scan stop requested" }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Scan not currently running",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": { "code": "NOT_FOUND", "message": "Scan not running" } }
              }
            }
          }
        }
      }
    },
    "/seo-analyzer/scan/{scanId}/pages": {
      "get": {
        "tags": ["SEO Analyzer — Results"],
        "summary": "Get paginated page results",
        "description": "Returns pages audited during the scan, ordered by `score ASC, error_count DESC` (worst pages first). Use the `filter` parameter to narrow results to a specific issue category — the same filters used by the UI tabs.",
        "parameters": [
          { "$ref": "#/components/parameters/scanId" },
          {
            "name": "filter",
            "in": "query",
            "description": "Filter by issue category",
            "schema": {
              "type": "string",
              "enum": ["errors", "warnings", "noindex", "title", "description", "headings", "canonical", "og", "json-ld"]
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (1-indexed)",
            "schema": { "type": "integer", "minimum": 1, "default": 1 }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Results per page (max 200)",
            "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 }
          }
        ],
        "responses": {
          "200": {
            "description": "Page results",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SeoPagesPage" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/seo-analyzer/scan/{scanId}/export": {
      "get": {
        "tags": ["SEO Analyzer — Results"],
        "summary": "Export SEO results as CSV or JSON",
        "description": "Downloads all audited pages (up to 5,000) as a file attachment. When a `filter` query param is provided, only matching pages are exported — the same filter used by the UI tabs.",
        "parameters": [
          { "$ref": "#/components/parameters/scanId" },
          {
            "name": "format",
            "in": "query",
            "description": "Export format",
            "schema": { "type": "string", "enum": ["csv", "json"], "default": "csv" }
          },
          {
            "name": "filter",
            "in": "query",
            "description": "Optionally restrict export to pages matching this filter",
            "schema": {
              "type": "string",
              "enum": ["errors", "warnings", "noindex", "title", "description", "headings", "canonical", "og", "json-ld"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "File download",
            "headers": {
              "Content-Disposition": {
                "schema": { "type": "string" },
                "example": "attachment; filename=\"seo-<id>.csv\""
              }
            },
            "content": {
              "text/csv": {
                "schema": { "type": "string" },
                "example": "URL,Score,Errors,Warnings,Title,H1 Count,Has Canonical,Has OG,Noindex,Issues\nhttps://example.com/,85,0,3,Example Domain,1,yes,yes,no,DESC_TOO_SHORT; OG_DESC_MISSING; TWITTER_CARD_MISSING"
              },
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SeoExportJson" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/seo-analyzer/scans": {
      "get": {
        "tags": ["SEO Analyzer — Scans"],
        "summary": "List recent SEO scans",
        "description": "Returns the most recent SEO analyzer scans ordered by creation time descending.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Number of scans to return (max 100)",
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 30 }
          }
        ],
        "responses": {
          "200": {
            "description": "Recent SEO scans",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "scans": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/SeoScanSummary" }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/seo-analyzer/lighthouse-scan": {
      "post": {
        "tags": ["SEO Analyzer — Scans"],
        "summary": "Start a standalone Lighthouse audit",
        "description": "Starts an asynchronous Lighthouse audit. First performs a BFS crawl to discover internal URLs to the requested depth (1–5, max 200 URLs), then runs Lighthouse against each URL sequentially. Returns immediately with a `scanId` and WebSocket URL.\n\nRequires `LIGHTHOUSE_ENABLED=1` on the server and a reachable Chrome binary.\n\n**WebSocket frames:** `seo:lighthouse-discovering` → `seo:lighthouse-started` → `seo:lighthouse-progress` (one per URL) → `seo:lighthouse-complete`",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["url"],
                "properties": {
                  "url": { "type": "string", "format": "uri", "description": "Root URL to crawl and audit" },
                  "depth": { "type": "integer", "minimum": 1, "maximum": 5, "default": 1, "description": "BFS crawl depth for URL discovery (1 = root page only, 5 = five hops deep)" }
                }
              },
              "examples": {
                "minimal": { "summary": "Root page only", "value": { "url": "https://example.com" } },
                "deep": { "summary": "Depth 3 crawl", "value": { "url": "https://example.com", "depth": 3 } }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Lighthouse scan accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ScanStarted" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/seo-analyzer/lighthouse-scan/{scanId}": {
      "delete": {
        "tags": ["SEO Analyzer — Scans"],
        "summary": "Stop a running Lighthouse scan",
        "description": "Signals the Lighthouse runner to stop after the current URL's audit completes. Broadcasts `seo:lighthouse-cancelled` on the WebSocket.",
        "parameters": [
          { "$ref": "#/components/parameters/scanId" }
        ],
        "responses": {
          "200": {
            "description": "Stop signal sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "example": true },
                    "message": { "type": "string", "example": "Lighthouse scan stop requested" }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Scan not currently running",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/seo-analyzer/config": {
      "get": {
        "tags": ["System"],
        "summary": "Get SEO analyzer configuration defaults",
        "description": "Returns the server's default SEO analyzer settings read from environment variables.",
        "responses": {
          "200": {
            "description": "Server defaults",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SeoConfig" }
              }
            }
          }
        }
      }
    },
    "/redirect-tracer/trace": {
      "post": {
        "tags": ["Redirect Tracer"],
        "summary": "Trace a redirect chain",
        "description": "Follows redirect hops for the given URL using GET requests (no automatic redirect following). Returns every hop with its status code, Location header, response time, and HTTP version. Detects redirect loops automatically.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/TraceRequest" },
              "examples": {
                "minimal": { "summary": "URL only", "value": { "url": "https://example.com" } },
                "full":    { "summary": "All options", "value": { "url": "https://example.com", "maxHops": 20 } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Redirect chain traced",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TraceResult" } } }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/redirect-tracer/traces": {
      "get": {
        "tags": ["Redirect Tracer"],
        "summary": "List recent traces",
        "description": "Returns the last 20 (up to 100 with `?limit=`) redirect traces.",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 } }
        ],
        "responses": {
          "200": {
            "description": "Trace list",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "traces": { "type": "array", "items": { "$ref": "#/components/schemas/TraceSummary" } } } } } }
          }
        }
      }
    },
    "/redirect-tracer/trace/{traceId}": {
      "get": {
        "tags": ["Redirect Tracer"],
        "summary": "Get a single trace",
        "parameters": [
          { "name": "traceId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "responses": {
          "200": { "description": "Trace found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TraceResult" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/page-speed/inspect": {
      "post": {
        "tags": ["Page Speed Inspector"],
        "summary": "Inspect page speed",
        "description": "Measures server-side performance for the given URL. Records DNS lookup, TCP connect, TLS handshake, Time to First Byte (TTFB), and download time using a fresh socket (no connection pooling). Also checks HTTP version, compression, cache headers, and render-blocking resources in the HTML. Returns a 0–100 performance score and actionable recommendations.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PageSpeedRequest" },
              "example": { "url": "https://example.com" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Inspection complete",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PageSpeedResult" } } }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/page-speed/inspections": {
      "get": {
        "tags": ["Page Speed Inspector"],
        "summary": "List recent inspections",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 } }
        ],
        "responses": {
          "200": {
            "description": "Inspection list",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "inspections": { "type": "array", "items": { "$ref": "#/components/schemas/PageSpeedSummary" } } } } } }
          }
        }
      }
    },
    "/page-speed/inspection/{inspectionId}": {
      "get": {
        "tags": ["Page Speed Inspector"],
        "summary": "Get a single inspection",
        "parameters": [
          { "name": "inspectionId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "responses": {
          "200": { "description": "Inspection found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PageSpeedResult" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/sitemap-validator/validate": {
      "post": {
        "tags": ["Sitemap Validator"],
        "summary": "Start a sitemap validation",
        "description": "Fetches the XML sitemap, parses all `<url>` entries (or child sitemaps for sitemap indexes), then checks each URL with a GET request. Progress is streamed via WebSocket (`sitemap:progress` frames). Supports up to 500 URLs per validation.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SitemapValidateRequest" }, "example": { "url": "https://example.com/sitemap.xml" } } }
        },
        "responses": {
          "202": { "description": "Validation started", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SitemapValidationStarted" } } } },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/sitemap-validator/validations": {
      "get": {
        "tags": ["Sitemap Validator"],
        "summary": "List recent validations",
        "parameters": [{ "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 100 } }],
        "responses": { "200": { "description": "Validation list", "content": { "application/json": { "schema": { "type": "object", "properties": { "validations": { "type": "array", "items": { "$ref": "#/components/schemas/SitemapValidationSummary" } } } } } } } }
      }
    },
    "/sitemap-validator/validation/{validationId}": {
      "get": {
        "tags": ["Sitemap Validator"],
        "summary": "Get validation metadata and stats",
        "parameters": [{ "name": "validationId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": { "200": { "description": "Validation found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SitemapValidationSummary" } } } }, "404": { "$ref": "#/components/responses/NotFound" } }
      },
      "delete": {
        "tags": ["Sitemap Validator"],
        "summary": "Stop a running validation",
        "parameters": [{ "name": "validationId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": { "200": { "description": "Stop signal sent" }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/sitemap-validator/validation/{validationId}/urls": {
      "get": {
        "tags": ["Sitemap Validator"],
        "summary": "Get paginated URL results",
        "parameters": [
          { "name": "validationId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "name": "filter", "in": "query", "schema": { "type": "string", "enum": ["all","ok","broken","redirect","noindex","error"], "default": "all" } },
          { "name": "page",  "in": "query", "schema": { "type": "integer", "default": 1 } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 500 } }
        ],
        "responses": { "200": { "description": "URL results page", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SitemapUrlPage" } } } } }
      }
    },
    "/schema-tester/test": {
      "post": {
        "tags": ["Schema Markup Tester"],
        "summary": "Test schema markup on a page",
        "description": "Fetches the page HTML and extracts all JSON-LD, Microdata, and RDFa structured data. Validates each schema against required and recommended properties for 18 schema.org types.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SchemaTestRequest" }, "example": { "url": "https://example.com/page" } } }
        },
        "responses": {
          "200": { "description": "Test complete", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SchemaTestResult" } } } },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/schema-tester/tests": {
      "get": {
        "tags": ["Schema Markup Tester"],
        "summary": "List recent tests",
        "parameters": [{ "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 100 } }],
        "responses": { "200": { "description": "Test list", "content": { "application/json": { "schema": { "type": "object", "properties": { "tests": { "type": "array", "items": { "$ref": "#/components/schemas/SchemaTestSummary" } } } } } } } }
      }
    },
    "/schema-tester/test/{testId}": {
      "get": {
        "tags": ["Schema Markup Tester"],
        "summary": "Get a single test result",
        "parameters": [{ "name": "testId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": { "200": { "description": "Test found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SchemaTestResult" } } } }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/schema-tester/scan": {
      "post": {
        "tags": ["Schema Markup Tester — Scans"],
        "summary": "Start an async sitemap-based schema scan",
        "description": "Fetches the given sitemap URL, then tests schema markup on each page found. Returns immediately with a `scanId` and WebSocket URL. Connect to the WebSocket to receive per-page `schema:progress` frames in real time.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SchemaScanRequest" },
              "examples": {
                "minimal": { "summary": "Sitemap URL only", "value": { "url": "https://example.com/sitemap.xml" } },
                "full": { "summary": "All options", "value": { "url": "https://example.com/sitemap.xml", "maxUrls": 100, "concurrency": 3 } }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Scan accepted and queued",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SchemaScanStarted" } } }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/schema-tester/scan/{scanId}": {
      "get": {
        "tags": ["Schema Markup Tester — Scans"],
        "summary": "Get scan metadata and per-page results",
        "description": "Returns the scan record plus an array of per-page schema test results.",
        "parameters": [
          { "name": "scanId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "responses": {
          "200": { "description": "Scan found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SchemaScanDetail" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "delete": {
        "tags": ["Schema Markup Tester — Scans"],
        "summary": "Cancel a running schema scan",
        "description": "Signals the scanner to stop after in-flight requests complete. Broadcasts a `schema:cancelled` WebSocket frame. Returns 404 if the scan is not currently running.",
        "parameters": [
          { "name": "scanId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "responses": {
          "200": {
            "description": "Stop signal sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "example": true },
                    "message": { "type": "string", "example": "Scan stop requested" }
                  }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/schema-tester/scans": {
      "get": {
        "tags": ["Schema Markup Tester — Scans"],
        "summary": "List recent sitemap scans",
        "description": "Returns the most recent sitemap-based schema scans ordered by creation time descending.",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 100 } }
        ],
        "responses": {
          "200": {
            "description": "Scan list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "scans": { "type": "array", "items": { "$ref": "#/components/schemas/SchemaScanSummary" } }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/evals/context-retrieval/models": {
      "get": {
        "tags": ["Context Eval"],
        "summary": "List available models",
        "description": "Returns the static model catalog — all LLMs available for evaluation, with pricing, context window size, vendor, and required `keyType`. Models are grouped by vendor; OpenRouter models require a single OpenRouter API key regardless of underlying vendor.",
        "responses": {
          "200": {
            "description": "Model catalog",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/EvalModelsResponse" },
                "example": {
                  "models": [
                    { "id": "claude-sonnet-4-6", "name": "Claude Sonnet 4.6", "vendor": "Anthropic", "keyType": "anthropic", "contextK": 200, "inputPer1M": 3.00, "outputPer1M": 15.00 },
                    { "id": "gpt-4o", "name": "GPT-4o", "vendor": "OpenAI", "keyType": "openai", "contextK": 128, "inputPer1M": 2.50, "outputPer1M": 10.00 }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/evals/context-retrieval/samples": {
      "get": {
        "tags": ["Context Eval"],
        "summary": "List pre-computed sample runs",
        "description": "Returns a list of pre-computed eval runs shipped with ByteWaveNetwork. Each entry includes the models used, the needle fact, and the question. Use the `runId` to fetch full results via `GET /evals/context-retrieval/sample/{runId}`.",
        "responses": {
          "200": {
            "description": "Sample run list",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/EvalSamplesResponse" }
              }
            }
          }
        }
      }
    },
    "/evals/context-retrieval/sample/{runId}": {
      "get": {
        "tags": ["Context Eval"],
        "summary": "Get full results for a sample run",
        "description": "Returns the complete `results.json` for a pre-computed sample run — all models, all positions, all trials, per-trial scored responses (tier, matchRatio, matchedWords, missedWords), and per-position summaries.",
        "parameters": [
          {
            "name": "runId",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "pattern": "^[a-zA-Z0-9_]+$" },
            "description": "Alphanumeric run identifier (path traversal characters are stripped)"
          }
        ],
        "responses": {
          "200": {
            "description": "Sample run detail",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/EvalSampleDetail" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/evals/context-retrieval/run": {
      "post": {
        "tags": ["Context Eval"],
        "summary": "Start a live eval run",
        "description": "Starts an asynchronous needle-in-haystack evaluation. The server injects `fact` into a long document at each `positions` percentage point and asks the model `question`. Each trial is scored for exact/partial/miss retrieval. Connect to `ws://host/ws?scanId={evalId}` to stream `eval:started → eval:progress → eval:position-done → eval:complete` frames.\n\n**Model support:** Pass a `models` array (1–4 IDs) for side-by-side comparison, or the legacy `modelId` string for a single-model run. All models must use the same `keyType`.\n\n**Custom document:** Supply `customDocument` to use your own text as the haystack instead of the built-in synthetic IT filler sentences. The needle is injected at the same `positions` percentages relative to the sentence count of your document.\n\n**Context size:** `tokens` controls the approximate size of the generated haystack (synthetic mode only). Preset values: 8 000, 50 000, 64 000, 128 000.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/EvalRunRequest" },
              "examples": {
                "single": {
                  "summary": "Single model, default positions",
                  "value": {
                    "apiKey": "sk-...",
                    "keyType": "openai",
                    "models": ["gpt-4o"],
                    "positions": [85, 90, 95],
                    "trials": 3,
                    "tokens": 50000,
                    "fact": "Project Helios launch date is March 7, 2031",
                    "question": "What is the launch date of Project Helios?"
                  }
                },
                "multi_model": {
                  "summary": "Multi-model comparison via OpenRouter",
                  "value": {
                    "apiKey": "sk-or-...",
                    "keyType": "openrouter",
                    "models": ["anthropic/claude-sonnet-4-6", "openai/gpt-4.1", "google/gemini-2.0-flash-001"],
                    "positions": [50, 75, 90, 95],
                    "trials": 2,
                    "tokens": 64000,
                    "fact": "The project code name is Falcon and the budget is $4.2M",
                    "question": "What is the project code name and budget?"
                  }
                },
                "custom_doc": {
                  "summary": "Custom document haystack",
                  "value": {
                    "apiKey": "gsk_...",
                    "keyType": "groq",
                    "models": ["llama-3.3-70b-versatile"],
                    "positions": [90],
                    "trials": 3,
                    "fact": "The CEO approved the merger on April 12, 2025",
                    "question": "When did the CEO approve the merger?",
                    "customDocument": "Paragraph one of your long document... Paragraph two... (provide full text here)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Eval accepted — connect to WebSocket using returned `evalId`",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/EvalRunStarted" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/seo-page-checker/check": {
      "post": {
        "tags": ["SEO Page Checker"],
        "summary": "Audit a single page (sync)",
        "description": "Fetches and audits a single URL against 14 on-page SEO checks. Returns a 0–100 score plus a list of individual check results with PASS / WARN / FAIL outcomes. Synchronous — responds with full results in one request.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SeoPageCheckerRequest" },
              "example": { "url": "https://example.com/blog/post" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Audit complete",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SeoPageCheckerResult" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": {
            "description": "Could not fetch the target URL",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": { "code": "FETCH_ERROR", "message": "connect ECONNREFUSED" } }
              }
            }
          }
        }
      }
    },
    "/seo-page-checker/scan": {
      "post": {
        "tags": ["SEO Page Checker"],
        "summary": "Start a sitemap-based multi-page scan (async)",
        "description": "Discovers all URLs from the given sitemap (or site root), then audits each page against the 14-point check suite. Returns immediately with a `scanId`. Connect to the WebSocket to receive per-page `check:progress` frames.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SeoPageCheckerScanRequest" },
              "examples": {
                "minimal": { "summary": "Site root (auto-discovers sitemap)", "value": { "url": "https://example.com" } },
                "full": { "summary": "All options", "value": { "url": "https://example.com", "depth": 3, "maxPages": 100 } }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Scan accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ScanStarted" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/seo-page-checker/scan/{scanId}": {
      "get": {
        "tags": ["SEO Page Checker"],
        "summary": "Get scan metadata and all page results",
        "parameters": [{ "$ref": "#/components/parameters/scanId" }],
        "responses": {
          "200": {
            "description": "Scan found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SeoPageCheckerScanDetail" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "delete": {
        "tags": ["SEO Page Checker"],
        "summary": "Cancel a running scan",
        "description": "Signals the scanner to stop after the current page completes. Broadcasts a `check:cancelled` frame.",
        "parameters": [{ "$ref": "#/components/parameters/scanId" }],
        "responses": {
          "200": {
            "description": "Stop signal sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "example": true },
                    "message": { "type": "string", "example": "Scan stop requested" }
                  }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/seo-page-checker/scans": {
      "get": {
        "tags": ["SEO Page Checker"],
        "summary": "List recent scans",
        "description": "Returns the last 20 SEO page checker scans (both single-page and sitemap-based), newest first.",
        "responses": {
          "200": {
            "description": "List of recent scans",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "scans": { "type": "array", "items": { "$ref": "#/components/schemas/SeoPageCheckerScanSummary" } }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/seo-page-checker/audits": {
      "get": {
        "tags": ["SEO Page Checker"],
        "summary": "List recent audits (legacy alias)",
        "description": "Legacy alias for `/seo-page-checker/scans` used by the original on-page-auditor frontend. Returns the same data wrapped under the `audits` key for backward compatibility.",
        "responses": {
          "200": {
            "description": "List of recent audits",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "audits": { "type": "array", "items": { "$ref": "#/components/schemas/SeoPageCheckerScanSummary" } }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/on-page-auditor/check": {
      "post": {
        "tags": ["SEO Page Checker"],
        "summary": "(Legacy alias) Audit a single page",
        "description": "Legacy path. Identical to `POST /seo-page-checker/check`. Use the new path for new integrations.",
        "deprecated": true,
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SeoPageCheckerCheckRequest" } } } },
        "responses": {
          "200": { "description": "Audit result", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SeoPageCheckerCheckResponse" } } } }
        }
      }
    },
    "/seo-page-checker/scan/{scanId}/export": {
      "get": {
        "tags": ["SEO Page Checker"],
        "summary": "Export page results as CSV or JSON",
        "parameters": [
          { "$ref": "#/components/parameters/scanId" },
          {
            "name": "format",
            "in": "query",
            "schema": { "type": "string", "enum": ["csv", "json"], "default": "csv" },
            "description": "Export format"
          }
        ],
        "responses": {
          "200": { "description": "File download" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/version": {
      "get": {
        "tags": ["System"],
        "summary": "App version",
        "description": "Returns the running app version and git commit hash.",
        "responses": {
          "200": {
            "description": "Version info",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "version": { "type": "string", "example": "1.0.0" },
                    "hash":    { "type": "string", "example": "a1b2c3d", "nullable": true }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/newsletter/subscribe": {
      "post": {
        "tags": ["Newsletter"],
        "summary": "Subscribe to newsletter",
        "description": "Validates the email address, inserts a pending subscriber, and sends a double opt-in confirmation email. If the email is already confirmed the request is silently accepted.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email"],
                "properties": {
                  "email": { "type": "string", "format": "email", "example": "hello@example.com" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmation email sent (or already confirmed)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok":      { "type": "boolean", "example": true },
                    "already": { "type": "boolean", "description": "true when the email was already confirmed", "nullable": true }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/newsletter/confirm": {
      "get": {
        "tags": ["Newsletter"],
        "summary": "Confirm subscription",
        "description": "Marks the subscriber as confirmed and redirects to `/?newsletter=confirmed`. Sends a welcome email.",
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": { "type": "string" },
            "description": "UUID confirmation token from the opt-in email"
          }
        ],
        "responses": {
          "302": { "description": "Redirect to /?newsletter=confirmed" },
          "400": { "description": "Missing or invalid token" }
        }
      }
    },
    "/newsletter/unsubscribe": {
      "get": {
        "tags": ["Newsletter"],
        "summary": "Unsubscribe",
        "description": "Sets `unsubscribed=true` for the subscriber and redirects to `/?newsletter=unsubscribed`.",
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": { "type": "string" },
            "description": "UUID unsubscribe token from any newsletter email"
          }
        ],
        "responses": {
          "302": { "description": "Redirect to /?newsletter=unsubscribed" },
          "400": { "description": "Missing or invalid token" }
        }
      }
    },
    "/evals/instruction-following/models": {
      "get": {
        "tags": ["Instruction Eval"],
        "summary": "List available models",
        "description": "Returns the model catalog — same catalog as the context retrieval eval.",
        "responses": {
          "200": {
            "description": "Model catalog",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/EvalModelsResponse" }
              }
            }
          }
        }
      }
    },
    "/evals/instruction-following/samples": {
      "get": {
        "tags": ["Instruction Eval"],
        "summary": "List pre-computed sample runs",
        "responses": {
          "200": {
            "description": "Sample run list",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/EvalSamplesResponse" }
              }
            }
          }
        }
      }
    },
    "/evals/instruction-following/sample/{runId}": {
      "get": {
        "tags": ["Instruction Eval"],
        "summary": "Get full results for a sample run",
        "parameters": [
          { "name": "runId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Sample run detail" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/evals/instruction-following/run": {
      "post": {
        "tags": ["Instruction Eval"],
        "summary": "Start a live instruction following eval",
        "description": "Starts an asynchronous multi-step instruction following evaluation. Tests whether models can follow layered instructions (easy/medium/hard difficulty levels). Connect to `ws://host/ws?scanId={evalId}` to stream `instruct:started → instruct:progress → instruct:difficulty-done → instruct:complete` frames.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "keyType", "models"],
                "properties": {
                  "apiKey":       { "type": "string", "description": "API key for the model provider" },
                  "keyType":      { "type": "string", "enum": ["anthropic","openai","openrouter","groq","deepseek"], "description": "Provider that issued the API key" },
                  "models":       { "type": "array", "items": { "type": "string" }, "minItems": 1, "maxItems": 4, "description": "Model IDs to test" },
                  "difficulties": { "type": "array", "items": { "type": "string", "enum": ["easy","medium","hard"] }, "default": ["easy","medium","hard"] },
                  "trials":       { "type": "integer", "minimum": 1, "maximum": 10, "default": 3, "description": "Trials per difficulty level" }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Eval accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/EvalRunStarted" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/evals/instruction-following/runs": {
      "get": {
        "tags": ["Instruction Eval"],
        "summary": "List recent runs (in-memory, last 20)",
        "responses": {
          "200": { "description": "Run history" }
        }
      }
    },
    "/evals/instruction-following/run/{evalId}": {
      "get": {
        "tags": ["Instruction Eval"],
        "summary": "Get full results for a completed run",
        "parameters": [
          { "name": "evalId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Run results" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "delete": {
        "tags": ["Instruction Eval"],
        "summary": "Cancel a running eval",
        "parameters": [
          { "name": "evalId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Cancelled" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/evals/instruction-following/run/{evalId}/export": {
      "get": {
        "tags": ["Instruction Eval"],
        "summary": "Export results as CSV or JSON",
        "parameters": [
          { "name": "evalId", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "format", "in": "query", "schema": { "type": "string", "enum": ["csv","json"], "default": "json" } }
        ],
        "responses": {
          "200": { "description": "File download" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/evals/agentic-loop/models": {
      "get": {
        "tags": ["Agentic Loop Eval"],
        "summary": "List available models",
        "responses": {
          "200": { "description": "Model catalog", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EvalModelsResponse" } } } }
        }
      }
    },
    "/evals/agentic-loop/samples": {
      "get": {
        "tags": ["Agentic Loop Eval"],
        "summary": "List pre-computed sample runs",
        "responses": {
          "200": { "description": "Sample run list", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EvalSamplesResponse" } } } }
        }
      }
    },
    "/evals/agentic-loop/sample/{runId}": {
      "get": {
        "tags": ["Agentic Loop Eval"],
        "summary": "Get full results for a sample run",
        "parameters": [
          { "name": "runId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Sample run detail" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/evals/agentic-loop/run": {
      "post": {
        "tags": ["Agentic Loop Eval"],
        "summary": "Start a live agentic loop eval",
        "description": "Starts an asynchronous 5-step agentic loop evaluation using real external APIs (Wikipedia, Open-Meteo, REST Countries). Tests whether a model completes the full research chain without abandoning tool calls. Connect to `ws://host/ws?scanId={evalId}` to stream `agent:started → agent:trial-done → agent:complete` frames.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "keyType", "models"],
                "properties": {
                  "apiKey":  { "type": "string", "description": "API key for the model provider" },
                  "keyType": { "type": "string", "enum": ["anthropic","openai","openrouter","groq","deepseek"] },
                  "models":  { "type": "array", "items": { "type": "string" }, "minItems": 1, "maxItems": 4 },
                  "trials":  { "type": "integer", "minimum": 1, "maximum": 10, "default": 3 },
                  "webhookUrl": { "type": "string", "format": "uri", "nullable": true, "description": "Optional URL to POST when eval completes" }
                }
              }
            }
          }
        },
        "responses": {
          "202": { "description": "Eval accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EvalRunStarted" } } } },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/evals/agentic-loop/runs": {
      "get": { "tags": ["Agentic Loop Eval"], "summary": "List recent runs (in-memory, last 20)", "responses": { "200": { "description": "Run history" } } }
    },
    "/evals/agentic-loop/run/{evalId}": {
      "get": {
        "tags": ["Agentic Loop Eval"],
        "summary": "Get full results for a completed run",
        "parameters": [ { "name": "evalId", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "Run results" }, "404": { "$ref": "#/components/responses/NotFound" } }
      },
      "delete": {
        "tags": ["Agentic Loop Eval"],
        "summary": "Cancel a running eval",
        "parameters": [ { "name": "evalId", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "Cancelled" }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/evals/agentic-loop/run/{evalId}/export": {
      "get": {
        "tags": ["Agentic Loop Eval"],
        "summary": "Export results as CSV or JSON",
        "parameters": [
          { "name": "evalId", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "format", "in": "query", "schema": { "type": "string", "enum": ["csv","json"], "default": "json" } }
        ],
        "responses": { "200": { "description": "File download" }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/evals/thinking-mode/models": {
      "get": { "tags": ["Thinking Mode Eval"], "summary": "List available models", "responses": { "200": { "description": "Model catalog", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EvalModelsResponse" } } } } } }
    },
    "/evals/thinking-mode/samples": {
      "get": { "tags": ["Thinking Mode Eval"], "summary": "List pre-computed sample runs", "responses": { "200": { "description": "Sample run list", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EvalSamplesResponse" } } } } } }
    },
    "/evals/thinking-mode/sample/{runId}": {
      "get": {
        "tags": ["Thinking Mode Eval"],
        "summary": "Get full results for a sample run",
        "parameters": [ { "name": "runId", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "Sample run detail" }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/evals/thinking-mode/run": {
      "post": {
        "tags": ["Thinking Mode Eval"],
        "summary": "Start a live thinking mode eval",
        "description": "Compares model output quality with reasoning/thinking mode ON vs OFF across math, coding, factual, and creative tasks. Each model is tested in both modes; an LLM judge scores every response 0–10. Connect to `ws://host/ws?scanId={evalId}` to stream `think:started → think:question-done → think:model-done → think:complete` frames.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "keyType", "models"],
                "properties": {
                  "apiKey":          { "type": "string" },
                  "keyType":         { "type": "string", "enum": ["anthropic","openai","openrouter","groq","deepseek"] },
                  "models":          { "type": "array", "items": { "type": "string" }, "minItems": 1, "maxItems": 4 },
                  "categories":      { "type": "array", "items": { "type": "string", "enum": ["math","coding","factual","creative"] }, "default": ["math","coding","factual","creative"] },
                  "thinkingModes":   { "type": "array", "items": { "type": "string", "enum": ["on","off"] }, "default": ["on","off"] },
                  "questionsPerCat": { "type": "integer", "minimum": 1, "maximum": 20, "default": 10 },
                  "webhookUrl":      { "type": "string", "format": "uri", "nullable": true }
                }
              }
            }
          }
        },
        "responses": {
          "202": { "description": "Eval accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EvalRunStarted" } } } },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/evals/thinking-mode/runs": {
      "get": { "tags": ["Thinking Mode Eval"], "summary": "List recent runs", "responses": { "200": { "description": "Run history" } } }
    },
    "/evals/thinking-mode/run/{evalId}": {
      "get": {
        "tags": ["Thinking Mode Eval"],
        "summary": "Get full results for a completed run",
        "parameters": [ { "name": "evalId", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "Run results" }, "404": { "$ref": "#/components/responses/NotFound" } }
      },
      "delete": {
        "tags": ["Thinking Mode Eval"],
        "summary": "Cancel a running eval",
        "parameters": [ { "name": "evalId", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "Cancelled" }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/evals/thinking-mode/run/{evalId}/export": {
      "get": {
        "tags": ["Thinking Mode Eval"],
        "summary": "Export results as CSV or JSON",
        "parameters": [
          { "name": "evalId", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "format", "in": "query", "schema": { "type": "string", "enum": ["csv","json"], "default": "json" } }
        ],
        "responses": { "200": { "description": "File download" }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/evals/prompt-sensitivity/models": {
      "get": { "tags": ["Prompt Sensitivity Eval"], "summary": "List available models", "responses": { "200": { "description": "Model catalog", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EvalModelsResponse" } } } } } }
    },
    "/evals/prompt-sensitivity/samples": {
      "get": { "tags": ["Prompt Sensitivity Eval"], "summary": "List pre-computed sample runs", "responses": { "200": { "description": "Sample run list", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EvalSamplesResponse" } } } } } }
    },
    "/evals/prompt-sensitivity/sample/{runId}": {
      "get": {
        "tags": ["Prompt Sensitivity Eval"],
        "summary": "Get full results for a sample run",
        "parameters": [ { "name": "runId", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "Sample run detail" }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/evals/prompt-sensitivity/run": {
      "post": {
        "tags": ["Prompt Sensitivity Eval"],
        "summary": "Start a live prompt sensitivity eval",
        "description": "Measures how output quality varies across 10 phrasing variants of the same task (terse to verbose, formal to informal, zero-shot to few-shot). Tracks per-model score standard deviation as the brittleness metric. Connect to `ws://host/ws?scanId={evalId}` to stream `prompt:started → prompt:variant-done → prompt:task-done → prompt:model-done → prompt:complete` frames.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apiKey", "keyType", "models"],
                "properties": {
                  "apiKey":  { "type": "string" },
                  "keyType": { "type": "string", "enum": ["anthropic","openai","openrouter","groq","deepseek"] },
                  "models":  { "type": "array", "items": { "type": "string" }, "minItems": 1, "maxItems": 4 },
                  "tasks":   { "type": "array", "items": { "type": "string", "enum": ["summarise","classify","generate"] }, "default": ["summarise","classify","generate"] },
                  "webhookUrl": { "type": "string", "format": "uri", "nullable": true }
                }
              }
            }
          }
        },
        "responses": {
          "202": { "description": "Eval accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EvalRunStarted" } } } },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/evals/prompt-sensitivity/runs": {
      "get": { "tags": ["Prompt Sensitivity Eval"], "summary": "List recent runs", "responses": { "200": { "description": "Run history" } } }
    },
    "/evals/prompt-sensitivity/run/{evalId}": {
      "get": {
        "tags": ["Prompt Sensitivity Eval"],
        "summary": "Get full results for a completed run",
        "parameters": [ { "name": "evalId", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "Run results" }, "404": { "$ref": "#/components/responses/NotFound" } }
      },
      "delete": {
        "tags": ["Prompt Sensitivity Eval"],
        "summary": "Cancel a running eval",
        "parameters": [ { "name": "evalId", "in": "path", "required": true, "schema": { "type": "string" } } ],
        "responses": { "200": { "description": "Cancelled" }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/evals/prompt-sensitivity/run/{evalId}/export": {
      "get": {
        "tags": ["Prompt Sensitivity Eval"],
        "summary": "Export results as CSV or JSON",
        "parameters": [
          { "name": "evalId", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "format", "in": "query", "schema": { "type": "string", "enum": ["csv","json"], "default": "json" } }
        ],
        "responses": { "200": { "description": "File download" }, "404": { "$ref": "#/components/responses/NotFound" } }
      }
    },
    "/health": {
      "get": {
        "tags": ["System"],
        "summary": "Health check",
        "description": "Returns server health including database connectivity. Use this to confirm the service is alive before sending traffic (e.g. in monitoring dashboards, CI pipelines, or AI agent preflight checks).",
        "responses": {
          "200": {
            "description": "Service healthy",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Health" },
                "example": {
                  "status": "ok",
                  "version": "1.0.0",
                  "uptime": 3600,
                  "db": "ok",
                  "timestamp": "2026-05-08T12:00:00.000Z"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "scanId": {
        "name": "scanId",
        "in": "path",
        "required": true,
        "description": "UUID of the scan",
        "schema": { "type": "string", "format": "uuid" },
        "example": "a701030c-2d94-40da-a1ce-a9cfa27f45b9"
      }
    },
    "responses": {
      "NotFound": {
        "description": "Scan not found",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": { "code": "NOT_FOUND", "message": "Scan not found" } }
          }
        }
      },
      "ValidationError": {
        "description": "Invalid request body",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": { "code": "VALIDATION_ERROR", "message": "depth must be 1–10" } }
          }
        }
      },
      "RateLimited": {
        "description": "Too many scan requests",
        "headers": {
          "Retry-After": { "schema": { "type": "integer" }, "description": "Seconds to wait before retrying" }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": { "code": "RATE_LIMITED", "message": "Too many requests, please slow down" } }
          }
        }
      }
    },
    "schemas": {
      "LinkScanRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Seed URL to start crawling from. Must use http or https.",
            "example": "https://example.com"
          },
          "depth": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10,
            "default": 3,
            "description": "Maximum BFS crawl depth from the seed URL."
          },
          "concurrency": {
            "type": "integer",
            "minimum": 1,
            "maximum": 20,
            "default": 5,
            "description": "Number of URLs checked in parallel."
          },
          "respectRobots": {
            "type": "boolean",
            "default": true,
            "description": "When true, fetches and obeys the target site's robots.txt. Disable when auditing your own site."
          },
          "slowThresholdInternalMs": {
            "type": "integer",
            "minimum": 500,
            "maximum": 10000,
            "default": 2000,
            "description": "Response time (ms) above which an internal link is flagged slow."
          },
          "slowThresholdExternalMs": {
            "type": "integer",
            "minimum": 500,
            "maximum": 10000,
            "default": 5000,
            "description": "Response time (ms) above which an external link is flagged slow."
          },
          "webhookUrl": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "Optional URL to POST when the scan completes. Payload: `{ scanId, stats, url }`.",
            "example": "https://webhook.site/your-id"
          },
          "excludePatterns": {
            "type": "array",
            "items": { "type": "string" },
            "nullable": true,
            "description": "URL patterns to skip during the crawl. Supports glob wildcards (`*`) and prefix matching.",
            "example": ["*/admin/*", "/staging"]
          },
          "bypassHeader": {
            "type": "object",
            "nullable": true,
            "description": "HTTP header injected on internal requests only (e.g. Vercel protection bypass). Never sent to external domains.",
            "properties": {
              "name": { "type": "string", "example": "x-vercel-protection-bypass" },
              "value": { "type": "string", "example": "your-bypass-secret" }
            },
            "required": ["name", "value"]
          }
        }
      },
      "SitemapValidateRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url":         { "type": "string", "format": "uri", "description": "URL of the XML sitemap to validate.", "example": "https://example.com/sitemap.xml" },
          "concurrency": { "type": "integer", "minimum": 1, "maximum": 10, "default": 5 },
          "maxUrls":     { "type": "integer", "minimum": 1, "maximum": 500, "default": 200 }
        }
      },
      "SitemapValidationStarted": {
        "type": "object",
        "properties": {
          "validationId": { "type": "string", "format": "uuid" },
          "status":       { "type": "string", "example": "pending" },
          "wsUrl":        { "type": "string", "example": "ws://localhost:3000/ws?scanId=<validationId>" },
          "url":          { "type": "string", "format": "uri" }
        }
      },
      "SitemapValidationSummary": {
        "type": "object",
        "properties": {
          "id":            { "type": "string", "format": "uuid" },
          "url":           { "type": "string", "format": "uri" },
          "status":        { "type": "string", "enum": ["pending","running","done","cancelled","failed"] },
          "total_urls":    { "type": "integer" },
          "ok_urls":       { "type": "integer" },
          "broken_urls":   { "type": "integer" },
          "redirect_urls": { "type": "integer" },
          "noindex_urls":  { "type": "integer" },
          "error_urls":    { "type": "integer" },
          "created_at":    { "type": "string", "format": "date-time" },
          "completed_at":  { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "SitemapUrl": {
        "type": "object",
        "properties": {
          "url":               { "type": "string", "format": "uri" },
          "http_status":       { "type": "integer", "nullable": true },
          "response_ms":       { "type": "integer" },
          "is_noindex":        { "type": "integer", "enum": [0,1] },
          "canonical_url":     { "type": "string", "nullable": true },
          "canonical_mismatch":{ "type": "integer", "enum": [0,1] },
          "lastmod":           { "type": "string", "nullable": true },
          "changefreq":        { "type": "string", "nullable": true },
          "priority":          { "type": "number", "nullable": true },
          "source_sitemap":    { "type": "string", "nullable": true },
          "status_label":      { "type": "string", "enum": ["ok","broken","redirect","error"] },
          "error":             { "type": "string", "nullable": true }
        }
      },
      "SitemapUrlPage": {
        "type": "object",
        "properties": {
          "urls":  { "type": "array", "items": { "$ref": "#/components/schemas/SitemapUrl" } },
          "total": { "type": "integer" },
          "page":  { "type": "integer" },
          "pages": { "type": "integer" }
        }
      },
      "SchemaTestRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": { "type": "string", "format": "uri", "description": "Page URL to extract and validate structured data from.", "example": "https://example.com/page" }
        }
      },
      "SchemaItem": {
        "type": "object",
        "properties": {
          "format":     { "type": "string", "enum": ["json-ld","microdata","rdfa"] },
          "type":       { "type": "string", "description": "Simplified type name (e.g. FAQPage)", "example": "FAQPage" },
          "typeRaw":    { "type": "string", "description": "Raw @type value from the document", "example": "https://schema.org/FAQPage" },
          "context":    { "type": "string", "nullable": true, "example": "https://schema.org" },
          "properties": { "type": "object", "description": "Extracted property key-value pairs" },
          "parseError": { "type": "string", "nullable": true, "description": "JSON parse error (JSON-LD only)" },
          "issues":     { "type": "array", "items": {
            "type": "object",
            "properties": {
              "severity": { "type": "string", "enum": ["error","warning"] },
              "code":     { "type": "string", "example": "MISSING_REQUIRED_PROP" },
              "message":  { "type": "string" }
            }
          }}
        }
      },
      "SchemaTestSummary": {
        "type": "object",
        "properties": {
          "id":              { "type": "string", "format": "uuid" },
          "url":             { "type": "string", "format": "uri" },
          "http_status":     { "type": "integer" },
          "total_schemas":   { "type": "integer" },
          "error_count":     { "type": "integer" },
          "warning_count":   { "type": "integer" },
          "created_at":      { "type": "string", "format": "date-time" }
        }
      },
      "SchemaTestResult": {
        "type": "object",
        "properties": {
          "testId":      { "type": "string", "format": "uuid" },
          "url":         { "type": "string", "format": "uri" },
          "httpStatus":  { "type": "integer" },
          "responseMs":  { "type": "integer" },
          "schemas":     { "type": "array", "items": { "$ref": "#/components/schemas/SchemaItem" } },
          "summary": {
            "type": "object",
            "properties": {
              "totalSchemas":   { "type": "integer" },
              "jsonLdCount":    { "type": "integer" },
              "microdataCount": { "type": "integer" },
              "rdfaCount":      { "type": "integer" },
              "errorCount":     { "type": "integer" },
              "warningCount":   { "type": "integer" }
            }
          }
        }
      },
      "SchemaScanRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "URL of the XML sitemap to crawl for schema testing.",
            "example": "https://example.com/sitemap.xml"
          },
          "maxUrls": {
            "type": "integer",
            "minimum": 1,
            "maximum": 500,
            "default": 200,
            "description": "Maximum number of URLs to test from the sitemap."
          },
          "concurrency": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5,
            "default": 3,
            "description": "Number of pages tested in parallel."
          }
        }
      },
      "SchemaScanStarted": {
        "type": "object",
        "properties": {
          "scanId":      { "type": "string", "format": "uuid", "example": "a701030c-2d94-40da-a1ce-a9cfa27f45b9" },
          "status":      { "type": "string", "example": "pending" },
          "wsUrl":       { "type": "string", "example": "ws://localhost:3000/ws?scanId=a701030c-2d94-40da-a1ce-a9cfa27f45b9" },
          "sitemapUrl":  { "type": "string", "format": "uri", "example": "https://example.com/sitemap.xml" }
        }
      },
      "SchemaScanSummary": {
        "type": "object",
        "properties": {
          "id":           { "type": "string", "format": "uuid" },
          "sitemap_url":  { "type": "string", "format": "uri" },
          "status":       { "type": "string", "enum": ["running", "complete", "cancelled", "failed"] },
          "total_urls":   { "type": "integer", "description": "Total URLs found in the sitemap" },
          "checked_urls": { "type": "integer", "description": "URLs tested so far" },
          "created_at":   { "type": "string", "format": "date-time" }
        }
      },
      "SchemaScanDetail": {
        "type": "object",
        "properties": {
          "scan": { "$ref": "#/components/schemas/SchemaScanSummary" },
          "pages": {
            "type": "array",
            "description": "Per-page schema test results with additional timing and count fields",
            "items": {
              "allOf": [
                { "$ref": "#/components/schemas/SchemaTestSummary" },
                {
                  "type": "object",
                  "properties": {
                    "response_ms":       { "type": "integer", "description": "Page fetch time in milliseconds" },
                    "json_ld_count":     { "type": "integer", "description": "Number of JSON-LD blocks found" },
                    "microdata_count":   { "type": "integer", "description": "Number of Microdata items found" },
                    "rdfa_count":        { "type": "integer", "description": "Number of RDFa items found" }
                  }
                }
              ]
            }
          }
        }
      },
      "TraceRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": { "type": "string", "format": "uri", "description": "URL to trace redirects from.", "example": "https://example.com" },
          "maxHops": { "type": "integer", "minimum": 1, "maximum": 20, "default": 20, "description": "Maximum redirect hops to follow before stopping." }
        }
      },
      "TraceHop": {
        "type": "object",
        "properties": {
          "url":         { "type": "string", "format": "uri" },
          "status":      { "type": "integer", "nullable": true, "example": 301 },
          "statusText":  { "type": "string", "nullable": true, "example": "Moved Permanently" },
          "location":    { "type": "string", "nullable": true, "description": "Value of the Location header, if present." },
          "server":      { "type": "string", "nullable": true },
          "contentType": { "type": "string", "nullable": true },
          "cacheControl":{ "type": "string", "nullable": true },
          "httpVersion": { "type": "string", "example": "1.1" },
          "responseMs":  { "type": "integer", "description": "Round-trip time in milliseconds for this hop." },
          "error":       { "type": "string", "nullable": true, "description": "Set when the request failed (e.g. TIMEOUT, ECONNREFUSED)." }
        }
      },
      "TraceSummary": {
        "type": "object",
        "properties": {
          "id":            { "type": "string", "format": "uuid" },
          "url":           { "type": "string", "format": "uri" },
          "hop_count":     { "type": "integer" },
          "redirect_count":{ "type": "integer" },
          "final_url":     { "type": "string", "nullable": true },
          "final_status":  { "type": "integer", "nullable": true },
          "total_ms":      { "type": "integer" },
          "has_loop":      { "type": "integer", "enum": [0, 1] },
          "created_at":    { "type": "string", "format": "date-time" }
        }
      },
      "TraceResult": {
        "type": "object",
        "properties": {
          "traceId":       { "type": "string", "format": "uuid" },
          "url":           { "type": "string", "format": "uri" },
          "hops":          { "type": "array", "items": { "$ref": "#/components/schemas/TraceHop" } },
          "hopCount":      { "type": "integer" },
          "redirectCount": { "type": "integer" },
          "finalUrl":      { "type": "string", "nullable": true },
          "finalStatus":   { "type": "integer", "nullable": true },
          "hasLoop":       { "type": "boolean" },
          "totalMs":       { "type": "integer" }
        }
      },
      "PageSpeedRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": { "type": "string", "format": "uri", "description": "Page URL to inspect.", "example": "https://example.com" }
        }
      },
      "PageSpeedMetrics": {
        "type": "object",
        "properties": {
          "dnsMs":                    { "type": "integer", "description": "DNS lookup time in ms" },
          "tcpMs":                    { "type": "integer", "description": "TCP connection time in ms" },
          "tlsMs":                    { "type": "integer", "description": "TLS handshake time in ms (0 for HTTP)" },
          "ttfbMs":                   { "type": "integer", "description": "Time to first byte (server processing) in ms" },
          "downloadMs":               { "type": "integer", "description": "Body download time in ms" },
          "totalMs":                  { "type": "integer", "description": "Total elapsed time in ms" },
          "httpVersion":              { "type": "string", "example": "2.0" },
          "status":                   { "type": "integer", "example": 200 },
          "compression":              { "type": "string", "example": "gzip", "description": "Content-Encoding header value or 'none'" },
          "cachePolicy":              { "type": "string", "enum": ["immutable", "long", "short", "revalidate", "isr", "no-cache"], "description": "immutable/long = cached for hours; isr = Next.js ISR (max-age=0 must-revalidate, CDN revalidates in background); revalidate = stale-while-revalidate; no-cache = not cached" },
          "cacheControl":             { "type": "string", "nullable": true },
          "server":                   { "type": "string", "nullable": true },
          "contentType":              { "type": "string", "nullable": true },
          "transferSize":             { "type": "integer", "description": "Raw bytes received" },
          "renderBlockingScripts":    { "type": "integer" },
          "renderBlockingStyles":     { "type": "integer" },
          "imagesTotal":              { "type": "integer" },
          "imagesMissingDimensions":  { "type": "integer" },
          "hasViewport":              { "type": "boolean" },
          "domNodeCount":             { "type": "integer", "description": "Total DOM nodes in the page" },
          "thirdPartyScriptCount":    { "type": "integer", "description": "Number of third-party scripts detected" },
          "inlineCssBytes":           { "type": "integer", "description": "Total bytes of inline CSS (style attributes + style blocks)" },
          "inlineJsBytes":            { "type": "integer", "description": "Total bytes of inline JavaScript, excluding Next.js RSC flight payloads (script blocks without src)" },
          "inlineJsRscBytes":         { "type": "integer", "description": "Bytes of Next.js RSC flight payload scripts (self.__next_f.push) — data, not executable code; excluded from inlineJsBytes" },
          "imagesMissingLazyCount":   { "type": "integer", "description": "Number of below-fold images without loading=lazy" },
          "lcpCandidate":             { "type": "string", "nullable": true, "description": "URL of the largest image candidate for Largest Contentful Paint" }
        }
      },
      "PageSpeedRecommendation": {
        "type": "object",
        "properties": {
          "priority": { "type": "string", "enum": ["high", "medium", "low"] },
          "code":     { "type": "string", "example": "SLOW_TTFB" },
          "message":  { "type": "string", "example": "Server response time exceeds 1.5s. Consider caching or a CDN." },
          "details":  { "type": "array", "items": { "type": "string" }, "nullable": true, "description": "List of affected file URLs or resource names" }
        }
      },
      "PageSpeedSummary": {
        "type": "object",
        "properties": {
          "id":           { "type": "string", "format": "uuid" },
          "url":          { "type": "string", "format": "uri" },
          "http_status":  { "type": "integer", "nullable": true },
          "ttfb_ms":      { "type": "integer" },
          "total_ms":     { "type": "integer" },
          "score":        { "type": "integer", "minimum": 0, "maximum": 100 },
          "compression":  { "type": "string" },
          "cache_policy": { "type": "string" },
          "created_at":   { "type": "string", "format": "date-time" }
        }
      },
      "PageSpeedResult": {
        "type": "object",
        "properties": {
          "inspectionId":    { "type": "string", "format": "uuid" },
          "url":             { "type": "string", "format": "uri" },
          "metrics":         { "$ref": "#/components/schemas/PageSpeedMetrics" },
          "score":           { "type": "integer", "minimum": 0, "maximum": 100, "description": "Overall performance score (0–100). Deductions for slow TTFB, missing compression, no cache, HTTP/1.1, render-blocking resources." },
          "deductions":      { "type": "array", "items": { "type": "object", "properties": { "reason": { "type": "string" }, "pts": { "type": "integer" } } } },
          "recommendations": { "type": "array", "items": { "$ref": "#/components/schemas/PageSpeedRecommendation" } }
        }
      },
      "SeoScanRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Seed URL to start crawling from. Must use http or https.",
            "example": "https://example.com"
          },
          "depth": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10,
            "default": 3,
            "description": "Maximum BFS crawl depth. Only internal HTML pages are crawled."
          },
          "concurrency": {
            "type": "integer",
            "minimum": 1,
            "maximum": 20,
            "default": 5,
            "description": "Number of pages analyzed in parallel."
          },
          "respectRobots": {
            "type": "boolean",
            "default": true,
            "description": "When true, obeys the site's robots.txt. Disable when auditing your own site."
          },
          "webhookUrl": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "Optional URL to POST when the SEO scan completes. Payload: `{ scanId, stats, url }`.",
            "example": "https://webhook.site/your-id"
          }
        }
      },
      "ScanStarted": {
        "type": "object",
        "properties": {
          "scanId": { "type": "string", "format": "uuid", "example": "a701030c-2d94-40da-a1ce-a9cfa27f45b9" },
          "status": { "type": "string", "example": "pending" },
          "wsUrl": { "type": "string", "example": "ws://localhost:3000/ws?scanId=a701030c-2d94-40da-a1ce-a9cfa27f45b9" },
          "url": { "type": "string", "example": "https://example.com" }
        }
      },
      "ScanStatus": {
        "type": "string",
        "enum": ["pending", "running", "done", "cancelled", "failed"],
        "description": "- `pending`: queued, not yet started\n- `running`: BFS crawl in progress\n- `done`: completed normally\n- `cancelled`: stopped via DELETE endpoint\n- `failed`: unrecoverable error during crawl"
      },
      "LinkScanSummary": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "url": { "type": "string", "format": "uri" },
          "status": { "$ref": "#/components/schemas/ScanStatus" },
          "total_links": { "type": "integer", "nullable": true },
          "broken_links": { "type": "integer", "nullable": true },
          "redirect_chains": { "type": "integer", "nullable": true },
          "slow_links": { "type": "integer", "nullable": true },
          "created_at": { "type": "string", "format": "date-time" },
          "completed_at": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "LinkScanDetail": {
        "type": "object",
        "properties": {
          "scan": {
            "allOf": [
              { "$ref": "#/components/schemas/LinkScanSummary" },
              {
                "type": "object",
                "properties": {
                  "depth": { "type": "integer" },
                  "concurrency": { "type": "integer" },
                  "respect_robots": { "type": "integer", "description": "1 = true, 0 = false" },
                  "error_message": { "type": "string", "nullable": true },
                  "webhook_url": { "type": "string", "nullable": true },
                  "slow_threshold_internal_ms": { "type": "integer" },
                  "slow_threshold_external_ms": { "type": "integer" },
                  "exclude_patterns": { "type": "string", "nullable": true, "description": "JSON-stringified array of excluded patterns" }
                }
              }
            ]
          },
          "stats": { "$ref": "#/components/schemas/LinkScanStats" }
        }
      },
      "LinkScanStats": {
        "type": "object",
        "properties": {
          "total": { "type": "integer", "description": "Total links checked" },
          "broken": { "type": "integer", "description": "Links returning 4xx/5xx (excluding 403 and 429)" },
          "blocked": { "type": "integer", "description": "Links returning 403 or 429 — blocked by the target, not necessarily broken" },
          "redirects": { "type": "integer", "description": "Links that redirected (3xx chain)" },
          "slow": { "type": "integer", "description": "Total slow links (internal + external)" },
          "slowInternal": { "type": "integer", "description": "Internal links exceeding slowThresholdInternalMs" },
          "slowExternal": { "type": "integer", "description": "External links exceeding slowThresholdExternalMs" },
          "skipped": { "type": "integer", "description": "Non-HTTP links skipped (mailto:, tel:, javascript:)" },
          "avgResponseMs": { "type": "number", "nullable": true, "description": "Average response time across all checked links (ms)" }
        }
      },
      "Link": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "scan_id": { "type": "string", "format": "uuid" },
          "url": { "type": "string", "format": "uri" },
          "source_url": { "type": "string", "format": "uri", "nullable": true, "description": "Page where this link was found" },
          "link_type": {
            "type": "string",
            "enum": ["internal", "external", "image", "stylesheet", "script"]
          },
          "http_status": {
            "oneOf": [
              { "type": "integer", "description": "HTTP status code (e.g. 200, 404)" },
              { "type": "string", "enum": ["SKIP"], "description": "Non-HTTP link that was not checked" }
            ],
            "nullable": true,
            "description": "null if a network error prevented any response"
          },
          "redirect_url": { "type": "string", "nullable": true, "description": "Final URL after following the redirect chain" },
          "response_ms": { "type": "integer", "nullable": true, "description": "Time to first response in milliseconds" },
          "is_broken": { "type": "integer", "enum": [0, 1], "description": "1 if HTTP status ≥ 400 (excluding 403 and 429)" },
          "is_redirect": { "type": "integer", "enum": [0, 1] },
          "is_slow": { "type": "integer", "enum": [0, 1], "description": "1 if response_ms exceeded the applicable slow threshold" },
          "ssl_error": { "type": "string", "nullable": true, "description": "SSL error code if a certificate problem was detected (e.g. CERT_HAS_EXPIRED)" },
          "error_message": { "type": "string", "nullable": true, "description": "Human-readable error for network-level failures" },
          "depth_level": { "type": "integer", "description": "BFS depth at which this link was found (0 = seed page)" },
          "checked_at": { "type": "string", "format": "date-time" },
          "canonical_url": { "type": "string", "nullable": true, "description": "Value of <link rel=canonical> on this page, if present" },
          "is_noindex": { "type": "integer", "enum": [0, 1], "description": "1 if the page has noindex directive (meta robots or X-Robots-Tag header)" },
          "browser_verified": { "type": "integer", "enum": [0, 1], "description": "1 if re-checked via headless Chromium after initial 403/429" }
        }
      },
      "LinksPage": {
        "type": "object",
        "properties": {
          "links": { "type": "array", "items": { "$ref": "#/components/schemas/Link" } },
          "total": { "type": "integer", "description": "Total matching links (for pagination)" },
          "page": { "type": "integer" },
          "limit": { "type": "integer" }
        }
      },
      "LinkExportJson": {
        "type": "object",
        "properties": {
          "scan": { "$ref": "#/components/schemas/LinkScanSummary" },
          "exportedAt": { "type": "string", "format": "date-time" },
          "links": { "type": "array", "items": { "$ref": "#/components/schemas/Link" } }
        }
      },
      "HealSuggestion": {
        "type": "object",
        "properties": {
          "broken": { "type": "string", "format": "uri", "description": "The 404 URL" },
          "suggestion": { "type": "string", "format": "uri", "description": "Best matching URL from sitemap.xml" },
          "confidence": { "type": "number", "minimum": 0, "maximum": 1, "description": "Similarity score (0–1). Only suggestions ≥ 0.3 are returned." }
        }
      },
      "SeoScanSummary": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "url": { "type": "string", "format": "uri" },
          "status": { "$ref": "#/components/schemas/ScanStatus" },
          "total_pages": { "type": "integer", "nullable": true, "description": "Total pages crawled" },
          "error_pages": { "type": "integer", "nullable": true, "description": "Pages with at least one error-level issue" },
          "warning_pages": { "type": "integer", "nullable": true, "description": "Pages with at least one warning-level issue" },
          "noindex_pages": { "type": "integer", "nullable": true, "description": "Pages with a noindex directive" },
          "created_at": { "type": "string", "format": "date-time" },
          "completed_at": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "SeoScanDetail": {
        "type": "object",
        "properties": {
          "scan": {
            "allOf": [
              { "$ref": "#/components/schemas/SeoScanSummary" },
              {
                "type": "object",
                "properties": {
                  "depth": { "type": "integer" },
                  "concurrency": { "type": "integer" },
                  "error_message": { "type": "string", "nullable": true }
                }
              }
            ]
          },
          "stats": { "$ref": "#/components/schemas/SeoScanStats" }
        }
      },
      "SeoScanStats": {
        "type": "object",
        "properties": {
          "total": { "type": "integer", "description": "Total pages crawled" },
          "errorPages": { "type": "integer", "description": "Pages with at least one error-severity issue" },
          "warningPages": { "type": "integer", "description": "Pages with at least one warning-severity issue" },
          "noindexPages": { "type": "integer", "description": "Pages with noindex directive" },
          "missingTitles": { "type": "integer", "description": "Pages with no <title> tag" },
          "missingDescriptions": { "type": "integer", "description": "Pages with no meta description" },
          "avgScore": { "type": "number", "nullable": true, "description": "Average SEO score across all crawled pages (0–100)" }
        }
      },
      "SeoIssue": {
        "type": "object",
        "properties": {
          "severity": { "type": "string", "enum": ["error", "warning", "info"] },
          "code": {
            "type": "string",
            "description": "Machine-readable issue code",
            "enum": [
              "TITLE_MISSING", "TITLE_TOO_SHORT", "TITLE_TOO_LONG", "TITLE_DUPLICATE",
              "DESC_MISSING", "DESC_TOO_SHORT", "DESC_TOO_LONG", "DESC_DUPLICATE",
              "H1_MISSING", "H1_MULTIPLE", "HEADING_SKIP",
              "CANONICAL_MISSING", "CANONICAL_MISMATCH",
              "OG_TITLE_MISSING", "OG_IMAGE_MISSING", "OG_DESC_MISSING", "OG_URL_MISSING",
              "TWITTER_CARD_MISSING", "NOINDEX_DETECTED", "VIEWPORT_MISSING",
              "IMAGES_MISSING_ALT", "JSON_LD_MISSING"
            ]
          },
          "message": { "type": "string", "description": "Human-readable description", "example": "Title is 18 chars (min 30)" }
        }
      },
      "SeoPage": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "scan_id": { "type": "string", "format": "uuid" },
          "url": { "type": "string", "format": "uri" },
          "http_status": { "type": "integer", "nullable": true },
          "response_ms": { "type": "integer", "nullable": true },
          "title": { "type": "string", "nullable": true },
          "title_length": { "type": "integer", "nullable": true },
          "meta_description": { "type": "string", "nullable": true },
          "meta_description_length": { "type": "integer", "nullable": true },
          "h1_count": { "type": "integer", "description": "Number of <h1> tags on the page" },
          "h1_text": { "type": "string", "nullable": true, "description": "Text content of the first <h1>" },
          "h2_count": { "type": "integer" },
          "has_canonical": { "type": "integer", "enum": [0, 1] },
          "canonical_url": { "type": "string", "nullable": true },
          "canonical_type": {
            "type": "string",
            "nullable": true,
            "enum": ["self", "other", "missing"],
            "description": "- `self`: canonical points to this page\n- `other`: canonical defers to a different URL\n- `missing`: no canonical tag present"
          },
          "has_og": { "type": "integer", "enum": [0, 1], "description": "1 if any og: meta tag is present" },
          "og_title": { "type": "string", "nullable": true },
          "og_image": { "type": "string", "nullable": true },
          "twitter_card": { "type": "string", "nullable": true, "example": "summary_large_image" },
          "is_noindex": { "type": "integer", "enum": [0, 1] },
          "has_viewport": { "type": "integer", "enum": [0, 1] },
          "has_json_ld": { "type": "integer", "enum": [0, 1] },
          "json_ld_types": { "type": "string", "nullable": true, "description": "JSON-stringified array of @type values found in JSON-LD blocks" },
          "images_missing_alt": { "type": "integer", "description": "Number of <img> elements missing the alt attribute" },
          "score": { "type": "integer", "minimum": 0, "maximum": 100, "description": "SEO health score. Formula: max(0, 100 − errors×15 − warnings×5 − info×2)" },
          "error_count": { "type": "integer" },
          "warning_count": { "type": "integer" },
          "info_count": { "type": "integer" },
          "issues": {
            "type": "string",
            "nullable": true,
            "description": "JSON-stringified array of SeoIssue objects",
            "example": "[{\"severity\":\"warning\",\"code\":\"DESC_TOO_SHORT\",\"message\":\"Meta description is 42 chars (min 50)\"}]"
          },
          "checked_at": { "type": "string", "format": "date-time" }
        }
      },
      "SeoPagesPage": {
        "type": "object",
        "properties": {
          "pages": { "type": "array", "items": { "$ref": "#/components/schemas/SeoPage" } },
          "total": { "type": "integer", "description": "Total matching pages (for pagination)" },
          "page": { "type": "integer" },
          "limit": { "type": "integer" }
        }
      },
      "SeoExportJson": {
        "type": "object",
        "properties": {
          "scan": { "$ref": "#/components/schemas/SeoScanSummary" },
          "pages": { "type": "array", "items": { "$ref": "#/components/schemas/SeoPage" } }
        }
      },
      "LinkConfig": {
        "type": "object",
        "properties": {
          "defaultDepth": { "type": "integer", "example": 3 },
          "defaultConcurrency": { "type": "integer", "example": 5 },
          "defaultSlowThresholdInternalMs": { "type": "integer", "example": 2000 },
          "defaultSlowThresholdExternalMs": { "type": "integer", "example": 5000 },
          "maxUrls": { "type": "integer", "example": 500, "description": "Maximum URLs visited per scan" },
          "timeoutMs": { "type": "integer", "example": 10000, "description": "Per-request timeout in milliseconds" }
        }
      },
      "SeoConfig": {
        "type": "object",
        "properties": {
          "defaultDepth": { "type": "integer", "example": 3 },
          "defaultConcurrency": { "type": "integer", "example": 5 },
          "maxPages": { "type": "integer", "example": 200, "description": "Maximum pages analyzed per scan" },
          "timeoutMs": { "type": "integer", "example": 10000, "description": "Per-request timeout in milliseconds" }
        }
      },
      "EvalModel": {
        "type": "object",
        "required": ["id", "name", "vendor", "keyType", "contextK", "inputPer1M", "outputPer1M"],
        "properties": {
          "id":          { "type": "string", "example": "claude-sonnet-4-6", "description": "Model identifier to pass in `models[]`" },
          "name":        { "type": "string", "example": "Claude Sonnet 4.6" },
          "vendor":      { "type": "string", "example": "Anthropic", "description": "Underlying provider name" },
          "keyType":     { "type": "string", "enum": ["anthropic","openai","google","mistral","xai","groq","deepseek","cohere","openrouter"], "description": "Required API key type for this model" },
          "contextK":    { "type": "integer", "example": 200, "description": "Advertised context window in thousands of tokens" },
          "inputPer1M":  { "type": "number", "example": 3.00, "description": "Input cost in USD per 1 million tokens" },
          "outputPer1M": { "type": "number", "example": 15.00, "description": "Output cost in USD per 1 million tokens" }
        }
      },
      "EvalModelsResponse": {
        "type": "object",
        "properties": {
          "models": { "type": "array", "items": { "$ref": "#/components/schemas/EvalModel" } }
        }
      },
      "EvalSampleSummary": {
        "type": "object",
        "properties": {
          "runId":    { "type": "string", "example": "20250512_154200" },
          "models":   { "type": "array", "items": { "type": "string" }, "example": ["claude-sonnet-4-6", "gpt-4o"] },
          "fact":     { "type": "string", "example": "Project Helios launch date is March 7, 2031" },
          "question": { "type": "string", "example": "What is the launch date of Project Helios?" }
        }
      },
      "EvalSamplesResponse": {
        "type": "object",
        "properties": {
          "samples": { "type": "array", "items": { "$ref": "#/components/schemas/EvalSampleSummary" } }
        }
      },
      "EvalTrialResult": {
        "type": "object",
        "properties": {
          "trial":           { "type": "integer", "example": 1 },
          "positionPct":     { "type": "number", "example": 90, "description": "Needle insertion position as a percentage of document length" },
          "response":        { "type": "string", "description": "Raw model response text" },
          "tier":            { "type": "string", "enum": ["exact","partial","miss","error","dry-run"], "description": "Retrieval quality tier" },
          "matchRatio":      { "type": "number", "minimum": 0, "maximum": 1, "example": 0.857, "description": "Fraction of keyword tokens matched in response" },
          "matchedWords":    { "type": "array", "items": { "type": "string" }, "description": "Keywords from `fact` found in the response" },
          "missedWords":     { "type": "array", "items": { "type": "string" }, "description": "Keywords from `fact` absent from the response" },
          "notes":           { "type": "string", "description": "Human-readable scoring explanation" },
          "latencyS":        { "type": "number", "example": 1.43, "description": "Model call latency in seconds" },
          "estimatedTokens": { "type": "integer", "example": 51200, "description": "Approximate haystack token count" },
          "error":           { "type": "string", "description": "Error message if tier is 'error'" }
        }
      },
      "EvalPositionSummary": {
        "type": "object",
        "properties": {
          "trials":        { "type": "integer", "example": 3 },
          "exact":         { "type": "integer", "example": 2 },
          "partial":       { "type": "integer", "example": 1 },
          "miss":          { "type": "integer", "example": 0 },
          "exactRate":     { "type": "number", "minimum": 0, "maximum": 1, "example": 0.667 },
          "avgMatchRatio": { "type": "number", "minimum": 0, "maximum": 1, "example": 0.810 },
          "verdict":       { "type": "string", "enum": ["PASS — strong retrieval at this position", "DEGRADED — partial retrieval, suspect tail-end attention loss", "FAIL — poor retrieval, likely attention sink or context overflow"] }
        }
      },
      "EvalPositionResult": {
        "type": "object",
        "description": "Map of position percentage string (e.g. \"90.0\") → trials + summary",
        "additionalProperties": {
          "type": "object",
          "properties": {
            "trials":  { "type": "array", "items": { "$ref": "#/components/schemas/EvalTrialResult" } },
            "summary": { "$ref": "#/components/schemas/EvalPositionSummary" }
          }
        }
      },
      "EvalSampleDetail": {
        "type": "object",
        "properties": {
          "fact":     { "type": "string" },
          "question": { "type": "string" },
          "results":  {
            "type": "object",
            "description": "Map of modelId → EvalPositionResult",
            "additionalProperties": { "$ref": "#/components/schemas/EvalPositionResult" }
          }
        }
      },
      "EvalRunRequest": {
        "type": "object",
        "required": ["apiKey", "keyType"],
        "properties": {
          "apiKey":         { "type": "string", "description": "Provider API key (min 10 chars). Never logged or stored." },
          "keyType":        { "type": "string", "enum": ["anthropic","openai","google","mistral","xai","groq","deepseek","cohere","openrouter"], "description": "Which provider the key belongs to" },
          "models":         { "type": "array", "items": { "type": "string" }, "minItems": 1, "maxItems": 4, "description": "Model IDs to compare (1–4). Use IDs from `/evals/context-retrieval/models`.", "example": ["gpt-4o", "claude-sonnet-4-6"] },
          "modelId":        { "type": "string", "description": "Legacy single-model alias — equivalent to `models: [modelId]`. Ignored if `models` array is supplied." },
          "positions":      { "type": "array", "items": { "type": "number" }, "minItems": 1, "maxItems": 10, "default": [85, 90, 95], "description": "Needle positions as percentage of document (0–100). e.g. `[50, 75, 90, 95]`" },
          "trials":         { "type": "integer", "minimum": 1, "maximum": 10, "default": 3, "description": "Repetitions per position (for statistical reliability)" },
          "tokens":         { "type": "integer", "minimum": 1000, "maximum": 500000, "default": 50000, "description": "Target haystack size in tokens (synthetic mode only). Common presets: 8 000, 50 000, 64 000, 128 000." },
          "fact":           { "type": "string", "default": "Project Helios launch date is March 7, 2031", "description": "The needle sentence injected into the document. Scored by keyword overlap." },
          "question":       { "type": "string", "default": "What is the launch date of Project Helios?", "description": "The question posed to the model after presenting the document." },
          "customDocument": { "type": "string", "description": "Optional — your own document text to use as the haystack instead of synthetic filler. The needle is injected at `positions` percentage through the sentence list. When supplied, `tokens` is ignored." }
        }
      },
      "EvalRunStarted": {
        "type": "object",
        "properties": {
          "evalId": { "type": "string", "example": "eval_1747042800000_x4k2z", "description": "Unique ID — use as `scanId` for the WebSocket connection" },
          "total":  { "type": "integer", "example": 9, "description": "Total trials across all models and positions (models × positions × trials)" }
        }
      },
      "SeoPageCheckerRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": { "type": "string", "format": "uri", "description": "The page URL to audit.", "example": "https://example.com/blog/post" }
        }
      },
      "SeoPageCheckerScanRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": { "type": "string", "format": "uri", "description": "Site root or sitemap URL. Auto-discovers /sitemap.xml if a root URL is given.", "example": "https://example.com" },
          "depth": { "type": "integer", "minimum": 1, "maximum": 10, "default": 3, "description": "BFS crawl depth for URL discovery." },
          "maxPages": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50, "description": "Maximum pages to audit." }
        }
      },
      "SeoPageCheck": {
        "type": "object",
        "description": "Result of one individual on-page SEO check.",
        "properties": {
          "id": { "type": "string", "example": "TITLE", "description": "Machine-readable check ID" },
          "label": { "type": "string", "example": "Page Title", "description": "Human-readable check name" },
          "status": { "type": "string", "enum": ["pass", "warn", "fail", "na"], "description": "Outcome — na means not applicable for this page type" },
          "message": { "type": "string", "example": "Title is 52 characters — good length", "description": "Human-readable explanation" },
          "details": { "type": "string", "nullable": true, "description": "Extra detail or extracted value (e.g. the actual title text)" }
        }
      },
      "SeoPageCheckerResult": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "url": { "type": "string", "format": "uri" },
          "score": { "type": "integer", "minimum": 0, "maximum": 100, "description": "Overall score: 100 − (fails×15) − (warns×5)" },
          "passCount": { "type": "integer" },
          "warnCount": { "type": "integer" },
          "failCount": { "type": "integer" },
          "httpStatus": { "type": "integer", "nullable": true },
          "responseMs": { "type": "integer", "nullable": true },
          "checks": { "type": "array", "items": { "$ref": "#/components/schemas/SeoPageCheck" } }
        }
      },
      "SeoPageCheckerScanSummary": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "url": { "type": "string", "format": "uri" },
          "mode": { "type": "string", "enum": ["single", "sitemap"], "description": "single = one-page check; sitemap = multi-page scan" },
          "status": { "type": "string", "enum": ["running", "complete", "error", "cancelled"] },
          "checkedUrls": { "type": "integer" },
          "totalUrls": { "type": "integer", "nullable": true },
          "avgScore": { "type": "number", "nullable": true },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "SeoPageCheckerScanDetail": {
        "type": "object",
        "properties": {
          "scan": { "$ref": "#/components/schemas/SeoPageCheckerScanSummary" },
          "pages": { "type": "array", "items": { "$ref": "#/components/schemas/SeoPageCheckerResult" } }
        }
      },
      "Health": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["ok", "error"] },
          "version": { "type": "string", "example": "1.0.0" },
          "uptime": { "type": "integer", "description": "Server uptime in seconds" },
          "db": { "type": "string", "enum": ["ok", "error"] },
          "timestamp": { "type": "string", "format": "date-time" }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": { "type": "string", "description": "Machine-readable error code", "example": "NOT_FOUND" },
              "message": { "type": "string", "description": "Human-readable description", "example": "Scan not found" }
            }
          }
        }
      }
    }
  },
  "x-websocket": {
    "description": "WebSocket endpoint for real-time scan progress. Both the Link Checker and SEO Analyzer use the same WebSocket endpoint — frame types are prefixed to distinguish them (`scan:*` vs `seo:*`).",
    "url": "ws://localhost:3000/ws",
    "queryParams": {
      "scanId": "UUID of the scan to subscribe to"
    },
    "frames": {
      "scan:started": {
        "description": "Link Checker — fired once when the BFS crawler begins.",
        "schema": { "type": "object", "properties": { "type": { "type": "string", "example": "scan:started" }, "scanId": { "type": "string" } } }
      },
      "scan:progress": {
        "description": "Link Checker — fired once per URL checked. `link` contains full details of the checked URL.",
        "schema": { "type": "object", "properties": { "type": { "type": "string", "example": "scan:progress" }, "scanId": { "type": "string" }, "checked": { "type": "integer" }, "queued": { "type": "integer" }, "link": { "$ref": "#/components/schemas/Link" } } }
      },
      "scan:browser-result": {
        "description": "Link Checker — fired when a blocked link (403/429) has been re-verified via headless Chromium. Updates the previously emitted link record.",
        "schema": { "type": "object", "properties": { "type": { "type": "string", "example": "scan:browser-result" }, "scanId": { "type": "string" }, "link": { "$ref": "#/components/schemas/Link" } } }
      },
      "scan:complete": {
        "description": "Link Checker — fired when all URLs have been checked.",
        "schema": { "type": "object", "properties": { "type": { "type": "string", "example": "scan:complete" }, "scanId": { "type": "string" }, "stats": { "$ref": "#/components/schemas/LinkScanStats" } } }
      },
      "scan:cancelled": {
        "description": "Link Checker — fired when the scan is stopped via DELETE. Contains partial stats.",
        "schema": { "type": "object", "properties": { "type": { "type": "string", "example": "scan:cancelled" }, "scanId": { "type": "string" }, "stats": { "$ref": "#/components/schemas/LinkScanStats" } } }
      },
      "scan:error": {
        "description": "Link Checker — fired if the crawler encounters an unrecoverable error.",
        "schema": { "type": "object", "properties": { "type": { "type": "string", "example": "scan:error" }, "scanId": { "type": "string" }, "message": { "type": "string" } } }
      },
      "seo:started": {
        "description": "SEO Analyzer — fired once when the SEO BFS crawler begins.",
        "schema": { "type": "object", "properties": { "type": { "type": "string", "example": "seo:started" }, "scanId": { "type": "string" } } }
      },
      "seo:progress": {
        "description": "SEO Analyzer — fired once per page crawled. `page` contains the full SEO audit result for that page including score, issue list, and all extracted signals.",
        "schema": {
          "type": "object",
          "properties": {
            "type": { "type": "string", "example": "seo:progress" },
            "scanId": { "type": "string" },
            "checked": { "type": "integer", "description": "Pages crawled so far" },
            "queued": { "type": "integer", "description": "Pages remaining in BFS queue" },
            "page": { "$ref": "#/components/schemas/SeoPage" }
          }
        }
      },
      "seo:complete": {
        "description": "SEO Analyzer — fired when all pages have been analyzed.",
        "schema": { "type": "object", "properties": { "type": { "type": "string", "example": "seo:complete" }, "scanId": { "type": "string" }, "stats": { "$ref": "#/components/schemas/SeoScanStats" } } }
      },
      "seo:cancelled": {
        "description": "SEO Analyzer — fired when the scan is stopped via DELETE. Contains partial stats.",
        "schema": { "type": "object", "properties": { "type": { "type": "string", "example": "seo:cancelled" }, "scanId": { "type": "string" }, "stats": { "$ref": "#/components/schemas/SeoScanStats" } } }
      },
      "seo:error": {
        "description": "SEO Analyzer — fired if the crawler encounters an unrecoverable error.",
        "schema": { "type": "object", "properties": { "type": { "type": "string", "example": "seo:error" }, "scanId": { "type": "string" }, "message": { "type": "string" } } }
      },
      "sitemap:started": {
        "description": "Sitemap Validator — fired once when URL checking begins.",
        "schema": { "type": "object", "properties": { "type": { "type": "string", "example": "sitemap:started" }, "validationId": { "type": "string" }, "sitemapUrl": { "type": "string" } } }
      },
      "sitemap:progress": {
        "description": "Sitemap Validator — fired once per URL checked. Contains the full result for that URL.",
        "schema": { "type": "object", "properties": { "type": { "type": "string", "example": "sitemap:progress" }, "validationId": { "type": "string" }, "checked": { "type": "integer" }, "total": { "type": "integer" }, "url": { "type": "string" }, "httpStatus": { "type": "integer", "nullable": true }, "responseMs": { "type": "integer" }, "statusLabel": { "type": "string" }, "isNoindex": { "type": "boolean" }, "canonicalMismatch": { "type": "boolean" } } }
      },
      "sitemap:complete": {
        "description": "Sitemap Validator — fired when all URLs have been checked.",
        "schema": { "type": "object", "properties": { "type": { "type": "string", "example": "sitemap:complete" }, "validationId": { "type": "string" }, "stats": { "type": "object", "properties": { "total": { "type": "integer" }, "ok": { "type": "integer" }, "broken": { "type": "integer" }, "redirect": { "type": "integer" }, "noindex": { "type": "integer" }, "error": { "type": "integer" } } } } }
      },
      "sitemap:cancelled": {
        "description": "Sitemap Validator — fired when the validation is stopped via DELETE.",
        "schema": { "type": "object", "properties": { "type": { "type": "string", "example": "sitemap:cancelled" }, "validationId": { "type": "string" } } }
      },
      "sitemap:error": {
        "description": "Sitemap Validator — fired on unrecoverable error (e.g. sitemap fetch failed).",
        "schema": { "type": "object", "properties": { "type": { "type": "string", "example": "sitemap:error" }, "validationId": { "type": "string" }, "message": { "type": "string" } } }
      },
      "schema:started": {
        "description": "Schema Markup Tester — fired once when a sitemap-based schema scan begins.",
        "schema": { "type": "object", "properties": { "type": { "type": "string", "example": "schema:started" }, "scanId": { "type": "string" }, "sitemapUrl": { "type": "string" } } }
      },
      "schema:progress": {
        "description": "Schema Markup Tester — fired once per page tested. `result` contains the full schema test result for that page.",
        "schema": {
          "type": "object",
          "properties": {
            "type":    { "type": "string", "example": "schema:progress" },
            "scanId":  { "type": "string" },
            "checked": { "type": "integer", "description": "Pages tested so far" },
            "total":   { "type": "integer", "description": "Total pages to test" },
            "result":  { "$ref": "#/components/schemas/SchemaTestResult" }
          }
        }
      },
      "schema:complete": {
        "description": "Schema Markup Tester — fired when all pages in the sitemap have been tested.",
        "schema": {
          "type": "object",
          "properties": {
            "type":    { "type": "string", "example": "schema:complete" },
            "scanId":  { "type": "string" },
            "summary": {
              "type": "object",
              "properties": {
                "total":         { "type": "integer", "description": "Total pages in sitemap" },
                "checked":       { "type": "integer", "description": "Pages successfully tested" },
                "withSchema":    { "type": "integer", "description": "Pages that had at least one schema block" },
                "withoutSchema": { "type": "integer", "description": "Pages with no structured data found" },
                "totalIssues":   { "type": "integer", "description": "Sum of all errors and warnings across all pages" }
              }
            }
          }
        }
      },
      "schema:cancelled": {
        "description": "Schema Markup Tester — fired when the scan is stopped via DELETE. Contains partial progress.",
        "schema": {
          "type": "object",
          "properties": {
            "type":    { "type": "string", "example": "schema:cancelled" },
            "scanId":  { "type": "string" },
            "checked": { "type": "integer", "description": "Pages tested before cancellation" },
            "total":   { "type": "integer", "description": "Total pages that were queued" }
          }
        }
      },
      "schema:error": {
        "description": "Schema Markup Tester — fired if the scan encounters an unrecoverable error (e.g. sitemap fetch failed).",
        "schema": {
          "type": "object",
          "properties": {
            "type":    { "type": "string", "example": "schema:error" },
            "scanId":  { "type": "string" },
            "message": { "type": "string" }
          }
        }
      },
      "check:started": {
        "direction": "server→client",
        "description": "SEO Page Checker (sitemap scan) — fired once when auditing begins.",
        "schema": {
          "type": "object",
          "properties": {
            "type": { "type": "string", "enum": ["check:started"] },
            "scanId": { "type": "string", "format": "uuid" },
            "total": { "type": "integer", "description": "Total pages to audit" }
          }
        }
      },
      "check:progress": {
        "direction": "server→client",
        "description": "SEO Page Checker — fired after each page is audited. Contains the full check result for that page.",
        "schema": {
          "type": "object",
          "properties": {
            "type": { "type": "string", "enum": ["check:progress"] },
            "scanId": { "type": "string", "format": "uuid" },
            "checked": { "type": "integer", "description": "Pages audited so far" },
            "total": { "type": "integer" },
            "result": { "$ref": "#/components/schemas/SeoPageCheckerResult" }
          }
        }
      },
      "check:complete": {
        "direction": "server→client",
        "description": "SEO Page Checker — fired when all pages have been audited.",
        "schema": {
          "type": "object",
          "properties": {
            "type": { "type": "string", "enum": ["check:complete"] },
            "scanId": { "type": "string", "format": "uuid" },
            "checkedUrls": { "type": "integer" },
            "avgScore": { "type": "number" }
          }
        }
      },
      "check:cancelled": {
        "direction": "server→client",
        "description": "SEO Page Checker — fired when the scan is stopped via DELETE.",
        "schema": {
          "type": "object",
          "properties": {
            "type": { "type": "string", "enum": ["check:cancelled"] },
            "scanId": { "type": "string", "format": "uuid" },
            "checked": { "type": "integer" }
          }
        }
      },
      "check:error": {
        "direction": "server→client",
        "description": "SEO Page Checker — fired if the scan encounters an unrecoverable error.",
        "schema": {
          "type": "object",
          "properties": {
            "type": { "type": "string", "enum": ["check:error"] },
            "scanId": { "type": "string", "format": "uuid" },
            "message": { "type": "string" }
          }
        }
      },
      "seo:lighthouse-discovering": {
        "direction": "server→client",
        "description": "Broadcast once when BFS URL discovery begins, before any Lighthouse audit starts.",
        "schema": {
          "type": "object",
          "properties": {
            "type": { "type": "string", "enum": ["seo:lighthouse-discovering"] },
            "scanId": { "type": "string", "format": "uuid" }
          }
        }
      },
      "seo:lighthouse-started": {
        "direction": "server→client",
        "description": "Broadcast when URL discovery is complete and Lighthouse audits are about to begin. Includes total URL count.",
        "schema": {
          "type": "object",
          "properties": {
            "type": { "type": "string", "enum": ["seo:lighthouse-started"] },
            "scanId": { "type": "string", "format": "uuid" },
            "total": { "type": "integer", "description": "Number of URLs to audit" }
          }
        }
      },
      "seo:lighthouse-progress": {
        "direction": "server→client",
        "description": "Broadcast after each URL's Lighthouse audit completes. If Lighthouse failed for a URL, all score fields are null.",
        "schema": {
          "type": "object",
          "properties": {
            "type": { "type": "string", "enum": ["seo:lighthouse-progress"] },
            "scanId": { "type": "string", "format": "uuid" },
            "url": { "type": "string", "format": "uri" },
            "perf": { "type": "integer", "nullable": true, "description": "Performance score 0–100" },
            "seo": { "type": "integer", "nullable": true, "description": "SEO score 0–100" },
            "a11y": { "type": "integer", "nullable": true, "description": "Accessibility score 0–100" },
            "bp": { "type": "integer", "nullable": true, "description": "Best Practices score 0–100" },
            "lcp": { "type": "number", "nullable": true, "description": "Largest Contentful Paint in ms" },
            "cls": { "type": "number", "nullable": true, "description": "Cumulative Layout Shift score" },
            "details": { "type": "object", "nullable": true, "description": "Top failing audits per category keyed by category ID (performance, seo, accessibility, best-practices)" },
            "done": { "type": "integer", "description": "Number of URLs audited so far" },
            "total": { "type": "integer", "description": "Total URLs in this scan" }
          }
        }
      },
      "seo:lighthouse-complete": {
        "direction": "server→client",
        "description": "Broadcast when all Lighthouse audits have finished successfully.",
        "schema": {
          "type": "object",
          "properties": {
            "type": { "type": "string", "enum": ["seo:lighthouse-complete"] },
            "scanId": { "type": "string", "format": "uuid" },
            "total": { "type": "integer", "description": "Total URLs audited" }
          }
        }
      },
      "seo:lighthouse-cancelled": {
        "direction": "server→client",
        "description": "Broadcast when a Lighthouse scan is stopped early (via DELETE endpoint or internal error).",
        "schema": {
          "type": "object",
          "properties": {
            "type": { "type": "string", "enum": ["seo:lighthouse-cancelled"] },
            "scanId": { "type": "string", "format": "uuid" },
            "total": { "type": "integer", "description": "Number of URLs audited before cancellation" }
          }
        }
      },
      "eval:started": {
        "direction": "server→client",
        "description": "Broadcast ~600 ms after `POST /evals/context-retrieval/run` once the server is ready to begin. Confirms the full run parameters. Sent once per eval, before the first `eval:progress` frame.",
        "schema": {
          "type": "object",
          "properties": {
            "type":      { "type": "string", "enum": ["eval:started"] },
            "evalId":    { "type": "string", "description": "Matches the `evalId` returned by POST /run" },
            "models":    { "type": "array", "items": { "type": "string" }, "description": "Model IDs being evaluated" },
            "positions": { "type": "array", "items": { "type": "number" }, "description": "Needle positions (percent)" },
            "trials":    { "type": "integer", "description": "Repetitions per position" },
            "tokens":    { "type": "integer", "description": "Target haystack size in tokens" },
            "fact":      { "type": "string" },
            "question":  { "type": "string" },
            "total":     { "type": "integer", "description": "Total trial count: models × positions × trials" }
          }
        }
      },
      "eval:progress": {
        "direction": "server→client",
        "description": "Broadcast after every individual trial completes. Includes the scored result with keyword transparency (`matchedWords`, `missedWords`) so the UI can render live heatmap cells and trial rows.",
        "schema": {
          "type": "object",
          "properties": {
            "type":        { "type": "string", "enum": ["eval:progress"] },
            "evalId":      { "type": "string" },
            "modelId":     { "type": "string", "description": "Which model this trial belongs to" },
            "positionPct": { "type": "number", "description": "Needle position percentage for this trial" },
            "trial":       { "type": "integer", "description": "Trial number (1-indexed)" },
            "result":      { "$ref": "#/components/schemas/EvalTrialResult" },
            "done":        { "type": "integer", "description": "Cumulative trials completed so far" },
            "total":       { "type": "integer", "description": "Total trials in this run" }
          }
        }
      },
      "eval:position-done": {
        "direction": "server→client",
        "description": "Broadcast after all trials for a given (modelId, positionPct) pair have completed. Contains the aggregated summary including exactRate and verdict.",
        "schema": {
          "type": "object",
          "properties": {
            "type":        { "type": "string", "enum": ["eval:position-done"] },
            "evalId":      { "type": "string" },
            "modelId":     { "type": "string" },
            "positionPct": { "type": "number" },
            "summary":     { "$ref": "#/components/schemas/EvalPositionSummary" }
          }
        }
      },
      "eval:complete": {
        "direction": "server→client",
        "description": "Broadcast when all models and positions have finished. Contains the full results map — identical in structure to `GET /evals/context-retrieval/sample/{runId}` for pre-computed runs.",
        "schema": {
          "type": "object",
          "properties": {
            "type":     { "type": "string", "enum": ["eval:complete"] },
            "evalId":   { "type": "string" },
            "models":   { "type": "array", "items": { "type": "string" } },
            "fact":     { "type": "string" },
            "question": { "type": "string" },
            "results":  {
              "type": "object",
              "description": "Map of modelId → EvalPositionResult",
              "additionalProperties": { "$ref": "#/components/schemas/EvalPositionResult" }
            }
          }
        }
      },
      "eval:error": {
        "direction": "server→client",
        "description": "Broadcast if the eval run fails (e.g. invalid API key, network error, model timeout). The run is terminated — no `eval:complete` will follow.",
        "schema": {
          "type": "object",
          "properties": {
            "type":    { "type": "string", "enum": ["eval:error"] },
            "evalId":  { "type": "string" },
            "modelId": { "type": "string", "description": "Present if the error is model-specific; absent for run-level errors" },
            "error":   { "type": "string", "description": "Human-readable error message" }
          }
        }
      }
    }
  },
  "x-mcp": {
    "endpoint": "POST https://www.bytewavenetwork.com/api/mcp",
    "discovery": "GET https://www.bytewavenetwork.com/api/mcp",
    "protocol": "MCP 2024-11-05 (JSON-RPC 2.0 over HTTP)",
    "note": "The MCP endpoint lives at /api/mcp — outside the /api/v1 base path used by REST endpoints. Full path definitions are below.",
    "paths": {
      "GET /api/mcp": {
        "summary": "MCP server discovery",
        "description": "Returns server name, version, protocol version, endpoint URL, and a summary list of all available tools. Useful for agents that support MCP server auto-discovery.",
        "response": {
          "name": "ByteWaveNetwork",
          "version": "1.0.0",
          "protocolVersion": "2024-11-05",
          "description": "string",
          "endpoint": "string (full URL)",
          "tools": [{ "name": "string", "description": "string" }]
        }
      },
      "POST /api/mcp": {
        "summary": "MCP JSON-RPC 2.0 handler",
        "description": "Accepts JSON-RPC 2.0 requests. All requests must include jsonrpc: '2.0', a method string, and an optional id.",
        "methods": {
          "initialize": {
            "params": { "protocolVersion": "string", "clientInfo": { "name": "string", "version": "string" }, "capabilities": {} },
            "result": { "protocolVersion": "2024-11-05", "capabilities": { "tools": {} }, "serverInfo": { "name": "ByteWaveNetwork", "version": "1.0.0" } }
          },
          "notifications/initialized": { "note": "No-op — returns HTTP 204" },
          "ping": { "result": {} },
          "tools/list": { "result": { "tools": "ToolDefinition[]" } },
          "tools/call": {
            "params": { "name": "string (tool name)", "arguments": "object" },
            "result": { "content": [{ "type": "text", "text": "string" }], "isError": "boolean" },
            "tools": ["link_checker", "seo_analyzer", "redirect_tracer", "page_speed_inspector", "sitemap_validator", "schema_tester", "page_seo_score", "get_scan_status", "context_retrieval", "instruction_following", "agentic_loop", "thinking_mode", "prompt_sensitivity", "get_eval_result"]
          }
        }
      }
    }
  },
  "x-phase-a-changelog": {
    "version": "1.3.0",
    "added": {
      "linkChecker": {
        "scanResponse":   ["prevScanId"],
        "scanLink":       ["issue_type (soft-404|hard-404|ok)", "delta_state (new|fixed|regressed|unchanged)", "content_signature"],
        "scanCompleteWs": ["delta {new,fixed,regressed,unchanged}", "stats.softFourOhFour", "stats.hardFourOhFour"],
        "linksFilters":   ["soft-404", "hard-404", "new", "fixed", "regressed"]
      },
      "seoAnalyzer": {
        "page":           ["heading_outline", "heading_skips", "lighthouse_perf", "lighthouse_seo", "lighthouse_a11y", "lighthouse_bp", "lcp_ms", "cls_score"],
        "pageFilters":    ["heading-skip", "low-perf"]
      },
      "redirectTracer": {
        "trace":          ["canonicalUrl", "canonicalOverride", "intentWarnings[]", "recommendedStatusCode"]
      },
      "schemaTester": {
        "summary":        ["eligibilityScore (0-100)", "eligibleTypes[{type,score,requiredMissing[],recommendedMissing[]}]"],
        "schema":         ["eligibilityScore", "requiredMissing[]", "recommendedMissing[]"]
      },
      "sitemapValidator": {
        "stats":          ["duplicates", "gzipUsed"],
        "url":            ["is_duplicate"],
        "filters":        ["duplicate"],
        "completeWs":     ["warnings[]"]
      }
    }
  }
}
