{
  "openapi": "3.1.0",
  "info": {
    "title": "Synapse",
    "description": "Self-organising wiki backend \u2014 M5 Phase 1 (F5 4-phase retrieval + [n] citations). 4-signal knowledge graph (F4): direct\u00d73 + source-overlap\u00d74 + Adamic-Adar\u00d71.5 + type\u00d71. FA2 server-side via igraph (I2); coords persisted in Postgres; dataVersion-debounced GraphCache; GET /graph precomputed coords (ADR-0014). Pluggable inference provider (F17): Local/Ollama, API/Anthropic-compatible, CLI/claude-agent-sdk. Bounded orchestrated ingest loop (I7). POST /ingest/upload: multipart upload \u2192 ingest (ADR-0020 Feature U). POST /ingest/from-text: inline text \u2192 ingest (ADR-0019 \u00a72.7, AC-F6-5 save-to-wiki). GET /search: F5 4-phase RAG retrieval (ADR-0022, AC-F5-6). GET|PUT /import-schedule + POST /import-schedule/run-now: scheduled folder import (ADR-0020 Feature S). Karpathy LLM Wiki pattern [K1\u2013K8]. POST /clip: Chrome MV3 web clipper ingress \u2014 token-gated, origin-checked, body-capped, safe-joined (F11, ADR-0038).",
    "version": "2.1.22"
  },
  "paths": {
    "/sources": {
      "get": {
        "tags": [
          "sources"
        ],
        "summary": "List raw-sources or wiki tree",
        "description": "Recursively lists all files and directories under the chosen root. root='sources' (default): vault/raw/sources/ \u2014 the original behaviour. root='wiki': vault/wiki/ \u2014 read-only wiki file tree; hidden entries (dotfiles, .obsidian) are excluded automatically (I5). Returns SourceEntry objects with path (relative to the chosen root), name, is_dir, size_bytes, ext (lowercased), mtime (ISO-8601). Read-only (I1). Capped at SOURCES_LIST_MAX=5000 entries (I7); truncated=true if hit. Returns empty list when the root directory does not exist.",
        "operationId": "list_sources_sources_get",
        "parameters": [
          {
            "name": "root",
            "in": "query",
            "required": false,
            "schema": {
              "enum": [
                "sources",
                "wiki",
                "vault"
              ],
              "type": "string",
              "description": "Which directory tree to list. 'sources' (default) = vault/raw/sources/; 'wiki' = vault/wiki/ (read-only; hidden dirs excluded); 'vault' = the whole vault root (raw/ + wiki/ + purpose.md + schema.md; read-only; hidden dirs excluded \u2014 v1.5 P1 Files-tab parity).",
              "default": "sources",
              "title": "Root"
            },
            "description": "Which directory tree to list. 'sources' (default) = vault/raw/sources/; 'wiki' = vault/wiki/ (read-only; hidden dirs excluded); 'vault' = the whole vault root (raw/ + wiki/ + purpose.md + schema.md; read-only; hidden dirs excluded \u2014 v1.5 P1 Files-tab parity)."
          }
        ],
        "responses": {
          "200": {
            "description": "Directory listing (may be empty or truncated)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SourcesListResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "delete": {
        "tags": [
          "sources"
        ],
        "summary": "Delete a raw source file or directory and cascade-delete derived pages",
        "description": "Deletes the raw source file or directory (S2, B3b) from disk and cascade-deletes all derived wiki pages for each contained file (soft-delete pages, remove Qdrant points, clean dead [[wikilinks]], update index.md). Reuses the existing cascade-delete machinery (ops/cascade_delete.py \u2014 F13/ADR-0026). Bumps data_version + notifies GraphCache (I2). path must be relative to vault/raw/sources/ \u2014 resolved via resolve_under_sources(). 404 when path escapes or target does not exist. \n\nFILE delete (is_dir=False): existing behaviour unchanged \u2014 cascade then remove file. If the source has no derived pages, the raw file is still deleted. Returns {deleted_source, files_deleted:1, pages_cascaded:N}. \n\nDIRECTORY delete (is_dir=True, S2): enumerates all files in the subtree, runs the SAME per-file cascade for each, then removes files + empty dirs from disk. Bounded at SOURCES_DELETE_MAX_FILES (env, default 500); if the subtree contains more files, returns 409 with a clear message (I7 \u2014 no unbounded cascade). Returns {deleted_source, files_deleted:K, pages_cascaded:M}. \n\nPath-safety: resolve_under_sources() for all paths (ADR-0020 \u00a72.2).",
        "operationId": "delete_source_sources_delete",
        "parameters": [
          {
            "name": "path",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Relative path under vault/raw/sources/",
              "title": "Path"
            },
            "description": "Relative path under vault/raw/sources/"
          }
        ],
        "responses": {
          "200": {
            "description": "Source deleted; derived pages cascade-deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SourceDeleteResponse"
                }
              }
            }
          },
          "404": {
            "description": "Source file or directory not found or path outside raw/sources/"
          },
          "409": {
            "description": "Directory exceeds SOURCES_DELETE_MAX_FILES limit (I7)"
          },
          "422": {
            "description": "Path traversal attempt"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/sources/content": {
      "get": {
        "tags": [
          "sources"
        ],
        "summary": "Metadata + preview payload for one source or wiki file",
        "description": "Returns metadata and preview payload for a single file. root='sources' (default): path relative to vault/raw/sources/. root='wiki': path relative to vault/wiki/ (read-only; no derived-page pivot). path resolved via containment check (ADR-0020 \u00a72.2 traversal guard). 404 if path escapes, is hidden (root=wiki), or file is absent. category: one of text/markdown/image/pdf/document/data/code/av/other. For text/markdown/code/data: includes raw text (capped at SOURCES_TEXT_MAX_CHARS). For pdf/docx/pptx/xlsx: includes extracted text (via ingest/extract.py, F12). For image/av/other: no text, is_text=false (use GET /sources/raw for bytes). ingested + page_ids: derived-page linkage via sources[] JSONB pivot (K6/F13); always ingested=false + page_ids=[] when root='wiki'.",
        "operationId": "source_content_sources_content_get",
        "parameters": [
          {
            "name": "path",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Relative path under the chosen root directory",
              "title": "Path"
            },
            "description": "Relative path under the chosen root directory"
          },
          {
            "name": "root",
            "in": "query",
            "required": false,
            "schema": {
              "enum": [
                "sources",
                "wiki",
                "vault"
              ],
              "type": "string",
              "description": "Which directory to resolve path against. 'sources' (default) = vault/raw/sources/; 'wiki' = vault/wiki/ (read-only; hidden files rejected); 'vault' = whole vault root (read-only; hidden files rejected \u2014 v1.5 P1).",
              "default": "sources",
              "title": "Root"
            },
            "description": "Which directory to resolve path against. 'sources' (default) = vault/raw/sources/; 'wiki' = vault/wiki/ (read-only; hidden files rejected); 'vault' = whole vault root (read-only; hidden files rejected \u2014 v1.5 P1)."
          }
        ],
        "responses": {
          "200": {
            "description": "File metadata + preview payload",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SourceContentResponse"
                }
              }
            }
          },
          "404": {
            "description": "File not found or path outside chosen root"
          },
          "422": {
            "description": "Path traversal attempt"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/sources/raw": {
      "get": {
        "tags": [
          "sources"
        ],
        "summary": "Stream raw bytes of a source or wiki file for inline preview",
        "description": "Streams the raw bytes of a file for inline browser preview (images, PDFs, text). root='sources' (default): path relative to vault/raw/sources/. root='wiki': path relative to vault/wiki/ (read-only; hidden files rejected). path resolved via containment check (ADR-0020 \u00a72.2 traversal guard). 404 if absent/outside chosen root. Returns 413 if file exceeds SOURCES_RAW_MAX_BYTES=52428800 bytes (I7). Content-Type derived from extension. Content-Disposition: inline.",
        "operationId": "source_raw_sources_raw_get",
        "parameters": [
          {
            "name": "path",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Relative path under the chosen root directory",
              "title": "Path"
            },
            "description": "Relative path under the chosen root directory"
          },
          {
            "name": "root",
            "in": "query",
            "required": false,
            "schema": {
              "enum": [
                "sources",
                "wiki",
                "vault"
              ],
              "type": "string",
              "description": "Which directory to resolve path against. 'sources' (default) = vault/raw/sources/; 'wiki' = vault/wiki/ (read-only; hidden files rejected); 'vault' = whole vault root (read-only; hidden files rejected \u2014 v1.5 P1).",
              "default": "sources",
              "title": "Root"
            },
            "description": "Which directory to resolve path against. 'sources' (default) = vault/raw/sources/; 'wiki' = vault/wiki/ (read-only; hidden files rejected); 'vault' = whole vault root (read-only; hidden files rejected \u2014 v1.5 P1)."
          }
        ],
        "responses": {
          "200": {
            "description": "Raw file bytes with correct Content-Type",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          },
          "404": {
            "description": "File not found or path outside chosen root"
          },
          "413": {
            "description": "File exceeds SOURCES_RAW_MAX_BYTES limit"
          },
          "422": {
            "description": "Path traversal attempt"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/sources/derived-pages": {
      "get": {
        "tags": [
          "sources"
        ],
        "summary": "List wiki pages derived from a raw source",
        "description": "Returns the wiki pages whose sources[] JSONB contains the given source path. path must be relative to vault/raw/sources/ \u2014 resolved via resolve_under_sources(). Returns an empty list when no pages are derived from this source. Non-deleted pages only. 404 when path escapes or file is absent.",
        "operationId": "source_derived_pages_sources_derived_pages_get",
        "parameters": [
          {
            "name": "path",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Relative path under vault/raw/sources/",
              "title": "Path"
            },
            "description": "Relative path under vault/raw/sources/"
          }
        ],
        "responses": {
          "200": {
            "description": "List of derived wiki pages (may be empty)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SourceDerivedPage"
                  },
                  "title": "Response Source Derived Pages Sources Derived Pages Get"
                }
              }
            }
          },
          "404": {
            "description": "Source file not found or path outside raw/sources/"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/sources/ingest-all": {
      "post": {
        "tags": [
          "sources"
        ],
        "summary": "Index all pre-existing files in vault/raw/sources/",
        "description": "Enumerates vault/raw/sources/ recursively and starts a background driver that calls ingest_file() on each supported file with a BOUNDED worker pool (SOURCES_INGEST_ALL_CONCURRENCY, default 3 \u2014 same cap as Deep Research; I7, never unbounded). Each file's mtime-then-hash gate skips already-indexed files cheaply (idempotent; I1). Supported extensions: same allow-list as the watcher (_UPLOAD_ACCEPTED from upload.py). Capped at SOURCES_INGEST_ALL_MAX (default=200) files (I7); truncation is logged. Returns 202 immediately \u2014 do NOT block the caller. Returns 409 if a scan is already running (single-flight guard). I6: this endpoint makes zero InferenceProvider calls; ingest_file routes them. ADR-0006: this is the explicit user-initiated path for pre-existing files, NOT a startup rescan (I1 is not violated). Poll GET /sources/ingest-all/status or GET /ingest/queue to track progress.",
        "operationId": "ingest_all_sources_ingest_all_post",
        "responses": {
          "202": {
            "description": "Driver started (or nothing to do if candidate_files=0)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestAllResponse"
                }
              }
            }
          },
          "409": {
            "description": "An ingest-all scan is already running"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/sources/ingest-all/status": {
      "get": {
        "tags": [
          "sources"
        ],
        "summary": "Status of a running (or most-recent) ingest-all scan",
        "description": "Returns running=true/false and the done/total counts for the current or most-recent POST /sources/ingest-all driver. When running=false and done=total=0 no scan has been started this session. Cheap read-only poll (no DB I/O).",
        "operationId": "ingest_all_status_sources_ingest_all_status_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestAllStatusResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/export": {
      "get": {
        "tags": [
          "export"
        ],
        "summary": "Download vault as ZIP",
        "description": "Stream a ZIP archive of the vault directory (raw/ + wiki/ + purpose.md + schema.md + .obsidian/*.json). Returns 413 if the uncompressed total exceeds EXPORT_MAX_BYTES (500 MB). Returns 429 if another export is already running for this vault.",
        "operationId": "export_vault_zip_export_get",
        "responses": {
          "200": {
            "description": "ZIP archive stream",
            "content": {
              "application/zip": {}
            }
          },
          "413": {
            "description": "Vault exceeds EXPORT_MAX_BYTES size cap"
          },
          "429": {
            "description": "Another export is already running for this vault"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/export/data.json": {
      "get": {
        "tags": [
          "export"
        ],
        "summary": "Export vault metadata as JSON",
        "description": "Return a JSON dump of live pages (no content bodies), links, edges, ingest_runs summary, review_items summary, data_version, and generated_at. All records for the current vault_id.",
        "operationId": "export_data_json_export_data_json_get",
        "responses": {
          "200": {
            "description": "JSON metadata export",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/export/full": {
      "get": {
        "tags": [
          "export"
        ],
        "summary": "Extended JSON export \u2014 data.json PLUS conversations/provider_config/vault_state",
        "description": "Everything GET /export/data.json returns, PLUS: conversations (with their messages), provider_config rows, and the vault_state row. Secrets are NEVER decrypted: provider_config.api_key_encrypted is emitted as base64-encoded Fernet ciphertext \u2014 restoring it requires the SAME SYNAPSE_SECRET_KEY that encrypted it. Read-only; no file mutations, no provider calls, no graph recompute (I1/I2/I6).",
        "operationId": "export_full_json_export_full_get",
        "responses": {
          "200": {
            "description": "Extended JSON metadata export",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/costs/summary": {
      "get": {
        "tags": [
          "costs"
        ],
        "summary": "Cost & usage summary",
        "description": "Return aggregated AI inference costs across all cost-bearing tables (ingest_runs, messages, deep_research_runs, lint_runs) for the requested calendar month (default: current month). Includes by_operation, by_provider (ingest + chat only \u2014 see by_provider_note), by_day (last 30 days), monthly_total_usd, and threshold_alert. Query param ?month=YYYY-MM selects an alternative month.",
        "operationId": "get_costs_summary_costs_summary_get",
        "parameters": [
          {
            "name": "month",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "pattern": "^\\d{4}-\\d{2}$"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Calendar month to aggregate (YYYY-MM). Defaults to the current UTC month.",
              "title": "Month"
            },
            "description": "Calendar month to aggregate (YYYY-MM). Defaults to the current UTC month."
          }
        ],
        "responses": {
          "200": {
            "description": "Cost summary",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/health/live": {
      "get": {
        "tags": [
          "health"
        ],
        "summary": "Minimal service liveness",
        "description": "Public liveness probe used by connection and container checks. Returns no component, vault, configuration, error, or dependency details.",
        "operationId": "get_health_live_health_live_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "additionalProperties": {
                    "type": "string"
                  },
                  "type": "object",
                  "title": "Response Get Health Live Health Live Get"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/health/detailed": {
      "get": {
        "tags": [
          "health"
        ],
        "summary": "Detailed component health snapshot",
        "description": "Returns per-component health: watcher heartbeat, import scheduler state, ingest queue snapshot, graph cache warmth, DB latency (SELECT 1), Qdrant ping (skipped when EMBEDDINGS_ENABLED=false), embeddings toggle, and the 5 most recent in-process ERROR log entries. Top-level status: 'ok' | 'degraded' | 'error'. NEVER returns 5xx \u2014 failed probes degrade status, not the HTTP layer. (R9-2, AC-R9-2-1..AC-R9-2-4)",
        "operationId": "get_health_detailed_health_detailed_get",
        "responses": {
          "200": {
            "description": "Health snapshot (always 200)",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/stats/overview": {
      "get": {
        "tags": [
          "stats"
        ],
        "summary": "Global vault KPI snapshot",
        "description": "Returns global vault KPIs: pages_total, pages_by_type, links_total, communities_count, review_pending, lint_open, monthly_cost_usd, data_version, and recent_activity (last 10 pages by updated_at). Memoised on data_version; cache miss triggers a fresh bounded DB read. monthly_cost_usd reuses the costs.get_monthly_cost_usd shared helper (AC-R12-1-3, ADR-0054 \u00a75.1). Auth: SynapseAuthMiddleware by construction. (R12-1 / F18 / ADR-0054 \u00a75.1)",
        "operationId": "get_stats_overview_stats_overview_get",
        "responses": {
          "200": {
            "description": "Global KPI snapshot",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/stats/sections": {
      "get": {
        "tags": [
          "stats"
        ],
        "summary": "Per-domain section breakdown",
        "description": "Returns one entry per active vocabulary domain (in vocabulary order) plus an untagged bucket (always last). Each section has: domain, pages_total, pages_by_type, last_activity (ISO-8601 or null), and top_pages (top 5 by degree DESC). Dormant vocabulary ([]) returns only the untagged bucket (owner-lock #4). Memoised on data_version + vocabulary hash. Auth: SynapseAuthMiddleware by construction. (R12-1 / F18 / ADR-0054 \u00a75.2)",
        "operationId": "get_stats_sections_stats_sections_get",
        "responses": {
          "200": {
            "description": "Per-domain section breakdown",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/stats/groups": {
      "get": {
        "tags": [
          "stats"
        ],
        "summary": "Dynamic Louvain community groups",
        "description": "Returns one entry per Louvain community derived from pages.community (persisted by GraphEngine \u2014 I2, G-P0-2). Community ids -1/NULL (unassigned) are excluded. Ordered by pages_total DESC, capped at 12 groups. label = title of the highest-degree page in that community, truncated to 48 chars (heuristic, zero AI calls). top_pages = up to 5 pages ordered by degree DESC. Memoised on data_version; cache miss triggers a fresh bounded DB read. Auth: SynapseAuthMiddleware by construction (ADR-0052). (SPRINT-v1.2-SCOPE \u00a710 A1 / F18 / I2)",
        "operationId": "get_stats_groups_stats_groups_get",
        "responses": {
          "200": {
            "description": "Louvain community groups",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ops/backfill-domains": {
      "get": {
        "summary": "Get Backfill Domains Status",
        "description": "Single-flight state + last summary of the domain backfill (ADR-0054 \u00a76).",
        "operationId": "get_backfill_domains_status_ops_backfill_domains_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BackfillDomainsStatusResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "post": {
        "summary": "Start Backfill Domains",
        "description": "Start ONE bounded domain backfill over the existing vault (R12-2, ADR-0054 \u00a76).",
        "operationId": "start_backfill_domains_ops_backfill_domains_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BackfillDomainsRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BackfillDomainsStartResponse"
                }
              }
            }
          },
          "400": {
            "description": "Domain vocabulary is empty (feature dormant)"
          },
          "409": {
            "description": "A backfill run is already in flight"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ops/reclassify-types": {
      "get": {
        "summary": "Get Reclassify Types Status",
        "description": "Single-flight state + last summary of the type re-classification (SPRINT-v1.2 tail).",
        "operationId": "get_reclassify_types_status_ops_reclassify_types_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReclassifyTypesStatusResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "post": {
        "summary": "Start Reclassify Types",
        "description": "Start ONE bounded page-type re-classification over the vault (SPRINT-v1.2 tail, K8/I7).",
        "operationId": "start_reclassify_types_ops_reclassify_types_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReclassifyTypesRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReclassifyTypesStartResponse"
                }
              }
            }
          },
          "409": {
            "description": "A reclassify run is already in flight"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ops/synthesize": {
      "get": {
        "summary": "Get Synthesize Status",
        "description": "Single-flight state + last summary of the corpus synthesize pass (ADR-0067 D3).",
        "operationId": "get_synthesize_status_ops_synthesize_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SynthesizeStatusResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "post": {
        "summary": "Start Synthesize",
        "description": "Start ONE bounded corpus-level synthesis/comparison pass (ADR-0067 D3, P0-3, I6/I7).",
        "operationId": "start_synthesize_ops_synthesize_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/SynthesizeRequest"
                  },
                  {
                    "type": "null"
                  }
                ],
                "title": "Body"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SynthesizeStartResponse"
                }
              }
            }
          },
          "409": {
            "description": "A synthesize run is already in flight"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ops/synthesize/audit": {
      "get": {
        "summary": "Dry-run audit of possible legacy corpus-page duplicates",
        "description": "Read-only ADR-0074 audit; never tags, merges, deletes or rewrites legacy pages.",
        "operationId": "audit_synthesize_duplicates_ops_synthesize_audit_get",
        "parameters": [
          {
            "name": "max_pages",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 2000,
              "minimum": 1,
              "default": 500,
              "title": "Max Pages"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true,
                  "title": "Response Audit Synthesize Duplicates Ops Synthesize Audit Get"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ops/backfill-related": {
      "get": {
        "summary": "Get Backfill Related Status",
        "description": "Single-flight state + last summary of the ADR-0067 D2 backfill [P2-1,P2-2,I1].",
        "operationId": "get_backfill_related_status_ops_backfill_related_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BackfillRelatedStatusResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "post": {
        "summary": "Start Backfill Related",
        "description": "Start ONE bounded ADR-0067 D2 backfill pass (apply=False \u2192 dry-run) [P2-1,P2-2,I1,I7].\n\nBrings existing wiki pages up to D2 conventions:\n  P2-1 \u2014 sets/replaces ``related:`` from resolved outbound wikilinks (cap 8, slugs only).\n  P2-2 \u2014 rewrites ``[[Title]]`` / ``[[Title|alias]]`` to ``[[slug|Title]]`` /\n          ``[[slug|alias]]`` in page bodies (rendering unchanged).\n\nZero LLM cost.  Dry-run by default; pass ``apply=true`` to commit changes.",
        "operationId": "start_backfill_related_ops_backfill_related_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BackfillRelatedRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BackfillRelatedStartResponse"
                }
              }
            }
          },
          "409": {
            "description": "A backfill-related run is already in flight"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ops/reconcile-folders": {
      "get": {
        "summary": "Get Reconcile Folders Status",
        "description": "Single-flight state + last summary of the folder-reconcile sweep [K1,I1].",
        "operationId": "get_reconcile_folders_status_ops_reconcile_folders_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReconcileFoldersStatusResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "post": {
        "summary": "Start Reconcile Folders",
        "description": "Start ONE bounded folder-reconcile sweep (apply=False \u2192 dry-run; apply=True \u2192 moves) [K1,I1].\n\nFinds wiki pages whose physical folder (e.g. ``concepts/``) does not match the folder\nimplied by their ``type`` frontmatter (e.g. ``entity`` \u2192 ``entities/``) and \u2014 when\n``apply=True`` \u2014 moves them, updating Postgres + Qdrant. Zero LLM cost; purely\ndeterministic. The plan is visible via GET /ops/reconcile-folders after the run\ncompletes (dry-run returns plan without writing; apply returns counts + by_folder).",
        "operationId": "start_reconcile_folders_ops_reconcile_folders_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReconcileFoldersRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReconcileFoldersStartResponse"
                }
              }
            }
          },
          "409": {
            "description": "A reconcile-folders run is already in flight"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ops/schedules": {
      "get": {
        "summary": "List schedulable ops with schedule and last-run state (R12-7/A5/R12-8/R12-9)",
        "description": "Returns the in-memory state of the OpsScheduler for each schedulable op (lint, backfill, schema_review, reclassify). Schedule frequencies are set via PUT /config/app/lint_schedule, PUT /config/app/backfill_schedule, PUT /config/app/schema_review_schedule, and PUT /config/app/reclassify_schedule (S10/S11/S12/S13 \u2014 allowed values: off|hourly|daily|weekly). State is in-memory and resets on container restart. Auth: SynapseAuthMiddleware (ADR-0052).",
        "operationId": "get_ops_schedules_ops_schedules_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpsSchedulesResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ops/schedules/{op}/run-now": {
      "post": {
        "summary": "Trigger a schedulable op immediately (R12-7/A5/R12-8/R12-9)",
        "description": "Manually trigger one scheduled op ('lint', 'backfill', 'schema_review', or 'reclassify') regardless of the configured schedule. Returns 202 if triggered, 409 if already in-flight, 400 if the op is 'backfill' and the domain vocabulary is empty (dormant). schema_review and reclassify have no dormant state \u2014 they run whenever called (anti-spam / single-flight dedup is inside each op). Auth: SynapseAuthMiddleware (ADR-0052).",
        "operationId": "run_ops_schedule_now_ops_schedules__op__run_now_post",
        "parameters": [
          {
            "name": "op",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Op"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Op triggered successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  },
                  "title": "Response Run Ops Schedule Now Ops Schedules  Op  Run Now Post"
                }
              }
            }
          },
          "400": {
            "description": "backfill: vocabulary dormant (feature off)."
          },
          "404": {
            "description": "Unknown op name."
          },
          "409": {
            "description": "Op is already in-flight."
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ops/update-status": {
      "get": {
        "summary": "Deployment update availability (R12-3)",
        "description": "Compares the running backend version with the latest published GitHub Release. Read-only and cached ~1h. `update_supported` reflects whether the Watchtower HTTP API is wired up.",
        "operationId": "get_update_status_endpoint_ops_update_status_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpdateStatusResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ops/system-update": {
      "post": {
        "summary": "Trigger a Watchtower pull + recreate (R12-3, B1)",
        "description": "Pokes Watchtower's /v1/update (fire-and-forget) to pull the latest images and recreate every container labelled com.centurylinklabs.watchtower.enable=true on the host. The backend itself is recreated, so the connection drops \u2014 the UI shows an indeterminate 'update in progress' state and reconnects when the new backend is up. Auth-gated in server mode (ADR-0052).",
        "operationId": "trigger_system_update_endpoint_ops_system_update_post",
        "responses": {
          "202": {
            "description": "Update triggered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SystemUpdateResponse"
                }
              }
            }
          },
          "501": {
            "description": "Update mechanism not configured (no Watchtower URL/token)."
          },
          "502": {
            "description": "Watchtower request failed."
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ops/overview/regenerate": {
      "post": {
        "summary": "Regenerate overview.md (manual op, ADR-0078)",
        "description": "Trigger a bounded, degrade-safe regeneration of vault/wiki/overview.md via the configured ingest provider. As of ADR-0078, overview.md is no longer auto-updated on every ingest; use this endpoint to refresh it on demand. Returns status='regenerated' on success or 'degraded' when the provider is unavailable (the previous overview.md is kept; this endpoint never fails the caller).",
        "operationId": "regenerate_overview_endpoint_ops_overview_regenerate_post",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OverviewRegenerateResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/status": {
      "get": {
        "summary": "Service health + data_version",
        "description": "Returns vault_id, current data_version (monotonic ingest counter), service started_at, uptime_seconds, and supports_vision (B2-C1). (AC-REST-1, AC-F16dv-3)",
        "operationId": "get_status_status_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusResponse"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/pages": {
      "get": {
        "summary": "List live pages",
        "description": "Paginated list of pages where deleted_at IS NULL. Supports limit/offset. (AC-REST-2)",
        "operationId": "list_pages_pages_get",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 500,
              "minimum": 1,
              "description": "Max rows to return",
              "default": 50,
              "title": "Limit"
            },
            "description": "Max rows to return"
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "description": "Row offset for pagination",
              "default": 0,
              "title": "Offset"
            },
            "description": "Row offset for pagination"
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Optional frontmatter type filter (e.g. 'query'); server-side, avoids over-fetching + client-side filtering (FE-PERF-2).",
              "title": "Type"
            },
            "description": "Optional frontmatter type filter (e.g. 'query'); server-side, avoids over-fetching + client-side filtering (FE-PERF-2)."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PageListResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "post": {
        "summary": "Create a new wiki page from the UI",
        "description": "Create a new wiki page via the shared write_wiki_page seam (I1, R7-2). Derives slug and subdirectory from title + page_type. 409 if a live page with the same path already exists. Bumps data_version on success. [F1, AC-R7-2-2]",
        "operationId": "create_page_pages_post",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PageCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Page created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PageCreateResponse"
                }
              }
            }
          },
          "409": {
            "description": "A live page with the same path already exists"
          },
          "422": {
            "description": "Validation error (invalid type or dir)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/pages/by-slug/{slug}": {
      "get": {
        "summary": "Resolve a derived citation slug to a page",
        "description": "Resolves a slug as produced by the retrieval citations (slugify(title), not stored in the DB) to the live page with that title. 404 if no live page slugifies to it. (v1.3.3, F5/F6 citation click-through)",
        "operationId": "get_page_by_slug_pages_by_slug__slug__get",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Slug"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PageResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/pages/{page_id}": {
      "get": {
        "summary": "Get a single page by UUID",
        "description": "Returns full page metadata; 404 if unknown or deleted; 422 on invalid UUID. (AC-REST-3, AC-REST-6)",
        "operationId": "get_page_pages__page_id__get",
        "parameters": [
          {
            "name": "page_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Page Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PageResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "delete": {
        "summary": "Cascade-delete a wiki page and clean up dead wikilinks",
        "description": "F13 Cascade Delete (ADR-0026, AC-F13-1..7) + L9 (lint-initiated delete). wiki/ pages only \u2014 409 for meta pages (index.md, log.md, overview.md). Single-pass, inference-free operation: soft-deletes the page (deleted_at=now()); hard-deletes its Qdrant point; rewrites all dead [[Target]] wikilinks to plain text (body-only, frontmatter-safe, I5); removes the index.md catalogue entry; deletes the raw/sources/ file (AQ-v0.5-5); appends 'DELETED | path' to log.md (K4); bumps data_version EXACTLY ONCE (I2); fires the debounced graph recompute (I2). Makes ZERO inference calls, ZERO FA2 calls. 404 on non-existent or already-soft-deleted page (idempotent double-delete, AC-F13-5c). Use POST /pages/{id}/cascade-delete/preview first (ADR-0026 \u00a76 \u2014 mandatory dry-run). NEVER called from any automated path (K8 \u2014 explicit human action only).",
        "operationId": "delete_page_pages__page_id__delete",
        "parameters": [
          {
            "name": "page_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Page Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page deleted; dead wikilinks cleaned; index.md updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CascadeDeleteResponse"
                }
              }
            }
          },
          "404": {
            "description": "Page not found or already deleted (AC-F13-5c)"
          },
          "409": {
            "description": "Meta page (index.md / log.md / overview.md) cannot be deleted via this endpoint"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/pages/{page_id}/related": {
      "get": {
        "summary": "Top-N related pages ranked by 4-signal edge weight",
        "description": "Returns the top *limit* pages most related to *page_id*, ranked by the stored 4-signal edge weight (direct \u00d73, source-overlap \u00d74, Adamic-Adar \u00d71.5, type-affinity \u00d71 \u2014 ADR-0012). Reads the persisted *edges* table directly: no graph recompute, no FA2 (I1/I2). Empty list (200) if the page has no edges yet. 404 if the page is unknown or soft-deleted. limit is capped at 50; default 10.",
        "operationId": "get_related_pages_pages__page_id__related_get",
        "parameters": [
          {
            "name": "page_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Page Id"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 50,
              "minimum": 1,
              "description": "Max results (1\u201350)",
              "default": 10,
              "title": "Limit"
            },
            "description": "Max results (1\u201350)"
          }
        ],
        "responses": {
          "200": {
            "description": "Related pages list (may be empty if no edges yet)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RelatedPagesResponse"
                }
              }
            }
          },
          "404": {
            "description": "Page not found"
          },
          "422": {
            "description": "Invalid page_id UUID or limit out of range"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/pages/{page_id}/content": {
      "get": {
        "summary": "Read raw markdown content of a wiki page",
        "description": "Returns the raw UTF-8 markdown (including YAML frontmatter) for the page identified by *page_id*. The content is read directly from the vault filesystem; no caching layer is applied so callers always get the latest committed bytes. 404 if the page row is unknown or soft-deleted; 410 if the row exists but the file is absent on disk (watcher has not yet re-indexed a deletion in flight); 400 on path-traversal attempt. (F1-content-read, I1, I5)",
        "operationId": "get_page_content_pages__page_id__content_get",
        "parameters": [
          {
            "name": "page_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Page Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page content returned",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PageContentResponse"
                }
              }
            }
          },
          "400": {
            "description": "Path traversal rejected"
          },
          "404": {
            "description": "Page not found in index"
          },
          "410": {
            "description": "Page row exists but file missing on disk"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "put": {
        "summary": "Overwrite the markdown content of a wiki page",
        "description": "Atomically overwrites the markdown file for *page_id* with the supplied content. Only edits pages inside vault/wiki/ \u2014 raw/sources/ files are rejected with 403 (K1 vault layer separation). Write is done via a temp-file + os.replace so a crash mid-write does not corrupt the vault. A trailing newline is enforced (Obsidian / git convention, I5). \n\nValidation (ADR-0035): (a) body > 4 MB \u2192 413; (b) YAML frontmatter must parse cleanly \u2192 422 (protects Obsidian vault validity, I5). \n\nOptimistic concurrency: when *expected_hash* is supplied and does not match the current sha256 of the on-disk file, 409 Conflict is returned \u2014 the caller should reload the page and present the diff to the user before retrying. \n\nRe-indexing (I1/ADR-0035): the watcher observes vault/raw/sources/ only, NOT vault/wiki/. Therefore this endpoint calls reindex_wiki_page_body() INLINE after writing so the Postgres row (content_hash, updated_at, wikilinks) and Qdrant point are updated synchronously before the response is returned. reindex_wiki_page_body() is the purpose-built single-page re-index primitive (ADR-0036 \u00a72.1): it updates content_hash, re-embeds the body into Qdrant, re-derives K5 wikilinks, and bumps data_version ONCE so the debounced GraphCache recompute fires (I2). It does NOT invoke the LLM analyze\u2192generate pipeline \u2014 preserving the user's exact edit (I5). This is a single-page update, never a full rescan (I1). (F1-content-write, I1, I5, ADR-0035)",
        "operationId": "put_page_content_pages__page_id__content_put",
        "parameters": [
          {
            "name": "page_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Page Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PageContentPutRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Content written; new hash returned",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PageContentPutResponse"
                }
              }
            }
          },
          "400": {
            "description": "Path traversal rejected"
          },
          "403": {
            "description": "Path is not inside vault/wiki/ (K1 layer separation)"
          },
          "404": {
            "description": "Page not found"
          },
          "409": {
            "description": "Stale expected_hash \u2014 content was modified since last read"
          },
          "410": {
            "description": "Page row exists but file missing (cannot overwrite)"
          },
          "413": {
            "description": "Content body exceeds _MAX_PAGE_CONTENT_BYTES (4 MB)"
          },
          "422": {
            "description": "YAML frontmatter is invalid \u2014 Obsidian vault would break (I5)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/pages/{page_id}/position": {
      "patch": {
        "summary": "Persist a manual node drag position and pin the node",
        "description": "Updates pages.x/y and sets pages.pinned=true so the node stays at the dropped position across FR recomputes.  Also patches the live GraphCache snapshot in place so the next GET /graph HIT reflects the new position immediately. Does NOT trigger FR, does NOT bump data_version \u2014 O(1). (Feature A, I2)",
        "operationId": "patch_node_position_pages__page_id__position_patch",
        "parameters": [
          {
            "name": "page_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Page Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchPositionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Position updated and node pinned",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PatchPositionResponse"
                }
              }
            }
          },
          "404": {
            "description": "Page not found"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/pages/{page_id}/cascade-delete/preview": {
      "post": {
        "summary": "Dry-run preview of cascade delete (read-only)",
        "description": "F13 Cascade Delete \u2014 mandatory dry-run (ADR-0026 \u00a76, AC-F13-5). Computes the full deletion plan WITHOUT mutating any store or file: no soft-delete, no Qdrant delete, no file write, no data_version bump. Returns will_delete, wikilinks_to_rewrite, shared_entity_warnings, match_methods_used. 404 if the page does not exist or is already soft-deleted. Call this before DELETE /pages/{id} to populate a confirmation modal (AC-F13-6).",
        "operationId": "cascade_delete_preview_pages__page_id__cascade_delete_preview_post",
        "parameters": [
          {
            "name": "page_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Page Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cascade plan computed (read-only)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CascadePreviewResponse"
                }
              }
            }
          },
          "404": {
            "description": "Page not found or already deleted"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ingest/trigger": {
      "post": {
        "summary": "Manually trigger ingest of a single file",
        "description": "Synchronously ingests the file at file_path through the seam. Returns HTTP 202 with typed {task_id, status, page_id} (ADR-0006, AC-REST-4, AC-D4u). status is 'completed' or 'skipped' (I1 fast-path). 429 if per-IP rate limit exceeded (R13-9).",
        "operationId": "trigger_ingest_ingest_trigger_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IngestTriggerRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Ingest accepted and completed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestTriggerResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation error (missing file_path, bad format, or file not found)"
          },
          "429": {
            "description": "Per-IP rate limit exceeded (R13-9)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ingest/upload": {
      "post": {
        "summary": "Upload a document for async watcher-driven ingest (F12 multi-format)",
        "description": "Feature U (ADR-0020 \u00a72, M4-EXT) + F12 Multi-format ingest (ADR-0025 \u00a74.2) + S1 folder-upload support (B3b). Accepts text/markdown (.md/.txt/.markdown), binary formats (.pdf/.docx/.pptx/.xlsx), and placeholder formats (.png/.jpg/.jpeg/.gif/.webp/.mp3/.mp4/.wav/.m4a). Optional 'rel_dir' form field: when supplied by the frontend '+ Folder' picker, the file is written under vault/raw/sources/<rel_dir>/<name> so the subfolder structure is preserved. rel_dir is sanitized segment-by-segment (same rules as safe_source_name) and containment-checked via resolve_under_sources \u2014 422 on any path-traversal attempt. When rel_dir is absent, behaviour is unchanged (writes flat). For text: writes to vault/raw/sources/[rel_dir/]<name>; watcher ingests asynchronously. For binary/placeholder: (1) writes original binary to vault/raw/sources/[rel_dir/]<name>.<ext> (preserved, I5/K1); (2) synchronously extracts text \u2192 companion <stem>.extracted.md (in the same rel_dir subdirectory) with valid YAML frontmatter (I5); (3) returns 202. The watcher ingests ONLY the companion (.md is in _ALLOWED_EXTENSIONS); the binary is ignored by the watcher (I1). Extraction is upload-time, NEVER in the watcher. 413 on oversize (MAX_UPLOAD_BYTES). 415 for truly unknown types. 422 for unsafe filename or unsafe rel_dir. 202 {file_path, status:'queued', overwritten}. 429 if per-IP rate limit exceeded (R13-9).",
        "operationId": "upload_ingest_ingest_upload_post",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/Body_upload_ingest_ingest_upload_post"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "File saved; watcher will ingest asynchronously (companion for binaries)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadResponse"
                }
              }
            }
          },
          "413": {
            "description": "File exceeds MAX_UPLOAD_BYTES"
          },
          "415": {
            "description": "Unsupported file type"
          },
          "422": {
            "description": "Filename or rel_dir is empty or unsafe after sanitization"
          },
          "429": {
            "description": "Per-IP rate limit exceeded (R13-9)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ingest/convert-marker": {
      "post": {
        "summary": "Queue one or more PDFs for async Marker conversion (R11-1, W0)",
        "description": "F12 / R11-1 \u2014 explicit Marker PDF conversion endpoint (W0 async). Accepts multipart files[] (\u226410 files, each \u2264 MARKER_MAX_UPLOAD_BYTES, .pdf only). Validates + saves each raw PDF synchronously, then enqueues background Marker conversion (concurrency=1 \u2014 single-GPU Marker service; I7). Returns 202 immediately with batch_id and per-file entries. Poll GET /ingest/convert-marker/status for per-file progress/result. On success the driver writes <stem>.extracted.md (I5 YAML frontmatter) and the watcher ingests it incrementally (I1). Per-file Marker failures mark that file 'failed' without aborting the rest of the batch. NO silent pypdf fallback (ADR-0051). 400 if > 10 files. 409 if a batch is already running. 413 if any file > MARKER_MAX_UPLOAD_BYTES. 415 for non-.pdf files.",
        "operationId": "convert_marker_ingest_convert_marker_post",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/Body_convert_marker_ingest_convert_marker_post"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Files queued for background Marker conversion.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MarkerConvertAcceptResponse"
                }
              }
            }
          },
          "400": {
            "description": "More than 10 files submitted."
          },
          "409": {
            "description": "A Marker conversion batch is already running."
          },
          "413": {
            "description": "A file exceeds MARKER_MAX_UPLOAD_BYTES."
          },
          "415": {
            "description": "A non-.pdf file was submitted."
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ingest/convert-marker/status": {
      "get": {
        "summary": "Per-file status of the current (or most-recent) Marker conversion batch",
        "description": "Returns per-file conversion status for the current or most-recent POST /ingest/convert-marker batch. Status values: 'pending' (not yet started) | 'converting' (Marker call in flight) | 'ok' (companion written; watcher will ingest) | 'failed' (Marker error, detail set). When running=false and total=0, no batch has been submitted this session. Safe to poll every 2-5 s; pure in-memory, no DB I/O.",
        "operationId": "get_convert_marker_status_ingest_convert_marker_status_get",
        "responses": {
          "200": {
            "description": "Batch status snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MarkerBatchStatusResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ingest/marker-health": {
      "get": {
        "summary": "Proxy the Marker microservice health check (R11-1)",
        "description": "R11-1 \u2014 GET {effective MARKER_SERVICE_URL}/health proxy. Returns {'status':'ok'} (200) when Marker responds 200. Returns {'status':'offline','detail':'...'} (503) when unreachable or non-200. Uses the effective Marker URL (env baseline + DB override via config_overrides S2).",
        "operationId": "marker_health_ingest_marker_health_get",
        "responses": {
          "200": {
            "description": "Marker is reachable and healthy.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MarkerHealthResponse"
                }
              }
            }
          },
          "503": {
            "description": "Marker is offline or unreachable."
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ingest/from-text": {
      "post": {
        "summary": "Write inline text to raw/sources/ and queue watcher-driven ingest",
        "description": "Save-to-wiki seam (ADR-0019 \u00a72.7, AC-F6-5). Materialises ``text`` to ``vault/raw/sources/chat-<hint>.md`` and returns 202 immediately. The watcher picks up the file and runs the full ingest pipeline (no new ingest logic \u2014 ADR-0003 guarantee, I1/I6). ``source_hint`` is sanitised to a safe basename; falls back to ``chat-<uuid>`` when omitted or unsafe. 422 on empty text. 429 if per-IP rate limit exceeded (R13-9).",
        "operationId": "ingest_from_text_ingest_from_text_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IngestFromTextRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Text saved; watcher will ingest asynchronously",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestFromTextResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation error (text empty or too long)"
          },
          "429": {
            "description": "Per-IP rate limit exceeded (R13-9)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ingest/runs": {
      "get": {
        "summary": "List ingest run history",
        "description": "Returns a paginated, started_at DESC list of ingest_runs rows. Exposes the I7 cost ledger to the user (AC-BE-IR-1..5, ADR-0018 \u00a77). limit: 1..100 default 20; offset: >=0 default 0; vault_id: optional UUID filter. Column aliases: max_iter_used\u2192iterations_used, finished_at\u2192completed_at. total_cost_usd serialised as a number; frontend formats to exactly 4dp (I7).",
        "operationId": "list_ingest_runs_ingest_runs_get",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 100,
              "minimum": 1,
              "description": "Max rows to return (1..100); 422 on out-of-range (AC-BE-IR-2)",
              "default": 20,
              "title": "Limit"
            },
            "description": "Max rows to return (1..100); 422 on out-of-range (AC-BE-IR-2)"
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "description": "Row offset for pagination (>=0); 422 on negative (AC-BE-IR-2)",
              "default": 0,
              "title": "Offset"
            },
            "description": "Row offset for pagination (>=0); 422 on negative (AC-BE-IR-2)"
          },
          {
            "name": "vault_id",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Optional vault_id filter; omit to list all vaults (AC-BE-IR-2)",
              "title": "Vault Id"
            },
            "description": "Optional vault_id filter; omit to list all vaults (AC-BE-IR-2)"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated ingest run list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestRunListResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation error (limit out of 1..100 or offset < 0)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ingest/queue": {
      "get": {
        "summary": "Live ingest activity queue snapshot",
        "description": "Returns the live in-memory queue state: processing (in-flight), pending (paused), failed (retained for retry), completed_since_idle, and per-task details. Pure in-memory \u2014 no DB scan. Safe to poll every 5 s (ADR-0046 \u00a76, I3). (ADR-0046)",
        "operationId": "get_ingest_queue_ingest_queue_get",
        "responses": {
          "200": {
            "description": "Queue snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueueSnapshotResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ingest/runs/{run_id}/cancel": {
      "post": {
        "summary": "Request cancellation of an in-flight ingest run",
        "description": "Sets the cooperative cancel event for the run. The loop checks the event at the next iteration boundary (never mid-provider-call, I7/I6). Cascade-deletes any pages written so far (I1) once the boundary is reached. 202 = cancel requested. 404 = run_id unknown. 409 = run already in a terminal state. (ADR-0046 \u00a73/\u00a76)",
        "operationId": "cancel_ingest_run_ingest_runs__run_id__cancel_post",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Run Id"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Cancel requested \u2014 cleanup happens asynchronously",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueueCancelResponse"
                }
              }
            }
          },
          "404": {
            "description": "run_id not found in the active queue"
          },
          "409": {
            "description": "Run is already in a terminal state (completed/failed/cancelled)"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ingest/{run_id}": {
      "delete": {
        "summary": "Cancel a queued or running ingest run (R13-3)",
        "description": "QUEUED (not yet started, in _pending): removes the run from the queue so it never starts. Returns 200 {'status': 'cancelled'}. RUNNING (in-flight): signals cooperative cancellation via cancel_event. Returns 202 {'status': 'cancelling'} \u2014 cleanup completes asynchronously. Already terminal (completed/failed/cancelled): 409. Unknown run_id: 404. (R13-3)",
        "operationId": "delete_ingest_run_ingest__run_id__delete",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Run Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Run was queued and has been immediately cancelled",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          },
          "202": {
            "description": "Cancel requested \u2014 pipeline will abort at next loop boundary"
          },
          "404": {
            "description": "run_id not found"
          },
          "409": {
            "description": "Run is already in a terminal state"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ingest/runs/{run_id}/retry": {
      "post": {
        "summary": "Retry a failed ingest run",
        "description": "Re-dispatches the source file for re-ingest, incrementing retry_count. Hard cap: 3 retries (I7). 202 = re-dispatch accepted. 404 = run_id unknown. 409 detail='max_retries_exceeded' when retry_count >= 3. 409 detail='not_retryable' when run is still active. (ADR-0046 \u00a75/\u00a76)",
        "operationId": "retry_ingest_run_ingest_runs__run_id__retry_post",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Run Id"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Retry dispatched",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueueRetryResponse"
                }
              }
            }
          },
          "404": {
            "description": "run_id unknown"
          },
          "409": {
            "description": "max_retries_exceeded or run is not in a retryable state"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ingest/queue/pause": {
      "post": {
        "summary": "Pause the ingest queue",
        "description": "Pauses dispatch \u2014 new FS events are parked in memory until resume. Idempotent; calling while already paused is a no-op. (ADR-0046 \u00a74)",
        "operationId": "pause_ingest_queue_ingest_queue_pause_post",
        "responses": {
          "200": {
            "description": "Queue paused (idempotent)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueuePauseResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/ingest/queue/resume": {
      "post": {
        "summary": "Resume the ingest queue",
        "description": "Resumes dispatch and drains any parked pending events through the watcher's normal debounce path. Idempotent; calling while not paused replays any stale pending entries. (ADR-0046 \u00a74)",
        "operationId": "resume_ingest_queue_ingest_queue_resume_post",
        "responses": {
          "200": {
            "description": "Queue resumed; pending entries replayed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueueResumeResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/search": {
      "get": {
        "summary": "4-phase RAG retrieval (F5, ADR-0022)",
        "description": "Run the F5 4-phase retrieval pipeline (ADR-0022 \u00a72.2, AC-F5-6) and return a grounded context string + citation list. Phase 1: dense vector search via bge-m3 (Qdrant, top-k). Phase 2: BFS graph-expansion over the `edges` table (depth \u2264 2). Phase 3: token-budget allocation (20% of context_window, F14). Phase 4: context assembly with inline [n] markers. 0-hit query \u2192 200 with empty results + empty context (AC-F5-7a). READ-ONLY \u2014 never bumps data_version (AC-F5-5). R8-5: optional `type` (comma-separated page types) and `sort` (relevance|date_desc|date_asc) params \u2014 422 on unknown type or sort value (AC-R8-5-1). Documented in openapi.json (I8, AC-F5-6).",
        "operationId": "search_search_get",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "description": "The query string to retrieve context for",
              "title": "Q"
            },
            "description": "The query string to retrieve context for"
          },
          {
            "name": "vault_id",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Vault scope; defaults to settings.vault_id",
              "title": "Vault Id"
            },
            "description": "Vault scope; defaults to settings.vault_id"
          },
          {
            "name": "k",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 50,
              "minimum": 1,
              "description": "Dense top-k for the vector phase (1..50); default 8 (ADR-0022 \u00a72.1)",
              "default": 8,
              "title": "K"
            },
            "description": "Dense top-k for the vector phase (1..50); default 8 (ADR-0022 \u00a72.1)"
          },
          {
            "name": "context_window",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "integer",
                  "maximum": 1000000,
                  "minimum": 4096
                },
                {
                  "type": "null"
                }
              ],
              "description": "Context window override (4096..1_000_000); null \u2192 32 768 default (F14)",
              "title": "Context Window"
            },
            "description": "Context window override (4096..1_000_000); null \u2192 32 768 default (F14)"
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "R8-5: Comma-separated page types to filter results. Valid values: entity, concept, source, synthesis, comparison, query. Multiple values: type=entity,concept. Omit to return all types (AC-R8-5-1). Unknown value \u2192 422.",
              "title": "Type"
            },
            "description": "R8-5: Comma-separated page types to filter results. Valid values: entity, concept, source, synthesis, comparison, query. Multiple values: type=entity,concept. Omit to return all types (AC-R8-5-1). Unknown value \u2192 422."
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "R8-5: Sort order for results. relevance (default) = cosine/edge-weight ranking unchanged. date_desc = newest first (updated_at DESC). date_asc = oldest first (updated_at ASC). Phase internals (budgets, BFS) are never changed (I7). Unknown value \u2192 422 (AC-R8-5-1).",
              "title": "Sort"
            },
            "description": "R8-5: Sort order for results. relevance (default) = cosine/edge-weight ranking unchanged. date_desc = newest first (updated_at DESC). date_asc = oldest first (updated_at ASC). Phase internals (budgets, BFS) are never changed (I7). Unknown value \u2192 422 (AC-R8-5-1)."
          }
        ],
        "responses": {
          "200": {
            "description": "Retrieval result (0-hit \u2192 empty results array)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation error (k out of range, missing q, or unknown type/sort)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/provider/cli-auth": {
      "get": {
        "summary": "Read-only CLI subscription OAuth token posture (ADR-0043)",
        "description": "Returns the current posture of the CLI provider subscription token: token_configured (bool, never the value), token_source (db|env|none), auth_mode (api-key|subscription|unconfigured). Mirrors GET /clip/config: no sensitive values ever returned. ADR-0043 \u00a72.5.",
        "operationId": "get_cli_auth_config_provider_cli_auth_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CliAuthConfigResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "put": {
        "summary": "Set or clear the CLI subscription OAuth token (ADR-0043 / W7)",
        "description": "ADR-0043 \u00a72.5 (W7 amendment) \u2014 store a pasted Claude subscription OAuth token or clear it. clear=true: set DB token to NULL (falls back to env / none). token=<value>: validate, Fernet-encrypt (requires SYNAPSE_SECRET_KEY \u2014 else HTTP 400), and store in vault_state.cli_oauth_token_encrypted; refresh cache. Returns post-write posture (same shape as GET); NEVER the token value. 400 if body has neither token nor clear. 400 if SYNAPSE_SECRET_KEY is unset when storing a new token (fail-closed). 422 if token is empty/whitespace or absurd length. Soft prefix check warns but does NOT hard-reject \u2014 ADR-0043 \u00a72.5.",
        "operationId": "put_cli_auth_config_provider_cli_auth_put",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CliAuthConfigRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CliAuthConfigResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/provider/config": {
      "get": {
        "summary": "List provider_config rows",
        "description": "Returns all raw provider_config rows. No API key field is stored or returned (\u00a712). (F17, AC-F17-6)",
        "operationId": "list_provider_configs_provider_config_get",
        "parameters": [
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Filter by scope (global|vault|operation)",
              "title": "Scope"
            },
            "description": "Filter by scope (global|vault|operation)"
          },
          {
            "name": "vault_id",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Filter by vault_id",
              "title": "Vault Id"
            },
            "description": "Filter by vault_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderConfigListResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "post": {
        "summary": "Create a provider_config row",
        "description": "Create a new provider_config row. provider_type must be one of: local | api | cli. api_key is WRITE-ONLY (W1): encrypted at rest and NEVER returned \u2014 the response exposes api_key_configured + api_key_masked only. Supplying api_key requires SYNAPSE_SECRET_KEY server-side (else HTTP 400). Omit api_key to keep env-var keys. (F17, ADR-0008, W1)",
        "operationId": "create_provider_config_provider_config_post",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProviderConfigCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Row created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderConfigResponse"
                }
              }
            }
          },
          "400": {
            "description": "api_key supplied but SYNAPSE_SECRET_KEY not configured"
          },
          "422": {
            "description": "Validation error (invalid provider_type, scope, or operation)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/provider/config/{config_id}": {
      "put": {
        "summary": "Update a provider_config row",
        "description": "Partial update of a provider_config row (W1). Omitted fields are left unchanged. api_key is WRITE-ONLY: a non-empty value replaces the stored key (encrypted); an empty string \"\" CLEARS it (env fallback); omitting it leaves the key untouched. Supplying a non-empty api_key requires SYNAPSE_SECRET_KEY server-side (else HTTP 400). The plaintext is NEVER returned. (F17, W1)",
        "operationId": "update_provider_config_provider_config__config_id__put",
        "parameters": [
          {
            "name": "config_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Config Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProviderConfigUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Row updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderConfigResponse"
                }
              }
            }
          },
          "400": {
            "description": "api_key supplied but SYNAPSE_SECRET_KEY not configured"
          },
          "404": {
            "description": "Row not found"
          },
          "422": {
            "description": "Validation error"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "delete": {
        "summary": "Delete a provider_config row by UUID",
        "description": "Hard-delete the provider_config row with the given id. (F17)",
        "operationId": "delete_provider_config_provider_config__config_id__delete",
        "parameters": [
          {
            "name": "config_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Config Id"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Row deleted"
          },
          "404": {
            "description": "Row not found"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/provider/vendors": {
      "get": {
        "summary": "List the provider vendor catalog",
        "description": "Returns the curated one-row-per-vendor catalog for the Settings 'LLM Models' UI (W1). Each entry carries id, display_name, provider_type (api|local|cli), default_base_url, needs_api_key, model_presets, and notes. Static \u2014 no secrets, no DB. (F17, W1)",
        "operationId": "list_provider_vendors_provider_vendors_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VendorListResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/provider/test/connection": {
      "post": {
        "summary": "Bounded provider connection probe (W1)",
        "description": "One bounded, token-capped call (timeout _PROVIDER_TEST_TIMEOUT_S) to verify the provider endpoint responds. Accepts a config_id (uses the stored, decrypted key) or an inline {provider_type, model, base_url?, api_key?}. Returns {ok, latency_ms, detail}; NEVER echoes the key. CLI backend is posture-checked, not live-probed. (F17, W1, I7)",
        "operationId": "provider_test_connection_provider_test_connection_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProviderTestRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderTestResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/provider/test/function": {
      "post": {
        "summary": "Bounded provider instruction-follow probe (W1)",
        "description": "One bounded, token-capped call asking the model to reply exactly 'OK'; ok=true iff the reply contains 'OK'. Same input contract and safety as /provider/test/connection. (F17, W1, I7)",
        "operationId": "provider_test_function_provider_test_function_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProviderTestRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderTestResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/config/embedding": {
      "get": {
        "summary": "Get current embedding configuration",
        "description": "Returns the active embedding config read from environment variables (EMBEDDING_URL, EMBEDDING_MODEL, EMBEDDING_DIM, EMBEDDINGS_ENABLED). Read-only \u2014 edit .env to change. (I9, ADR-0030)",
        "operationId": "get_embedding_config_config_embedding_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmbeddingConfigResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/mcp/info": {
      "get": {
        "summary": "Read-only MCP server introspection",
        "description": "Returns the live FastMCP server metadata: name, transport, entry-point command, and the full list of registered tools (name, description, input_schema). All values are derived from the live `mcp` object and settings \u2014 nothing hardcoded (I6). No MCP transport session is opened; no tool is invoked (I9). Read-only \u2014 edit MCP_TRANSPORT / MCP_ENTRY_COMMAND env vars to change. F1-MCP-UI (ADR-0027 \u00a72.1).",
        "operationId": "get_mcp_info_mcp_info_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/McpInfoResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/mcp/remote": {
      "put": {
        "summary": "Toggle the remote MCP HTTP surface at runtime",
        "description": "Persists vault_state.remote_mcp_enabled for the active vault and refreshes the in-process RemoteMcpFlag cache immediately (ADR-0032 \u00a72.2/\u00a72.4). Token-floor clamp: if MCP_AUTH_TOKEN is unset and enabled=true, the flag is forced to false and clamped=true is returned (HTTP 200). enabled=false always succeeds. Same-origin / unauthenticated \u2014 consistent with the rest of the REST API (ADR-0028 / ADR-0032 \u00a72.4). F1-MCP-UI (ADR-0032).",
        "operationId": "put_mcp_remote_mcp_remote_put",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/McpRemoteToggleRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/McpRemoteStateResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/mcp/remote-write": {
      "put": {
        "summary": "Toggle the remote MCP write tools at runtime",
        "description": "Persists vault_state.remote_mcp_write_enabled for the active vault and refreshes the in-process _mcp_write_flag cache immediately (ADR-0072 \u00a72/\u00a74). Token-floor clamp: if neither a token is configured (DB hash or MCP_AUTH_TOKEN env) nor allow_without_token is set, and enabled=true, the flag is forced to false and clamped=true is returned (HTTP 200). enabled=false always succeeds. Write tools (write_page, resolve_review, trigger_source_rescan) are always listed on the HTTP surface but error at call time when the flag is off (ADR-0072 \u00a73). Same-origin / unauthenticated \u2014 consistent with the rest of the REST API (ADR-0028 / ADR-0072 \u00a74). F17 [ADR-0072].",
        "operationId": "put_mcp_remote_write_mcp_remote_write_put",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/McpRemoteWriteToggleRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/McpRemoteWriteStateResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/mcp/auth": {
      "put": {
        "summary": "Set, rotate, or clear the MCP access token + allow-without-token flag",
        "description": "ADR-0033 \u00a72.5 \u2014 UI-settable MCP token management. rotate_token=true: generate a new token (secrets.token_urlsafe(32)), store its PBKDF2 hash in vault_state, return plaintext ONCE in generated_token. token=<value>: store an explicit token as hash; NOT echoed; generated_token=null. clear_token=true: set hash to NULL (token_source may fall back to env or none). allow_without_token: persist the private-source allow flag. If post-write state has no token AND allow_without_token=false, remote_enabled is clamped OFF (allow-aware clamp \u2014 ADR-0033 \u00a72.4). Same-origin / unauthenticated (consistent with ADR-0032 \u00a72.4). NEVER returns or stores token plaintext (except the one-time generated_token). F1-MCP-UI (ADR-0033).",
        "operationId": "put_mcp_auth_mcp_auth_put",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/McpAuthRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/McpAuthStateResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/import-schedule": {
      "get": {
        "summary": "Get scheduled folder import config + last-run status",
        "description": "Returns the current import schedule configuration and last-run status for the vault. Returns sane defaults (enabled=false, frequency='1h') if no row has been configured yet. Feature S (ADR-0020 \u00a74.6).",
        "operationId": "get_import_schedule_import_schedule_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportScheduleResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "put": {
        "summary": "Upsert import schedule configuration",
        "description": "Create or update the import schedule for the vault. Validates source_dir: if the directory does not exist inside the container, the row is still saved but dir_ok=false + dir_message is returned (save-then-warn). frequency must be one of '15m' | '1h' | '6h' | 'daily'. Config changes take effect on the next scheduler tick without a restart. Feature S (ADR-0020 \u00a74.6).",
        "operationId": "put_import_schedule_import_schedule_put",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImportSchedulePutBody"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Config saved (dir_ok may be false if mount is missing)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportSchedulePutResponse"
                }
              }
            }
          },
          "422": {
            "description": "Invalid frequency value"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/import-schedule/run-now": {
      "post": {
        "summary": "Trigger one bounded import scan immediately",
        "description": "Trigger one bounded scan of source_dir immediately (same bounds as the scheduler: IMPORT_SCAN_MAX_FILES + IMPORT_SCAN_MAX_SECONDS, I7). The scan runs in the background; poll GET /import-schedule for the result. 409 if a scan is already in-flight. 400 if disabled or source_dir unset/missing. Feature S (ADR-0020 \u00a74.6).",
        "operationId": "run_import_now_import_schedule_run_now_post",
        "responses": {
          "202": {
            "description": "Scan started in the background",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RunNowResponse"
                }
              }
            }
          },
          "400": {
            "description": "Schedule is disabled, source_dir not set, or directory missing"
          },
          "409": {
            "description": "A scan is already in-flight (I7 \u2014 no overlap)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/clip/config": {
      "get": {
        "summary": "Read-only web clipper ingress posture (ADR-0040)",
        "description": "Returns the current posture of the POST /clip ingress: enabled state, token_configured (bool, never the value), token_source (db|env|none), allowed_origins list, and max_body_bytes. Mirrors GET /mcp/info: no sensitive values ever returned. F11-clip-config (ADR-0040).",
        "operationId": "get_clip_config_clip_config_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClipConfigResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "put": {
        "summary": "Set, rotate, or clear the clip ingress token + enabled/origins (ADR-0040)",
        "description": "ADR-0040 \u00a72.4 \u2014 runtime web clipper configuration. rotate_token=true: generate a new token (secrets.token_urlsafe(32)), store its PBKDF2-SHA256 hash in vault_state.clip_access_token, return plaintext ONCE in generated_token (never stored). clear_token=true: set DB token to NULL (falls back to CLIP_TOKEN env or none). set_enabled: set clip_enabled_db (DB wins over CLIP_ENABLED env when set). set_allowed_origins: replace DB origins (empty string \"\" clears to env fallback). Same-origin / unauthenticated \u2014 consistent with PUT /mcp/auth (ADR-0033 \u00a72.5). NEVER returns or stores the token plaintext (except the one-time generated_token). F11-clip-config (ADR-0040).",
        "operationId": "put_clip_config_clip_config_put",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClipConfigRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClipConfigStateResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/web-search/config": {
      "get": {
        "summary": "Read-only SearXNG web-search posture (ADR-0041)",
        "description": "Returns the current SearXNG configuration: configured flag, resolved URL, categories, max_queries, and source (db|env|none). DB value wins over env when set (ADR-0041 \u00a72.2). The URL is NOT a secret and IS returned in full. F10-web-search-config (ADR-0041).",
        "operationId": "get_web_search_config_web_search_config_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebSearchConfigResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "put": {
        "summary": "Set or clear the SearXNG web-search configuration (ADR-0041)",
        "description": "ADR-0041 \u00a72.4 \u2014 runtime SearXNG configuration. set_url: set searxng_url_db (validates http/https; DB wins over SEARXNG_URL env). set_categories: set searxng_categories_db (comma-separated; empty string clears). set_max_queries: set searxng_max_queries_db (1\u201350; DB wins over env). clear=true: clear ALL three DB columns (falls back to env / code defaults). I9 invariant: SearXNG is the ONLY web-search backend. No provider field accepted \u2014 any attempt to configure a non-SearXNG provider is rejected. F10-web-search-config (ADR-0041).",
        "operationId": "put_web_search_config_web_search_config_put",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebSearchConfigRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebSearchConfigStateResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/web-search/provider-keys": {
      "get": {
        "summary": "Masked posture of cloud web-search provider API keys (P3-e)",
        "description": "Read-only masked posture for the opt-in cloud web-search providers. NEVER returns the key value \u2014 only whether one is set and its source (db | env | none). Keys are stored Fernet-encrypted at rest and require SYNAPSE_SECRET_KEY to set via the UI (ADR-0071).",
        "operationId": "get_web_search_provider_keys_web_search_provider_keys_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebSearchProviderKeysResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "put": {
        "summary": "Set or clear a cloud web-search provider API key (P3-e)",
        "description": "Store (encrypted at rest) or clear one cloud provider's API key. Setting a key requires SYNAPSE_SECRET_KEY (400 when absent) \u2014 mirrors the CLI-auth token contract (ADR-0043/W7). The stored key wins over the env `{PROVIDER}_API_KEY` fallback. The plaintext is never logged or returned. ADR-0071.",
        "operationId": "put_web_search_provider_key_web_search_provider_keys_put",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebSearchProviderKeyRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Key stored/cleared; returns the refreshed masked posture",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebSearchProviderKeysResponse"
                }
              }
            }
          },
          "400": {
            "description": "SYNAPSE_SECRET_KEY not set (cannot encrypt) or invalid provider/key"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/config/app": {
      "get": {
        "summary": "List all 13 runtime config overrides with effective value + source (ADR-0053)",
        "description": "Returns the 13 migrated settings (S1..S13) in stable order. Each entry has key, effective value (override wins over env), and source ('override' iff a DB row exists, else 'env'). All values from in-process cache \u2014 no DB round-trip (I7). Never returns infra/secret keys (allow-list boundary \u2014 ADR-0053 \u00a72.2/\u00a72.4). Auth: SynapseAuthMiddleware (ADR-0052 / ADR-0053 \u00a73 \u2014 BearerAuth in OpenAPI).",
        "operationId": "get_app_config_config_app_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppConfigListResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/config/app/{key}": {
      "put": {
        "summary": "Upsert one config override (ADR-0053)",
        "description": "Sets or updates the DB override for one of the 13 allowed keys. Returns 204 on success. Returns 400 with the stable error envelope (code='invalid_key', details.allowed=[...]) for a non-allowed key (ADR-0086). Returns 422 on value validation failure (per-key rules \u2014 ADR-0053 \u00a72.3). Auth: SynapseAuthMiddleware (ADR-0052). After write, the in-process cache is refreshed immediately so a subsequent GET /config/app reflects source='override' (ADR-0053 \u00a73.2).",
        "operationId": "put_app_config_config_app__key__put",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Key"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AppConfigPutBody"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Override stored; effective value updated in cache."
          },
          "400": {
            "description": "Key not in allow-list."
          },
          "422": {
            "description": "Value fails per-key validation rule (ADR-0053 \u00a72.3)."
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "delete": {
        "summary": "Remove a config override \u2192 revert to env default (ADR-0053 \u00a73.3)",
        "description": "Deletes the app_config row for *key*, reverting the setting to its env baseline. Returns 204 (idempotent \u2014 no-op if no row existed). Returns 400 with the stable error envelope (code='invalid_key', details.allowed=[...]) for a non-allowed key (ADR-0086). Auth: SynapseAuthMiddleware (ADR-0052). Reset is DELETE, not PUT null: app_config.value is NOT NULL by design (ADR-0053 \u00a73.3). S7 overview_language: frontend sends DELETE for the '(auto)' choice (\u00a72.3).",
        "operationId": "delete_app_config_config_app__key__delete",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Key"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Override removed; setting reverts to env default."
          },
          "400": {
            "description": "Key not in allow-list."
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/config/api-tokens": {
      "get": {
        "summary": "List active API tokens",
        "description": "Lists non-revoked api_tokens rows \u2014 label, scope, read_only, last_used_at, created_at. NEVER includes the secret or its hash. PF-AUTH-1 (1.9.4 W4).",
        "operationId": "list_api_tokens_config_api_tokens_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiTokenListResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "post": {
        "summary": "Create a scoped API token",
        "description": "Generates a new bearer secret (secrets.token_urlsafe), persists only its PBKDF2-SHA256 hash (app.runtime_state.hash_token), and returns the PLAINTEXT secret exactly once in this response. It is unrecoverable after that \u2014 losing it means creating a new token and revoking this one. vault_id=null (default) creates a global token; a non-null vault_id scopes the token to that vault (rejected as an invalid token on any other vault_id). read_only=true restricts the token to GET/HEAD/OPTIONS requests. PF-AUTH-1 (1.9.4 W4).",
        "operationId": "create_api_token_config_api_tokens_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApiTokenCreateRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiTokenCreateResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/config/api-tokens/{token_id}": {
      "delete": {
        "summary": "Revoke an API token",
        "description": "Soft-deletes the row (revoked_at = now()) and removes it from the in-process ApiTokenCache immediately \u2014 the token stops working on the very next request. 404 if the id does not exist or is already revoked. PF-AUTH-1 (1.9.4 W4).",
        "operationId": "revoke_api_token_config_api_tokens__token_id__delete",
        "parameters": [
          {
            "name": "token_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Token Id"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/conversations": {
      "get": {
        "summary": "List chat conversations for a vault",
        "description": "Returns live (non-soft-deleted) conversations for a vault, ordered updated_at DESC (drives last-active restore, AC-F6-1). Paginated (limit 1..100, offset >=0). F6.",
        "operationId": "list_conversations_conversations_get",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 100,
              "minimum": 1,
              "default": 50,
              "title": "Limit"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0,
              "title": "Offset"
            }
          },
          {
            "name": "vault_id",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Defaults to settings.vault_id",
              "title": "Vault Id"
            },
            "description": "Defaults to settings.vault_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationListResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "post": {
        "summary": "Create an empty chat conversation",
        "description": "Create a conversation {vault_id?, title?}. Also implicitly created by /chat/stream when conversation_id is null. F6 (ADR-0019 \u00a72.5).",
        "operationId": "create_conversation_conversations_post",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConversationCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/conversations/{conversation_id}/messages": {
      "get": {
        "summary": "Get ordered message history for a conversation",
        "description": "Messages ordered created_at ASC. content is RAW incl. literal <think>\u2026 (AC-F7-2). 404 if the conversation is unknown/soft-deleted. F6.",
        "operationId": "get_conversation_messages_conversations__conversation_id__messages_get",
        "parameters": [
          {
            "name": "conversation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Conversation Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatMessageListResponse"
                }
              }
            }
          },
          "404": {
            "description": "Conversation not found"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/conversations/{conversation_id}": {
      "delete": {
        "summary": "Soft-delete a conversation",
        "description": "Sets deleted_at (ADR-0005 pattern). 404 if unknown/already deleted. F6.",
        "operationId": "delete_conversation_conversations__conversation_id__delete",
        "parameters": [
          {
            "name": "conversation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Conversation Id"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Soft-deleted"
          },
          "404": {
            "description": "Not found"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "patch": {
        "summary": "Rename a conversation",
        "description": "Update the title of a live (non-deleted) conversation (R7-3, AC-R7-3-1, [F6]). 404 if the conversation is unknown or soft-deleted. body: {title: str 1..200}.",
        "operationId": "rename_conversation_conversations__conversation_id__patch",
        "parameters": [
          {
            "name": "conversation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Conversation Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConversationRenameRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Conversation renamed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationRenameResponse"
                }
              }
            }
          },
          "404": {
            "description": "Conversation not found or deleted"
          },
          "422": {
            "description": "Validation error (title empty or too long)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/chat/stream": {
      "post": {
        "summary": "Stream a chat turn (NDJSON)",
        "description": "Bounded chat turn (F6/F7, I6/I7, ADR-0019 \u00a72.2). Returns 200 with application/x-ndjson: one JSON event per line (token | think | done | error). Routes via resolve_provider_config('chat', vault_id) \u2014 never a hardcoded provider (I6). Bounded by token_budget + timeout (I7); total_cost_usd in the done event. 404 if conversation_id is unknown; 503 if no chat provider resolves. 429 if per-IP rate limit exceeded (R13-9).",
        "operationId": "chat_stream_chat_stream_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "NDJSON event stream",
            "content": {
              "application/json": {
                "schema": {}
              },
              "application/x-ndjson": {}
            }
          },
          "404": {
            "description": "conversation_id provided but unknown"
          },
          "422": {
            "description": "Body validation failure"
          },
          "429": {
            "description": "Per-IP rate limit exceeded (R13-9)"
          },
          "503": {
            "description": "No chat provider_config resolves (I6)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/chat/save-to-wiki": {
      "post": {
        "summary": "Save a chat answer to the wiki (synthesis by default, query if it is a question)",
        "description": "Mirrors nashsu/llm_wiki F6 'Save to Wiki' (CG-A2/A5, ADR-0067 P3-3): saves a cleaned assistant answer as wiki/synthesis/<slug(title)>.md with type=synthesis frontmatter \u2014 OR wiki/queries/<slug>.md with type=query when the saved content is itself an open question (title/first line ends with '?' or is interrogative). Strips <think>\u2026</think> and <!-- cited: \u2026 --> transport artifacts before saving. Persists via the single write_wiki_page seam (I1 \u2014 one data_version bump, no rescan; ADR-0067 D2 frontmatter + resolved related). The write makes no provider call (I6); a bounded fire-and-forget wikilink-enrichment pass may follow to graph-connect the page (CG-A3, degrade-safe). Returns {page_id, file_path}. 422 if title/content is missing.",
        "operationId": "save_chat_to_wiki_chat_save_to_wiki_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SaveToWikiRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Page created or updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SaveToWikiResponse"
                }
              }
            }
          },
          "422": {
            "description": "title or content is missing / empty"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/overview/regenerate": {
      "post": {
        "summary": "Regenerate the overview note in the vault language (F3)",
        "description": "Force a regeneration of the single auto-maintained overview.md note WITHOUT re-ingesting a source. Same seam as the per-ingest overview regen (nashsu/llm_wiki parity): resolves the ingest provider (I6), one bounded provider.chat() call (I7), overwrites overview.md with valid frontmatter (type: overview, I5), indexes it as a Page, and bumps data_version so the graph/tree refresh (I2). The language is detected from existing pages so the overview matches the vault content language (not defaulted to English). Degrade-safe: keeps the previous overview.md on provider failure/timeout.",
        "operationId": "regenerate_overview_overview_regenerate_post",
        "responses": {
          "200": {
            "description": "Overview regeneration attempted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegenerateOverviewResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/links/reresolve": {
      "post": {
        "summary": "Re-resolve dangling wikilinks against current pages (F3/K3)",
        "description": "Backfill for cross-ingest graph connectivity: re-resolves every dangling Link against the current live pages using the tolerant matcher (exact \u2192 case-insensitive \u2192 slug). Links whose target now matches a live page are reconnected (target_page_id set, dangling cleared). Bounded single pass (I7 \u2014 two queries, no per-row round-trips). Bumps data_version once so the debounced GraphCache recomputes with the new edges (I2). Returns {reconnected, remaining_dangling}.",
        "operationId": "reresolve_links_links_reresolve_post",
        "responses": {
          "200": {
            "description": "Backfill completed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReresolveLinksResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/graph": {
      "get": {
        "summary": "Precomputed knowledge graph (nodes + edges with FA2 coordinates)",
        "description": "Returns the precomputed graph with FA2 layout coordinates (I2, F4, ADR-0014). HIT (X-Graph-Cache: hit): pure read from persisted coords + edges \u2014 no FA2. MISS (X-Graph-Cache: miss): one inline synchronous recompute, then return. Synchronous 200 \u2014 never 202 (AQ-v0.3-3). A second request at the same data_version is always a HIT (G2).",
        "operationId": "get_graph_graph_get",
        "responses": {
          "200": {
            "description": "Graph payload with precomputed coords",
            "headers": {
              "X-Graph-Cache": {
                "description": "hit|miss \u2014 mirrors the cached field (ADR-0014 \u00a75)",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GraphResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/graph/recompute": {
      "post": {
        "summary": "Regenerate the graph: reconnect cross-ingest links + force FA2 recompute (F4/I2)",
        "description": "The 'Regenerate graph' action. Two bounded steps: (1) re-resolve dangling [[wikilinks]] against current pages (tolerant matcher: exact \u2192 case-insensitive \u2192 slug) so ingests link to each other; (2) FORCE a fresh server-side ForceAtlas2 recompute (I2) \u2014 invalidating the cache marker so the layout re-runs even when data_version has not changed. The recompute applies the ADR-0045 \u00a75 outlier clamp, which tames FA2 runaway nodes that would otherwise collapse the dense core in the viewer. Layout stays server-side (I2 \u2014 never on the client); the recompute is a single inline FA2 run under the cache in-flight guard (no concurrent FA2). Returns reconnected/remaining_dangling counts + fresh node/edge counts.",
        "operationId": "recompute_graph_graph_recompute_post",
        "responses": {
          "200": {
            "description": "Links reconnected + graph recomputed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegenerateGraphResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/graph/communities/{community_id}": {
      "get": {
        "summary": "Graph community drill-down: members + cohesion score (R9-5)",
        "description": "Returns member pages and cohesion for a Louvain community (R9-5, AC-R9-5-1). Data is read from the cached GraphSnapshot \u2014 NEVER triggers a graph recompute (I2). If the cache is cold (graph has never been computed), returns 409 with a clear message; the client should trigger POST /graph/recompute first. Members are ordered by degree descending, capped at 100 entries. cohesion_warning=true when cohesion < GRAPH_COHESION_WARN (default 0.2, env GRAPH_COHESION_WARN). Returns 404 if the community_id does not exist in the current snapshot.",
        "operationId": "get_graph_community_graph_communities__community_id__get",
        "parameters": [
          {
            "name": "community_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "title": "Community Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Community detail with members and cohesion",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GraphCommunityDetailResponse"
                }
              }
            }
          },
          "404": {
            "description": "Community id not found in the current snapshot"
          },
          "409": {
            "description": "Graph snapshot is cold \u2014 no recompute has run yet. Call POST /graph/recompute first."
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/graph/edges/{source_id}/{target_id}": {
      "get": {
        "summary": "4-signal edge weight breakdown between two pages (R9-5)",
        "description": "Returns the 4-signal edge weight breakdown for the undirected pair (source_id, target_id) (R9-5, AC-R9-5-4). Reads from the persisted edges table (signals JSONB column) \u2014 no graph recompute (I2). The pair is matched undirectionally (either endpoint order accepted). breakdown keys: direct_links (3*direct_count), shared_sources (4*shared_count), adamic_adar (1.5*AA), type_affinity (matrix value). Returns 404 if no edge exists for the pair in the current vault.",
        "operationId": "get_graph_edge_graph_edges__source_id___target_id__get",
        "parameters": [
          {
            "name": "source_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Source Id"
            }
          },
          {
            "name": "target_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Target Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Edge weight breakdown (4 signals)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GraphEdgeSignalsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No edge found for this pair in the persisted edges table"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/research/start": {
      "post": {
        "summary": "Start a bounded deep-research run",
        "description": "F10 Deep Research loop (ADR-0024 \u00a78.1, AC-F10-4a). Validates topic/vault_id; bounds max_iter (1..10) and token_budget (1k..1M) so the caller cannot request an unbounded run (I7). Freezes bounds onto the deep_research_runs row before the background task starts (AQ-v0.5-4). Schedules run_deep_research as a background asyncio task (fire-and-poll). Returns 202 {run_id} immediately \u2014 poll GET /research/runs/{id} for progress. 503 if SEARXNG_URL is unset (I9 \u2014 no fake run, no fallback engine). 429 if per-IP rate limit exceeded (R13-9).",
        "operationId": "research_start_research_start_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResearchStartRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Run accepted; poll GET /research/runs/{id} for progress",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchStartResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation error (empty topic, max_iter out of range, etc.)"
          },
          "429": {
            "description": "Per-IP rate limit exceeded (R13-9)"
          },
          "503": {
            "description": "SEARXNG_URL is not configured (I9)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/research/optimize-topic": {
      "post": {
        "summary": "Optimize a seed topic + propose search queries before a Deep Research run",
        "description": "B5/D3 pre-run optimization (llm_wiki parity). ONE bounded provider.chat() call (I6 provider-neutral, I7 single call + timeout + token budget + cost logged) that reads the vault overview.md + purpose.md and rephrases the seed `topic` into a domain-specific research topic plus 3-5 web-search-optimized queries. LLM-only \u2014 no web/SearXNG call here (I9 not engaged; the actual run happens later via POST /research/start with the edited topic/queries). GRACEFUL: when NO provider is configured (or the call times out / errors) it returns 200 with {optimized_topic: <topic>, queries: [<topic>]} so the confirm dialog still works offline \u2014 it does NOT 503/500. 429 if per-IP rate limit exceeded (R13-9).",
        "operationId": "research_optimize_topic_research_optimize_topic_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResearchOptimizeTopicRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Optimized topic + queries (or graceful echo fallback)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchOptimizeTopicResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation error (empty topic)"
          },
          "429": {
            "description": "Per-IP rate limit exceeded (R13-9)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/research/runs": {
      "get": {
        "summary": "List deep-research run history",
        "description": "Paginated, started_at DESC list of deep_research_runs rows (ADR-0024 \u00a78.2, AC-F10-4b). limit: 1..100 default 20; offset: >=0 default 0; vault_id: optional filter. Mirrors GET /ingest/runs contract.",
        "operationId": "list_research_runs_research_runs_get",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 100,
              "minimum": 1,
              "description": "Max rows (1..100)",
              "default": 20,
              "title": "Limit"
            },
            "description": "Max rows (1..100)"
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "description": "Row offset (>=0)",
              "default": 0,
              "title": "Offset"
            },
            "description": "Row offset (>=0)"
          },
          {
            "name": "vault_id",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Optional vault_id filter",
              "title": "Vault Id"
            },
            "description": "Optional vault_id filter"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated run list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchRunListResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation error (limit/offset out of range)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/research/runs/{run_id}": {
      "get": {
        "summary": "Get deep-research run detail + sources",
        "description": "Returns full run detail including queries_used, synthesis_text, and per-source summaries (ADR-0024 \u00a78.3, AC-F10-4c). synthesis_text is null until step 5 completes. sources array excludes fetched_content_md blobs (size guard). 404 if unknown run_id.",
        "operationId": "get_research_run_research_runs__run_id__get",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Run Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Run detail with sources",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchRunDetail"
                }
              }
            }
          },
          "404": {
            "description": "No run with this id"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/review/queue": {
      "get": {
        "summary": "List HITL review queue proposals",
        "description": "F9 HITL Review Queue (ADR-0034 \u00a77; ADR-0044 \u00a76 status filter + contextual projection). Returns paginated review_items for a vault, ordered created_at ASC. Each item is a PROPOSAL (missing-page|suggestion|contradiction|duplicate|confirm). status filter (ADR-0044 \u00a76): pending (default) | resolved | dismissed | all. limit: default 50, max 200 (I7 \u2014 bounded page size). offset: >=0. vault_id: required filter. page_title is a convenience join from pages.title for the page_id FK (UI display). referenced_pages joins referenced_page_ids to [{id,title,type}] (stale ids filtered).",
        "operationId": "list_review_queue_review_queue_get",
        "parameters": [
          {
            "name": "vault_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Vault scope (required)",
              "title": "Vault Id"
            },
            "description": "Vault scope (required)"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "description": "Status filter (ADR-0044 \u00a76): pending | resolved | dismissed | all",
              "default": "pending",
              "title": "Status"
            },
            "description": "Status filter (ADR-0044 \u00a76): pending | resolved | dismissed | all"
          },
          {
            "name": "item_type",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Optional proposal-category filter (e.g. suggestion)",
              "title": "Item Type"
            },
            "description": "Optional proposal-category filter (e.g. suggestion)"
          },
          {
            "name": "proposal_origin",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Optional rule|ai|corpus|system|lint|legacy filter",
              "title": "Proposal Origin"
            },
            "description": "Optional rule|ai|corpus|system|lint|legacy filter"
          },
          {
            "name": "proposed_page_type",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Optional proposed PageType filter (e.g. query)",
              "title": "Proposed Page Type"
            },
            "description": "Optional proposed PageType filter (e.g. query)"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 200,
              "minimum": 1,
              "description": "Max rows to return (1..200); I7 cap",
              "default": 50,
              "title": "Limit"
            },
            "description": "Max rows to return (1..200); I7 cap"
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "description": "Row offset for pagination",
              "default": 0,
              "title": "Offset"
            },
            "description": "Row offset for pagination"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated review proposals",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewQueueResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation error (limit out of range, missing vault_id)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/review/queue/{item_id}/approve": {
      "post": {
        "summary": "Create: lazy on-demand page generation from a proposal",
        "description": "F9 HITL Review Queue \u2014 Create action (ADR-0034 \u00a75; path kept for backward stability). Runs the bounded orchestrated loop targeting the proposed page, writes it through write_wiki_page (I1 \u2014 one data_version bump), sets status=created + created_page_id. 409 if item is not pending or no ingest provider is configured (I6 \u2014 never hardcode). 502 if generation fails; item is left pending \u2014 retry or skip. 404 if item_id is unknown. Prefer the /create alias (explicit verb) for new clients (ADR-0034 \u00a79 risk 6).",
        "operationId": "approve_review_item_review_queue__item_id__approve_post",
        "parameters": [
          {
            "name": "item_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Item Id"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/CreateReviewBody"
                  },
                  {
                    "type": "null"
                  }
                ],
                "title": "Body"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Page created; item status=created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewItemResponse"
                }
              }
            }
          },
          "404": {
            "description": "Review item not found"
          },
          "409": {
            "description": "Item not pending, or no ingest provider configured (I6)"
          },
          "502": {
            "description": "Generation failed; item left pending"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/review/queue/{item_id}/create": {
      "post": {
        "summary": "Create: lazy on-demand page generation from a proposal (explicit verb)",
        "description": "F9 HITL Review Queue \u2014 Create action (ADR-0034 \u00a75 \u2014 preferred explicit alias). Identical to POST /review/queue/{id}/approve. Runs the bounded orchestrated loop targeting the proposed page, writes it through write_wiki_page (I1 \u2014 one data_version bump), sets status=created + created_page_id. 409 if item is not pending or no ingest provider is configured (I6). 502 if generation fails; item is left pending. 404 if item_id is unknown.",
        "operationId": "create_review_item_review_queue__item_id__create_post",
        "parameters": [
          {
            "name": "item_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Item Id"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/CreateReviewBody"
                  },
                  {
                    "type": "null"
                  }
                ],
                "title": "Body"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Page created; item status=created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewItemResponse"
                }
              }
            }
          },
          "404": {
            "description": "Review item not found"
          },
          "409": {
            "description": "Item not pending, or no ingest provider configured (I6)"
          },
          "502": {
            "description": "Generation failed; item left pending"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/review/queue/{item_id}/skip": {
      "post": {
        "summary": "Skip a review proposal",
        "description": "F9 HITL Review Queue \u2014 skip action (ADR-0034 \u00a77). Sets status=skipped, resolution=skipped, reviewed_at=now(). 404 if item_id is unknown.",
        "operationId": "skip_review_item_review_queue__item_id__skip_post",
        "parameters": [
          {
            "name": "item_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Item Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Item skipped",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewItemResponse"
                }
              }
            }
          },
          "404": {
            "description": "Review item not found"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/review/queue/{item_id}/dismiss": {
      "post": {
        "summary": "Dismiss a review proposal",
        "description": "F9 HITL Review Queue \u2014 dismiss action (ADR-0044 \u00a76). Sets status=dismissed, resolution=dismissed, reviewed_at=now(). Terminal. Distinct from skip: 'hide this, I'm not acting' vs skip's 'considered and declined'. 404 if item_id is unknown.",
        "operationId": "dismiss_review_item_review_queue__item_id__dismiss_post",
        "parameters": [
          {
            "name": "item_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Item Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Item dismissed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewItemResponse"
                }
              }
            }
          },
          "404": {
            "description": "Review item not found"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/review/queue/{item_id}/deep-research": {
      "post": {
        "summary": "Trigger deep research for a review proposal",
        "description": "F9 HITL Review Queue \u2014 deep-research action (ADR-0034 \u00a77, AC-F9-3, AC-F10-5). Sets status=deep_researched, resolution=researched; delegates to F10 with the item's proposed_title \u2192 rationale (first line) \u2192 page.title as the research topic. (pre_generated_query is DROPPED in ADR-0034; topic derivation updated.) Stores the returned run_id in review_items.deep_research_run_id (AC-F10-5). Returns 202 {review_item_id, run_id} immediately (fire-and-poll). 503 if SEARXNG_URL is unset (inherits F10's guard, I9). 404 if item_id is unknown.",
        "operationId": "deep_research_review_item_review_queue__item_id__deep_research_post",
        "parameters": [
          {
            "name": "item_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Item Id"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Deep research started; poll GET /research/runs/{run_id} for progress",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewDeepResearchResponse"
                }
              }
            }
          },
          "404": {
            "description": "Review item not found"
          },
          "503": {
            "description": "SEARXNG_URL is not configured (I9)"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/review/queue/sweep": {
      "post": {
        "summary": "Manual auto-resolution sweep of pending review proposals",
        "description": "F9 HITL Review Queue \u2014 manual sweep trigger (ADR-0034 \u00a76). Runs Pass-1 (rule-based title-match for missing-page/duplicate) and Pass-2 (conservative bounded LLM judgment). Bounded; idempotent; never fails (returns partial results on error). vault_id: required. Auto-triggered after each orchestrated ingest run and after a successful Create. confirm items are NEVER auto-resolved (Do-NOT #7, ADR-0034 \u00a710).",
        "operationId": "sweep_review_queue_review_queue_sweep_post",
        "parameters": [
          {
            "name": "vault_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Vault scope (required)",
              "title": "Vault Id"
            },
            "description": "Vault scope (required)"
          }
        ],
        "responses": {
          "200": {
            "description": "Sweep complete; counts of resolved and kept items",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewSweepResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation error (missing vault_id)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/review/queue/bulk": {
      "post": {
        "summary": "Bulk status action on review proposals",
        "description": "F9 HITL Review Queue \u2014 bounded bulk status write (ADR-0044 \u00a76, I7). action: skip | dismiss | mark-resolved. Only PENDING ids (scoped to vault_id) are mutated; already-terminal ids are counted in skipped_terminal and NEVER re-mutated. mark-resolved NEVER auto-resolves a `confirm` item (Do-NOT #6/#10 \u2014 it is counted as skipped_terminal). No provider call. len(ids) is capped at REVIEW_BULK_MAX_IDS (400 over cap \u2014 I7).",
        "operationId": "bulk_review_queue_review_queue_bulk_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReviewBulkRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Bulk action applied; {updated, skipped_terminal}",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewBulkResponse"
                }
              }
            }
          },
          "400": {
            "description": "ids exceed REVIEW_BULK_MAX_IDS, or unknown action (I7)"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/review/queue/resolved": {
      "delete": {
        "summary": "Clear (hard-delete) terminal review proposals",
        "description": "F9 HITL Review Queue \u2014 'Clear resolved' (ADR-0044 \u00a76, I7). Hard-deletes terminal rows (skipped/dismissed/created/auto_resolved/deep_researched) for the vault in ONE bounded vault-scoped statement. PENDING rows are NEVER touched. Idempotent. These rows are advisory metadata (not vault content); created_page_id points at a page that persists independently (ADR-0044 \u00a79.5). vault_id: required.",
        "operationId": "clear_resolved_review_queue_review_queue_resolved_delete",
        "parameters": [
          {
            "name": "vault_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Vault scope (required)",
              "title": "Vault Id"
            },
            "description": "Vault scope (required)"
          }
        ],
        "responses": {
          "200": {
            "description": "Terminal rows deleted; {deleted}",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewClearResolvedResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation error (missing vault_id)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/review/queue/bulk-resolve": {
      "post": {
        "summary": "Bulk-resolve review items by id list (B5/D2 \u2014 llm_wiki parity)",
        "description": "F9 HITL Review Queue \u2014 per-item bulk resolve (B5/D2). Distinct from POST /review/queue/bulk (vault-scoped action-typed bulk): this endpoint accepts a flat id list and applies the action (skip|dismiss) to each via the EXACT per-item ops.review function (same seam as the REST /{id}/skip and /{id}/dismiss endpoints \u2014 I9, no second writer). len(ids) capped at 200 (I7 \u2014 422 beyond). action must be skip or dismiss (exact tokens from ops/review.py \u2014 422 on unknown). Already-terminal or not-found ids are counted and silently skipped.",
        "operationId": "bulk_resolve_review_queue_review_queue_bulk_resolve_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkResolveRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Bulk resolve complete; {resolved, not_found, count}",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkResolveResponse"
                }
              }
            }
          },
          "422": {
            "description": "ids exceed 200 cap, or unknown action (I7)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/review/queue/{item_id}": {
      "patch": {
        "summary": "Resolve or reopen a single review item (B5/D2 \u2014 llm_wiki parity)",
        "description": "F9 HITL Review Queue \u2014 single-item PATCH (B5/D2 llm_wiki parity). resolved=true (default): apply action (skip|dismiss) via the per-item ops.review seam. resolved=false: reopen the item to pending (clears status, resolution, reviewed_at). action must be skip|dismiss (422 on unknown); ignored when resolved=false. Routes through the exact ops.review._set_status primitive \u2014 no second writer (I9). 404 if item_id is unknown.",
        "operationId": "patch_review_item_review_queue__item_id__patch",
        "parameters": [
          {
            "name": "item_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Item Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchReviewRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Item patched; returns updated ReviewItemResponse",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewItemResponse"
                }
              }
            }
          },
          "404": {
            "description": "Review item not found"
          },
          "422": {
            "description": "Unknown action"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/lint/scan": {
      "post": {
        "summary": "Run a bounded lint scan (K2 \u2014 produces findings, never auto-fixes)",
        "description": "K2 Lint-fix loop (ADR-0037). Runs a BOUNDED, HUMAN-GATED health check of the wiki: deterministic structural checks (orphan-page via the graph/links read, no LLM) plus a bounded semantic pass (missing-xref | contradiction | stale-claim | missing-page) that rides the resolved ingest provider (I6 \u2014 never hardcoded). Bounds: max_iter (1..10) + token_budget (1k..1M) FROZEN on the lint_runs row (I7); findings capped at LINT_MAX_FINDINGS; total_cost_usd logged. Produces FINDINGS only \u2014 applying a fix requires the explicit human gate (POST /lint/findings/{id}/apply). Returns the run row + its findings.",
        "operationId": "lint_scan_lint_scan_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LintScanRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Scan complete; run + findings returned",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LintScanResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation error (max_iter/token_budget out of range)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/lint/runs": {
      "get": {
        "summary": "List lint scan run history",
        "description": "Paginated, created_at DESC list of lint_runs rows (ADR-0037 \u00a76). limit: 1..100 default 20; offset: >=0 default 0; vault_id: optional filter. Mirrors GET /research/runs.",
        "operationId": "list_lint_runs_endpoint_lint_runs_get",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 100,
              "minimum": 1,
              "description": "Max rows (1..100)",
              "default": 20,
              "title": "Limit"
            },
            "description": "Max rows (1..100)"
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "description": "Row offset (>=0)",
              "default": 0,
              "title": "Offset"
            },
            "description": "Row offset (>=0)"
          },
          {
            "name": "vault_id",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Optional vault_id filter",
              "title": "Vault Id"
            },
            "description": "Optional vault_id filter"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated lint run list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LintRunListResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation error (limit/offset out of range)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/lint/runs/{run_id}": {
      "get": {
        "summary": "Get a lint scan run by id",
        "description": "Returns one lint_runs row (ADR-0037 \u00a76). 404 if unknown run_id.",
        "operationId": "get_lint_run_lint_runs__run_id__get",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Run Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lint run detail",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LintRunResponse"
                }
              }
            }
          },
          "404": {
            "description": "No lint run with this id"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/lint/findings": {
      "get": {
        "summary": "List lint findings",
        "description": "Paginated, created_at ASC list of lint_findings rows (ADR-0037 \u00a76). vault_id: required. status: optional filter (open|applied|dismissed; default open). category: optional filter (orphan-page|broken-wikilink|missing-xref|contradiction|stale-claim|missing-page|no-outlinks|suggestion; 422 on invalid). severity: optional filter (info|warning|error; 422 on invalid). L10 / ADR-0037 B1. limit: default 50, max 200 (I7 \u2014 bounded page size). offset: >=0.",
        "operationId": "list_lint_findings_endpoint_lint_findings_get",
        "parameters": [
          {
            "name": "vault_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Vault scope (required)",
              "title": "Vault Id"
            },
            "description": "Vault scope (required)"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "open | applied | dismissed; null/omit for all statuses",
              "default": "open",
              "title": "Status"
            },
            "description": "open | applied | dismissed; null/omit for all statuses"
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Filter by category: orphan-page | broken-wikilink | missing-xref | contradiction | stale-claim | missing-page | no-outlinks | suggestion. 422 on invalid value. L10.",
              "title": "Category"
            },
            "description": "Filter by category: orphan-page | broken-wikilink | missing-xref | contradiction | stale-claim | missing-page | no-outlinks | suggestion. 422 on invalid value. L10."
          },
          {
            "name": "severity",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Filter by severity: info | warning | error. 422 on invalid value. L10.",
              "title": "Severity"
            },
            "description": "Filter by severity: info | warning | error. 422 on invalid value. L10."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 200,
              "minimum": 1,
              "description": "Max rows (1..200); I7 cap",
              "default": 50,
              "title": "Limit"
            },
            "description": "Max rows (1..200); I7 cap"
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "description": "Row offset for pagination",
              "default": 0,
              "title": "Offset"
            },
            "description": "Row offset for pagination"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated lint findings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LintFindingListResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation error (limit out of range, invalid category/severity)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/lint/findings/{finding_id}/apply": {
      "post": {
        "summary": "Apply a lint fix (HUMAN GATE)",
        "description": "K2 Lint-fix loop \u2014 human-gated apply (ADR-0037 \u00a75). Applies ONLY safe/bounded fixes; bumps data_version at most ONCE per applied fix (I1); NEVER full-rescans. missing-xref \u2192 reuses the wikilink-enrichment seam (adds the [[link]] into the page body, I5). missing-page \u2192 delegates to the lazy-generation seam (bounded orchestrated loop, one data_version bump). orphan-page / contradiction / stale-claim are FLAG-ONLY: apply records acknowledgement (status=applied) but performs no automatic edit. 409 if the finding is not open or no ingest provider is configured (I6). 502 if a bounded fix fails; finding left open. 404 if finding_id is unknown.",
        "operationId": "apply_lint_finding_lint_findings__finding_id__apply_post",
        "parameters": [
          {
            "name": "finding_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Finding Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Fix applied (or finding acknowledged for flag-only categories)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LintFindingResponse"
                }
              }
            }
          },
          "404": {
            "description": "Lint finding not found"
          },
          "409": {
            "description": "Finding not open, or no ingest provider configured (I6)"
          },
          "502": {
            "description": "Bounded fix failed; finding left open"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/lint/findings/{finding_id}/dismiss": {
      "post": {
        "summary": "Dismiss a lint finding",
        "description": "K2 Lint-fix loop \u2014 dismiss action (ADR-0037 \u00a75). Sets status=dismissed, reviewed_at=now(). No edit, no data_version bump. 404 if finding_id is unknown.",
        "operationId": "dismiss_lint_finding_endpoint_lint_findings__finding_id__dismiss_post",
        "parameters": [
          {
            "name": "finding_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Finding Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Finding dismissed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LintFindingResponse"
                }
              }
            }
          },
          "404": {
            "description": "Lint finding not found"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/lint/findings/{finding_id}/send-to-review": {
      "post": {
        "summary": "Bridge a lint finding to the F9 HITL review queue",
        "description": "L6 / ADR-0037 B1. Maps finding category \u2192 review item_type and calls ops/review.enqueue_review(). Category mapping: broken-wikilink \u2192 missing-page; missing-page \u2192 missing-page; contradiction \u2192 contradiction; stale-claim \u2192 suggestion; orphan-page \u2192 suggestion; missing-xref \u2192 suggestion. proposed_title = suggested_target when present (broken-wikilink), else target_title. On success: finding status \u2192 'applied', resolution_note = 'sent to review (<id>)'. 409 if the finding is not open. 404 if finding_id is unknown.",
        "operationId": "send_to_review_endpoint_lint_findings__finding_id__send_to_review_post",
        "parameters": [
          {
            "name": "finding_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Finding Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Finding sent to review; status \u2192 applied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LintFindingResponse"
                }
              }
            }
          },
          "404": {
            "description": "Lint finding not found"
          },
          "409": {
            "description": "Finding not open"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/lint/findings/batch": {
      "post": {
        "summary": "Batch apply|dismiss|send-to-review for multiple lint findings",
        "description": "L5 / ADR-0037 B1. Apply a single action to multiple findings sequentially. Actions: apply | dismiss | send-to-review. Cap: ids \u2264 200 (I7 \u2014 422 beyond). Per-id failure does NOT abort the batch \u2014 all ids are attempted. Response: {results: [{id, status: ok|error, detail}], ok_count, error_count}.",
        "operationId": "batch_findings_endpoint_lint_findings_batch_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchFindingsRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Batch processed; see results for per-id status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchFindingsResponse"
                }
              }
            }
          },
          "422": {
            "description": "ids exceeds cap of 200, or invalid action"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/clip": {
      "post": {
        "summary": "Chrome MV3 web clipper ingress \u2014 secure clip receiver (F11, ADR-0038)",
        "description": "F11 Web Clipper ingress (ADR-0038). Accepts already-converted Markdown from the Chrome MV3 extension, writes it atomically to vault/raw/sources/, then the EXISTING watcher ingests it asynchronously (I1/I5/K1 \u2014 no new ingest path). \n\nSecurity (ADR-0038 \u00a72 \u2014 explicitly addresses llm_wiki audit S-1..S-6): (a) CLIP_ENABLED must be true, else 503; (b) CLIP_TOKEN bearer required \u2014 constant-time compare, reject 401 on missing/invalid; (c) Origin/Host allowlist checked server-side BEFORE processing (chrome-extension://<id> + loopback + CLIP_ALLOWED_ORIGINS), reject 403 \u2014 CORS alone does not block simple POST drive-by writes; (d) body capped at CLIP_MAX_BODY_BYTES \u2192 413; (e) filename derived from title, sanitized, safe-joined under vault/raw/sources/, containment-verified \u2014 caller never supplies a base path \u2192 400 on traversal; (f) atomic write via temp+replace (I5). \n\nIdempotency (I1): watcher's mtime/SHA gate deduplicates re-clips of unchanged content. No second HTTP server. No 0.0.0.0 bind. NEVER stores or logs the token.",
        "operationId": "clip_ingest_clip_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClipRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "File saved; watcher ingests asynchronously",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClipResponse"
                }
              }
            }
          },
          "400": {
            "description": "Path traversal rejected or unsafe filename"
          },
          "401": {
            "description": "Missing or invalid CLIP_TOKEN"
          },
          "403": {
            "description": "Origin not in allowlist"
          },
          "413": {
            "description": "Body exceeds CLIP_MAX_BODY_BYTES"
          },
          "503": {
            "description": "CLIP_ENABLED is false \u2014 clipper ingress is disabled"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/scenarios": {
      "get": {
        "summary": "List available vault scenario templates",
        "description": "Return the 5 built-in vault preset templates (R7-1, AC-R7-1-1, [F1, K1]). Each preset provides a purpose.md body and schema.md stub appropriate to the domain. Apply a preset via POST /scenarios/{id}/apply.",
        "operationId": "list_scenarios_scenarios_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScenarioListResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/scenarios/{scenario_id}/apply": {
      "post": {
        "summary": "Apply a vault scenario template",
        "description": "Write vault/purpose.md and vault/schema.md for the chosen preset (R7-1, AC-R7-1-2, [F1, K1]). This is an explicit user action \u2014 both files are OVERWRITTEN with preset content. Bumps data_version. 404 for unknown scenario_id.",
        "operationId": "apply_scenario_scenarios__scenario_id__apply_post",
        "parameters": [
          {
            "name": "scenario_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Scenario Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Preset applied; purpose.md and schema.md written",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScenarioApplyResponse"
                }
              }
            }
          },
          "404": {
            "description": "Unknown scenario_id"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/vault/meta": {
      "get": {
        "summary": "Read vault meta files (schema.md, purpose.md)",
        "description": "Returns the two fixed vault-root meta files (schema.md, purpose.md) read directly from disk. Files are omitted from the array if absent. No Postgres write, no Qdrant, no ingest pipeline \u2014 read-only disk access only. [WS-D8, K1, I1, I5]",
        "operationId": "get_vault_meta_vault_meta_get",
        "parameters": [
          {
            "name": "vault_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "description": "Vault identifier (currently unused).",
              "default": "default",
              "title": "Vault Id"
            },
            "description": "Vault identifier (currently unused)."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VaultMetaResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/vault/meta/output-language": {
      "get": {
        "summary": "Read the AI output language for the active vault",
        "description": "Returns the ISO-639-1 output language stored in vault_state for the active vault (WS-E, ADR-0081). `null` means the vault predates 1.7.0 (auto-detect from source). 404 if vault_state has no row yet (vault not yet fully seeded).",
        "operationId": "get_output_language_vault_meta_output_language_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OutputLanguageResponse"
                }
              }
            }
          },
          "404": {
            "description": "vault_state row not found for the active vault."
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "put": {
        "summary": "Update the AI output language for the active vault",
        "description": "Persists the ISO-639-1 output language to vault_state for the active vault (WS-E, ADR-0081). Set to `null` to revert to auto-detect behaviour. Idempotent \u2014 safe to call multiple times. 404 if vault_state has no row yet (activate the vault first).",
        "operationId": "put_output_language_vault_meta_output_language_put",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OutputLanguageRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Language updated; returns the new persisted value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OutputLanguageResponse"
                }
              }
            }
          },
          "404": {
            "description": "vault_state row not found for the active vault."
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/vault/meta/{name}": {
      "put": {
        "summary": "Write a vault meta file (schema.md or purpose.md)",
        "description": "Overwrites one vault-root meta file with the supplied content. `name` MUST be exactly `schema.md` or `purpose.md` \u2014 any other value is 404 (this allow-list is the only path-traversal guard; no globs, no relative segments ever reach disk). Writes UTF-8 directly to the vault root; no Postgres/Qdrant/ingest side effects \u2014 these files are vault-meta, not wiki content (K1, I1, I5). v1.5 P1: makes purpose/schema editable in-app (ADR-0066).",
        "operationId": "put_vault_meta_vault_meta__name__put",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Name"
            }
          },
          {
            "name": "vault_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "description": "Vault identifier (currently unused).",
              "default": "default",
              "title": "Vault Id"
            },
            "description": "Vault identifier (currently unused)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VaultMetaWriteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "File written; returns the persisted entry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VaultMetaFile"
                }
              }
            }
          },
          "404": {
            "description": "name is not schema.md or purpose.md."
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/projects": {
      "get": {
        "tags": [
          "projects"
        ],
        "summary": "List known projects (vaults) and the active one",
        "description": "Returns the multi-vault project registry (ADR-0082): all known project vaults + which is active. Seeded from the boot vault when the registry does not exist yet, so single-vault deploys always see exactly their one vault. Read-only; no DB, no Qdrant.",
        "operationId": "list_projects_projects_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectsResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "projects"
        ],
        "summary": "Create + scaffold a new project vault",
        "description": "Creates a new vault at *path* (scaffolds raw/, wiki/, purpose.md, schema.md via the shared bootstrap \u2014 idempotent) and registers it (ADR-0082). Does NOT switch the active vault. Path must be absolute; 409 if a project already exists at that path.",
        "operationId": "create_project_projects_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateProjectRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "400": {
            "description": "Name missing or path not absolute."
          },
          "409": {
            "description": "A project already exists at this path."
          },
          "500": {
            "description": "Could not create the vault on disk."
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/projects/open": {
      "post": {
        "tags": [
          "projects"
        ],
        "summary": "Register an existing vault folder as a project",
        "description": "Adds an existing vault directory to the registry (ADR-0082). Does NOT switch the active vault (that is POST /projects/{id}/activate). Idempotent: opening an already-registered path returns the existing entry (touching last_opened_at). Path must be absolute and an existing directory. Server-side path (self-hosted).",
        "operationId": "open_project_projects_open_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OpenProjectRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "400": {
            "description": "Path is not absolute."
          },
          "404": {
            "description": "Path is not an existing directory."
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/projects/{project_id}/activate": {
      "post": {
        "tags": [
          "projects"
        ],
        "summary": "Switch the active vault to this project",
        "description": "Makes *project_id* the active vault at runtime (ADR-0082 \u00a72c): records it in the registry, re-points settings, restarts the watcher on the new root, invalidates the graph cache, seeds vault_state, and bumps active_vault_epoch (the frontend reloads its stores on epoch change). Runtime side effects are best-effort + logged. 404 if unknown.",
        "operationId": "activate_project_projects__project_id__activate_post",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Project Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActivateResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such project id."
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/events": {
      "get": {
        "summary": "Server-Sent Events push channel \u2014 data_version + ingest queue state",
        "description": "SSE stream (text/event-stream) pushing `data_version` bumps and ingest-queue counter changes as they happen (1.9.3 W1, FE-RT-2). Change-driven, not a continuous blast; periodic comment heartbeats keep the connection alive through proxies/tunnels. Supports `Last-Event-ID` for resume-on-reconnect (the first tick after connect always resends current state). Bounded to EVENTS_MAX_STREAM_SECONDS per connection (I7); EventSource reconnects automatically. Existing REST pollers (/status, /ingest/queue, etc.) remain the permanent fallback and are never disabled by this endpoint.",
        "operationId": "get_events_events_get",
        "responses": {
          "200": {
            "description": "SSE stream",
            "content": {
              "application/json": {
                "schema": {}
              },
              "text/event-stream": {}
            }
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "ActivateResponse": {
        "properties": {
          "project": {
            "$ref": "#/components/schemas/Project"
          },
          "active_vault_epoch": {
            "type": "integer",
            "title": "Active Vault Epoch"
          }
        },
        "type": "object",
        "required": [
          "project",
          "active_vault_epoch"
        ],
        "title": "ActivateResponse",
        "description": "Response for POST /projects/{id}/activate \u2014 the now-active project + reload epoch."
      },
      "ApiTokenCreateRequest": {
        "properties": {
          "label": {
            "type": "string",
            "maxLength": 200,
            "minLength": 1,
            "title": "Label",
            "description": "Human-readable description of what this token is used for."
          },
          "vault_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Vault Id",
            "description": "NULL (default) = global token, valid for any vault this backend serves. Non-NULL = the token is only accepted when it equals this backend instance's settings.vault_id at request time; a mismatch is rejected as an invalid token."
          },
          "read_only": {
            "type": "boolean",
            "title": "Read Only",
            "description": "True = the token may only be used for GET/HEAD/OPTIONS requests; any other HTTP method is rejected with 403.",
            "default": false
          }
        },
        "type": "object",
        "required": [
          "label"
        ],
        "title": "ApiTokenCreateRequest",
        "description": "Request body for POST /config/api-tokens (PF-AUTH-1)."
      },
      "ApiTokenCreateResponse": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id",
            "description": "Row id \u2014 pass this to DELETE /config/api-tokens/{id}."
          },
          "label": {
            "type": "string",
            "title": "Label"
          },
          "vault_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Vault Id"
          },
          "read_only": {
            "type": "boolean",
            "title": "Read Only"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "token": {
            "type": "string",
            "title": "Token",
            "description": "The plaintext bearer secret. Shown ONE TIME ONLY \u2014 copy it now. NEVER returned again by any endpoint, NEVER logged."
          }
        },
        "type": "object",
        "required": [
          "id",
          "label",
          "vault_id",
          "read_only",
          "created_at",
          "token"
        ],
        "title": "ApiTokenCreateResponse",
        "description": "Response body for POST /config/api-tokens (PF-AUTH-1).\n\n``token`` is the PLAINTEXT secret \u2014 shown exactly once, here, and never again. The\ncaller MUST copy it now; only its PBKDF2 hash is persisted (app.models.ApiToken)."
      },
      "ApiTokenListItem": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "label": {
            "type": "string",
            "title": "Label"
          },
          "vault_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Vault Id"
          },
          "read_only": {
            "type": "boolean",
            "title": "Read Only"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "last_used_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Used At"
          }
        },
        "type": "object",
        "required": [
          "id",
          "label",
          "vault_id",
          "read_only",
          "created_at",
          "last_used_at"
        ],
        "title": "ApiTokenListItem",
        "description": "One row in GET /config/api-tokens (PF-AUTH-1). NEVER includes the secret/hash."
      },
      "ApiTokenListResponse": {
        "properties": {
          "tokens": {
            "items": {
              "$ref": "#/components/schemas/ApiTokenListItem"
            },
            "type": "array",
            "title": "Tokens"
          }
        },
        "type": "object",
        "required": [
          "tokens"
        ],
        "title": "ApiTokenListResponse",
        "description": "Response body for GET /config/api-tokens (PF-AUTH-1)."
      },
      "AppConfigListResponse": {
        "properties": {
          "settings": {
            "items": {
              "$ref": "#/components/schemas/AppConfigSetting"
            },
            "type": "array",
            "title": "Settings",
            "description": "All 12 migrated settings in stable S1..S12 order."
          }
        },
        "type": "object",
        "required": [
          "settings"
        ],
        "title": "AppConfigListResponse",
        "description": "Response body for GET /config/app (ADR-0053 \u00a73.1)."
      },
      "AppConfigPutBody": {
        "properties": {
          "value": {
            "type": "string",
            "title": "Value",
            "description": "Override value as a string. Required and non-null (app_config.value is NOT NULL \u2014 use DELETE to reset to default, \u00a73.3). Per-key validation rules: ADR-0053 \u00a72.3."
          }
        },
        "type": "object",
        "required": [
          "value"
        ],
        "title": "AppConfigPutBody",
        "description": "Request body for PUT /config/app/{key} (ADR-0053 \u00a73.2)."
      },
      "AppConfigSetting": {
        "properties": {
          "key": {
            "type": "string",
            "title": "Key",
            "description": "Config key (lower-snake attribute form, e.g. pdf_extractor)."
          },
          "value": {
            "type": "string",
            "title": "Value",
            "description": "Effective value as a string (override wins; env baseline otherwise). S7 overview_language with no override serialises as '' (auto sentinel)."
          },
          "source": {
            "type": "string",
            "enum": [
              "override",
              "env"
            ],
            "title": "Source",
            "description": "\"override\" iff an app_config row exists for this key, else \"env\"."
          }
        },
        "type": "object",
        "required": [
          "key",
          "value",
          "source"
        ],
        "title": "AppConfigSetting",
        "description": "One entry in the GET /config/app response (ADR-0053 \u00a73.1)."
      },
      "BackfillDomainsRequest": {
        "properties": {
          "max_pages": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Max Pages",
            "description": "Cap on pages processed this run (clamped server-side)."
          },
          "token_budget": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Token Budget",
            "description": "Token budget for the run (clamped server-side, I7)."
          },
          "force": {
            "type": "boolean",
            "title": "Force",
            "description": "Re-classify pages that already carry a domain/ tag (default: skip them).",
            "default": false
          }
        },
        "type": "object",
        "title": "BackfillDomainsRequest",
        "description": "Request body for POST /ops/backfill-domains (ADR-0054 \u00a76)."
      },
      "BackfillDomainsStartResponse": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status",
            "description": "'started' \u2014 backfill runs in background",
            "default": "started"
          },
          "max_pages": {
            "type": "integer",
            "title": "Max Pages",
            "description": "Effective (clamped) page cap for this run"
          },
          "token_budget": {
            "type": "integer",
            "title": "Token Budget",
            "description": "Effective (clamped) token budget for this run"
          },
          "force": {
            "type": "boolean",
            "title": "Force",
            "description": "Whether already-tagged pages are re-classified"
          }
        },
        "type": "object",
        "required": [
          "max_pages",
          "token_budget",
          "force"
        ],
        "title": "BackfillDomainsStartResponse",
        "description": "202 response for POST /ops/backfill-domains."
      },
      "BackfillDomainsStatusResponse": {
        "properties": {
          "running": {
            "type": "boolean",
            "title": "Running",
            "description": "True while a backfill run is in flight"
          },
          "last_summary": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Summary",
            "description": "Summary of the most recent completed run (null if never ran)"
          }
        },
        "type": "object",
        "required": [
          "running"
        ],
        "title": "BackfillDomainsStatusResponse",
        "description": "GET /ops/backfill-domains \u2014 single-flight state + last completed summary."
      },
      "BackfillRelatedRequest": {
        "properties": {
          "max_pages": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Max Pages",
            "description": "Cap on wiki pages scanned per run (clamped server-side, I7)."
          },
          "apply": {
            "type": "boolean",
            "title": "Apply",
            "description": "False (default) = dry-run only \u2014 returns planned counts + samples with no file writes. True = perform actual writes + incremental re-index + data_version bump.",
            "default": false
          }
        },
        "type": "object",
        "title": "BackfillRelatedRequest",
        "description": "Request body for POST /ops/backfill-related (ADR-0067 D2 P2-1+P2-2)."
      },
      "BackfillRelatedStartResponse": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status",
            "description": "'started' \u2014 backfill runs in background",
            "default": "started"
          },
          "max_pages": {
            "type": "integer",
            "title": "Max Pages",
            "description": "Effective (clamped) page cap for this run"
          },
          "apply": {
            "type": "boolean",
            "title": "Apply",
            "description": "Whether file writes are performed (False = dry-run)"
          }
        },
        "type": "object",
        "required": [
          "max_pages",
          "apply"
        ],
        "title": "BackfillRelatedStartResponse",
        "description": "202 response for POST /ops/backfill-related."
      },
      "BackfillRelatedStatusResponse": {
        "properties": {
          "running": {
            "type": "boolean",
            "title": "Running",
            "description": "True while a backfill-related run is in flight"
          },
          "last_summary": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Summary",
            "description": "Summary of the most recent completed run (null if never ran). Includes 'samples' (first 5 changed pages) in dry-run mode."
          }
        },
        "type": "object",
        "required": [
          "running"
        ],
        "title": "BackfillRelatedStatusResponse",
        "description": "GET /ops/backfill-related \u2014 single-flight state + last completed summary."
      },
      "BatchFindingsRequest": {
        "properties": {
          "ids": {
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "type": "array",
            "title": "Ids",
            "description": "List of finding UUIDs to act on. Cap: <= 200 (I7 \u2014 bounded; 422 beyond)."
          },
          "action": {
            "type": "string",
            "title": "Action",
            "description": "Action to apply: apply | dismiss | send-to-review"
          }
        },
        "type": "object",
        "required": [
          "ids",
          "action"
        ],
        "title": "BatchFindingsRequest",
        "description": "Request body for POST /lint/findings/batch (L5 / ADR-0037 B1)."
      },
      "BatchFindingsResponse": {
        "properties": {
          "results": {
            "items": {
              "$ref": "#/components/schemas/BatchItemResult"
            },
            "type": "array",
            "title": "Results"
          },
          "ok_count": {
            "type": "integer",
            "title": "Ok Count"
          },
          "error_count": {
            "type": "integer",
            "title": "Error Count"
          }
        },
        "type": "object",
        "required": [
          "results",
          "ok_count",
          "error_count"
        ],
        "title": "BatchFindingsResponse",
        "description": "Response for POST /lint/findings/batch (L5 / ADR-0037 B1)."
      },
      "BatchItemResult": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "ok | error"
          },
          "detail": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Detail"
          }
        },
        "type": "object",
        "required": [
          "id",
          "status"
        ],
        "title": "BatchItemResult",
        "description": "Per-id result within POST /lint/findings/batch response (L5)."
      },
      "Body_convert_marker_ingest_convert_marker_post": {
        "properties": {
          "files": {
            "items": {
              "type": "string",
              "contentMediaType": "application/octet-stream"
            },
            "type": "array",
            "title": "Files",
            "description": "PDF files to convert (\u226410)."
          }
        },
        "type": "object",
        "required": [
          "files"
        ],
        "title": "Body_convert_marker_ingest_convert_marker_post"
      },
      "Body_upload_ingest_ingest_upload_post": {
        "properties": {
          "file": {
            "type": "string",
            "contentMediaType": "application/octet-stream",
            "title": "File",
            "description": "The document to upload"
          },
          "rel_dir": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Rel Dir",
            "description": "Optional relative subdirectory under vault/raw/sources/ for folder uploads (S1, B3b). Use forward slashes as separator (e.g. 'projects/notes'). Each segment is sanitized like safe_source_name. Path-traversal attempts ('..') \u2192 422. When absent, file is written flat under vault/raw/sources/ (unchanged behaviour)."
          }
        },
        "type": "object",
        "required": [
          "file"
        ],
        "title": "Body_upload_ingest_ingest_upload_post"
      },
      "BulkResolveRequest": {
        "properties": {
          "ids": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Ids",
            "description": "Review item id strings (UUID) to resolve; capped at 200 (I7 \u2014 422 beyond cap)."
          },
          "action": {
            "type": "string",
            "title": "Action",
            "description": "Resolution action: skip | dismiss (exact tokens from ops/review.py; 422 on unknown)."
          }
        },
        "type": "object",
        "required": [
          "ids",
          "action"
        ],
        "title": "BulkResolveRequest",
        "description": "Request body for POST /review/queue/bulk-resolve (B5/D2 \u2014 llm_wiki parity).",
        "example": {
          "action": "skip",
          "ids": [
            "00000000-0000-0000-0000-000000000001"
          ]
        }
      },
      "BulkResolveResponse": {
        "properties": {
          "resolved": {
            "type": "integer",
            "title": "Resolved",
            "description": "Items successfully resolved via the per-item action"
          },
          "not_found": {
            "type": "integer",
            "title": "Not Found",
            "description": "ids that had no matching review item"
          },
          "count": {
            "type": "integer",
            "title": "Count",
            "description": "Total ids supplied (= resolved + not_found + terminal_skipped)"
          }
        },
        "type": "object",
        "required": [
          "resolved",
          "not_found",
          "count"
        ],
        "title": "BulkResolveResponse",
        "description": "Response for POST /review/queue/bulk-resolve (B5/D2)."
      },
      "CascadeDeleteResponse": {
        "properties": {
          "deleted_page_id": {
            "type": "string",
            "format": "uuid",
            "title": "Deleted Page Id"
          },
          "wikilinks_cleaned": {
            "type": "integer",
            "title": "Wikilinks Cleaned"
          },
          "index_entry_removed": {
            "type": "boolean",
            "title": "Index Entry Removed"
          },
          "shared_entity_warnings": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Shared Entity Warnings"
          },
          "cleaned_references": {
            "type": "integer",
            "title": "Cleaned References",
            "description": "Number of referencing pages whose dead wikilinks were neutralised. Mirrors wikilinks_cleaned (L9 \u2014 same value). Returned as a convenience alias for the lint batch L9 consumer.",
            "default": 0
          }
        },
        "type": "object",
        "required": [
          "deleted_page_id",
          "wikilinks_cleaned",
          "index_entry_removed",
          "shared_entity_warnings"
        ],
        "title": "CascadeDeleteResponse",
        "description": "DELETE /pages/{id} response (ADR-0026 \u00a76.1, AC-F13-5 / L9).\n\ndeleted_page_id: the page that was deleted.\nwikilinks_cleaned: total [[Target]] spans neutralised.\nindex_entry_removed: True when index.md was successfully regenerated.\nshared_entity_warnings: advisory list of source-overlap pages.\ncleaned_references: alias for wikilinks_cleaned (L9 contract)."
      },
      "CascadePreviewResponse": {
        "properties": {
          "target_page_id": {
            "type": "string",
            "format": "uuid",
            "title": "Target Page Id"
          },
          "target_title": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Target Title"
          },
          "target_file_path": {
            "type": "string",
            "title": "Target File Path"
          },
          "will_delete": {
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "type": "array",
            "title": "Will Delete"
          },
          "will_preserve_with_pruned_source": {
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "type": "array",
            "title": "Will Preserve With Pruned Source"
          },
          "wikilinks_to_rewrite": {
            "items": {
              "additionalProperties": true,
              "type": "object"
            },
            "type": "array",
            "title": "Wikilinks To Rewrite"
          },
          "index_entry_will_be_removed": {
            "type": "boolean",
            "title": "Index Entry Will Be Removed"
          },
          "raw_source_to_delete": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Raw Source To Delete"
          },
          "shared_entity_warnings": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Shared Entity Warnings"
          },
          "match_methods_used": {
            "additionalProperties": {
              "type": "string"
            },
            "type": "object",
            "title": "Match Methods Used"
          }
        },
        "type": "object",
        "required": [
          "target_page_id",
          "target_file_path",
          "will_delete",
          "will_preserve_with_pruned_source",
          "wikilinks_to_rewrite",
          "index_entry_will_be_removed",
          "shared_entity_warnings",
          "match_methods_used"
        ],
        "title": "CascadePreviewResponse",
        "description": "POST /pages/{id}/cascade-delete/preview response (ADR-0026 \u00a76.1, DRY-RUN).\n\nRead-only: mutates nothing \u2014 no soft-delete, no Qdrant delete, no file write,\nno data_version bump.  Returns the full CascadePlan as JSON."
      },
      "ChatImageIn": {
        "properties": {
          "mime": {
            "type": "string",
            "minLength": 1,
            "title": "Mime",
            "description": "IANA media type, e.g. 'image/png'"
          },
          "data_base64": {
            "type": "string",
            "minLength": 1,
            "title": "Data Base64",
            "description": "base64 image payload WITHOUT a data-URI prefix"
          }
        },
        "type": "object",
        "required": [
          "mime",
          "data_base64"
        ],
        "title": "ChatImageIn",
        "description": "One image attachment for a chat message (B2-C1, F17/I6 vision surface).\n\n`mime` is the IANA media type (e.g. ``\"image/png\"``). `data_base64` is the\nbase64-encoded payload WITHOUT a ``data:...;base64,`` URI prefix.\nSize must be capped by the client before sending; the backend passes this through\ninto the provider-neutral :class:`~app.ingest.schemas.MessageImage` DTO without\nre-validating size (the provider layer handles/ignores per its ``supports_vision``\ncapability \u2014 B2-C1 cross-layer contract)."
      },
      "ChatMessageIn": {
        "properties": {
          "role": {
            "type": "string",
            "title": "Role",
            "description": "user | assistant | system"
          },
          "content": {
            "type": "string",
            "minLength": 1,
            "title": "Content"
          },
          "images": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/ChatImageIn"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Images",
            "description": "B2-C1: optional image attachments for this message turn. Passed through to the provider's chat() only when the active provider advertises supports_vision=True. Non-vision providers silently drop these (defense-in-depth, I6)."
          }
        },
        "type": "object",
        "required": [
          "role",
          "content"
        ],
        "title": "ChatMessageIn",
        "description": "One turn in a ChatRequest. Mirrors the backend-neutral Message shape (I6)."
      },
      "ChatMessageListResponse": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/ChatMessageResponse"
            },
            "type": "array",
            "title": "Items"
          },
          "total": {
            "type": "integer",
            "title": "Total"
          }
        },
        "type": "object",
        "required": [
          "items",
          "total"
        ],
        "title": "ChatMessageListResponse"
      },
      "ChatMessageResponse": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "conversation_id": {
            "type": "string",
            "format": "uuid",
            "title": "Conversation Id"
          },
          "role": {
            "type": "string",
            "title": "Role"
          },
          "content": {
            "type": "string",
            "title": "Content"
          },
          "citations": {
            "anyOf": [
              {
                "items": {},
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Citations",
            "description": "[] in M4 (M5 reserved)"
          },
          "provider_type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Provider Type"
          },
          "model_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Model Id"
          },
          "input_tokens": {
            "type": "integer",
            "title": "Input Tokens"
          },
          "output_tokens": {
            "type": "integer",
            "title": "Output Tokens"
          },
          "total_cost_usd": {
            "type": "number",
            "title": "Total Cost Usd",
            "description": "0.0 for local/cli (I7); serialised as number"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          }
        },
        "type": "object",
        "required": [
          "id",
          "conversation_id",
          "role",
          "content",
          "provider_type",
          "model_id",
          "input_tokens",
          "output_tokens",
          "total_cost_usd",
          "created_at"
        ],
        "title": "ChatMessageResponse",
        "description": "API shape for one messages row (ADR-0019 \u00a72.5). `content` is RAW incl. literal\n<think>\u2026 (AC-F7-2); the client re-derives think-vs-content with the same split."
      },
      "ChatRequest": {
        "properties": {
          "conversation_id": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "title": "Conversation Id",
            "description": "null = start a new conversation (id returned in done event)"
          },
          "messages": {
            "items": {
              "$ref": "#/components/schemas/ChatMessageIn"
            },
            "type": "array",
            "minItems": 1,
            "title": "Messages"
          },
          "vault_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Vault Id",
            "description": "Defaults to settings.vault_id"
          },
          "context_window": {
            "anyOf": [
              {
                "type": "integer",
                "maximum": 1000000.0,
                "minimum": 4096.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Context Window",
            "description": "F14 window override (4096..1_000_000); null \u2192 provider/32K default"
          },
          "operation": {
            "type": "string",
            "const": "chat",
            "title": "Operation",
            "description": "Fixed to 'chat'",
            "default": "chat"
          },
          "regenerate": {
            "type": "boolean",
            "title": "Regenerate",
            "description": "AC-F6-4: delete the last assistant message before re-streaming",
            "default": false
          },
          "use_web_search": {
            "type": "boolean",
            "title": "Use Web Search",
            "description": "B2-C2: when True, make ONE bounded SearXNG search and append a [W]-namespaced web-context block to the retrieval context (I9 \u2014 SearXNG only, no loop). For local_first retrieval_mode, web search fires only when wiki hits < LOCAL_FIRST_MIN_HITS. web_citations in the done event.",
            "default": false
          },
          "retrieval_mode": {
            "type": "string",
            "enum": [
              "fast",
              "standard",
              "deep",
              "local_first"
            ],
            "title": "Retrieval Mode",
            "description": "B2-C3: frozen retrieval preset. fast=k4/depth0 \u00b7 standard=k8/depth2 (default) \u00b7 deep=k12/depth2 \u00b7 local_first=k8/depth2+web-gate. expansion_depth is always clamped to \u22642 (I7).",
            "default": "standard"
          },
          "use_skills": {
            "type": "boolean",
            "title": "Use Skills",
            "description": "F6/P4: forward-compatible flag \u2014 when True, the client requests skill execution for this chat turn. Accepted and logged; actual execution deferred to P5 Skills view. No behavior change in the current release.",
            "default": false
          },
          "use_anytxt": {
            "type": "boolean",
            "title": "Use Anytxt",
            "description": "F6/P4: forward-compatible flag \u2014 when True, the client requests AnyTXT Searcher local-file search for this chat turn. Accepted and logged; AnyTXT is a Windows-only local-search tool not applicable to the current Mac/TrueNAS/Docker stack. No behavior change in the current release.",
            "default": false
          }
        },
        "type": "object",
        "required": [
          "messages"
        ],
        "title": "ChatRequest",
        "description": "Request body for POST /chat/stream (ADR-0019 \u00a72.2, B2-C1/C2/C3).\n\nThe server NEVER accepts provider_type / model_id (I6 / Do-NOT #4): the backend resolves\n`resolve_provider_config(\"chat\", vault_id)`. `operation` is fixed to \"chat\" so the same\nabstraction can route ingest-vs-chat differently.\n\nB2 additions (additive, non-breaking for existing clients):\n  - ``use_web_search``: trigger a single-shot SearXNG fetch (C2, I9). [W] namespace.\n  - ``retrieval_mode``: one of four frozen presets controlling k + expansion_depth (C3, I7)."
      },
      "CliAuthConfigRequest": {
        "properties": {
          "token": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Token",
            "description": "The Claude subscription OAuth token to store (from `claude setup-token`). Encrypted at rest via Fernet (SYNAPSE_SECRET_KEY); plaintext held in-memory only for outbound CLI injection. NEVER logged or returned. Requires SYNAPSE_SECRET_KEY configured server-side (else HTTP 400 \u2014 W7). Validated: non-empty, 20\u2013500 chars; soft prefix check (warns, does not block)."
          },
          "clear": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Clear",
            "description": "true \u21d2 set cli_oauth_token_encrypted = NULL (fall back to env / none). Wins over token if both are sent."
          }
        },
        "type": "object",
        "title": "CliAuthConfigRequest",
        "description": "Request body for PUT /provider/cli-auth (ADR-0043 \u00a72.5).\n\nExactly one of {token, clear} should be present:\n  token: str  \u2014 paste the token produced by ``claude setup-token`` (prefix: sk-ant- + oat01-)\n  clear: bool \u2014 true \u21d2 set vault_state.cli_oauth_token = NULL (fall back to env / none)\n\n``clear`` wins if both are sent. An empty body (neither field) \u2192 400."
      },
      "CliAuthConfigResponse": {
        "properties": {
          "token_configured": {
            "type": "boolean",
            "title": "Token Configured",
            "description": "True iff any credential is available (DB cli_oauth_token set OR any env signal: ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN / CLAUDE_CODE_USE_SUBSCRIPTION). NEVER contains the token value."
          },
          "token_source": {
            "type": "string",
            "title": "Token Source",
            "description": "\"db\" | \"env\" | \"none\". \"db\": vault_state.cli_oauth_token is set (DB wins \u2014 ADR-0043 \u00a72.3 tier 1). \"env\": no DB token; at least one env signal is present (ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN / CLAUDE_CODE_USE_SUBSCRIPTION). \"none\": nothing configured."
          },
          "auth_mode": {
            "type": "string",
            "title": "Auth Mode",
            "description": "\"api-key\" | \"subscription\" | \"unconfigured\". Derived from ADR-0043 \u00a72.3 precedence (presence-only; does NOT run injection): \"api-key\": env ANTHROPIC_API_KEY non-empty AND no DB token. \"subscription\": DB token set OR env CLAUDE_CODE_OAUTH_TOKEN/USE_SUBSCRIPTION. \"unconfigured\": nothing set."
          }
        },
        "type": "object",
        "required": [
          "token_configured",
          "token_source",
          "auth_mode"
        ],
        "title": "CliAuthConfigResponse",
        "description": "Response model for GET/PUT /provider/cli-auth (ADR-0043 \u00a72.5).\n\nPosture only \u2014 NEVER returns the token value. Mirrors ClipConfigResponse but simpler:\nno enabled/allowed_origins, no generated_token, no rotate. The user pastes their own\ntoken; the server never generates one."
      },
      "ClipConfigRequest": {
        "properties": {
          "rotate_token": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Rotate Token",
            "description": "true \u21d2 generate a new high-entropy token (secrets.token_urlsafe(32)), store its PBKDF2-SHA256 hash in clip_access_token (never the raw value), return plaintext ONCE in generated_token. The plaintext is NEVER stored or returned again after this call."
          },
          "clear_token": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Clear Token",
            "description": "true \u21d2 set clip_access_token = NULL (DB token cleared; falls back to CLIP_TOKEN env bootstrap or none)."
          },
          "set_enabled": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Set Enabled",
            "description": "Set the clip_enabled_db flag. true \u21d2 DB overrides CLIP_ENABLED env with True. false \u21d2 DB overrides with False (ingress disabled regardless of env). Omit to leave unchanged."
          },
          "set_allowed_origins": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Set Allowed Origins",
            "description": "Replace the DB clip_allowed_origins_db value with this comma-separated string. Empty string \"\" clears the DB value (falls back to CLIP_ALLOWED_ORIGINS env). Omit to leave unchanged."
          }
        },
        "type": "object",
        "title": "ClipConfigRequest",
        "description": "Request body for PUT /clip/config (ADR-0040 \u00a72.4).\n\nAll fields are optional; omitting a field leaves that aspect unchanged.\nMirrors McpAuthRequest (ADR-0033 \u00a72.5)."
      },
      "ClipConfigResponse": {
        "properties": {
          "enabled": {
            "type": "boolean",
            "title": "Enabled",
            "description": "Resolved enabled state (DB clip_enabled_db if set, else CLIP_ENABLED env). True iff POST /clip will be accepted."
          },
          "token_configured": {
            "type": "boolean",
            "title": "Token Configured",
            "description": "True iff a token is available (DB clip_access_token PBKDF2 hash set OR CLIP_TOKEN env set). NEVER contains the token value."
          },
          "token_source": {
            "type": "string",
            "title": "Token Source",
            "description": "\"db\" | \"env\" | \"none\" \u2014 which token source is authoritative (ADR-0040 \u00a72.2). \"db\": token set via PUT /clip/config. \"env\": CLIP_TOKEN env bootstrap. \"none\": no token configured. NEVER the token value."
          },
          "allowed_origins": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Allowed Origins",
            "description": "Resolved allowed-origins list (DB if set, else CLIP_ALLOWED_ORIGINS env). Loopback origins are always implicitly allowed in addition to this list."
          },
          "max_body_bytes": {
            "type": "integer",
            "title": "Max Body Bytes",
            "description": "Maximum allowed body size for POST /clip in bytes (CLIP_MAX_BODY_BYTES env). Not runtime-settable via PUT /clip/config; change the env var."
          }
        },
        "type": "object",
        "required": [
          "enabled",
          "token_configured",
          "token_source",
          "allowed_origins",
          "max_body_bytes"
        ],
        "title": "ClipConfigResponse",
        "description": "Response model for GET /clip/config (ADR-0040 \u00a72.3).\n\nMirrors McpInfoResponse structure: posture-only, token value NEVER returned."
      },
      "ClipConfigStateResponse": {
        "properties": {
          "enabled": {
            "type": "boolean",
            "title": "Enabled",
            "description": "Resolved enabled state after this write."
          },
          "token_configured": {
            "type": "boolean",
            "title": "Token Configured",
            "description": "True iff a token is available after this write (DB or env)."
          },
          "token_source": {
            "type": "string",
            "title": "Token Source",
            "description": "\"db\" | \"env\" | \"none\" \u2014 authoritative token source after this write."
          },
          "allowed_origins": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Allowed Origins",
            "description": "Resolved allowed-origins list after this write."
          },
          "max_body_bytes": {
            "type": "integer",
            "title": "Max Body Bytes",
            "description": "CLIP_MAX_BODY_BYTES (env, not runtime-settable)."
          },
          "generated_token": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Generated Token",
            "description": "Populated ONLY when rotate_token=true \u2014 the plaintext token shown ONCE. null in all other cases. NEVER stored as recoverable. NEVER returned by subsequent GET or PUT."
          }
        },
        "type": "object",
        "required": [
          "enabled",
          "token_configured",
          "token_source",
          "allowed_origins",
          "max_body_bytes"
        ],
        "title": "ClipConfigStateResponse",
        "description": "Response body for PUT /clip/config (ADR-0040 \u00a72.4).\n\nAlways reflects post-write posture. generated_token is populated ONLY when\nrotate_token=true \u2014 shown ONCE, never returned again.\nNEVER contains the token value (except the one-time generated_token on rotate)."
      },
      "ClipRequest": {
        "properties": {
          "url": {
            "type": "string",
            "minLength": 1,
            "title": "Url",
            "description": "Source URL of the clipped page"
          },
          "title": {
            "type": "string",
            "title": "Title",
            "description": "Article title (used for the filename)",
            "default": ""
          },
          "markdown": {
            "type": "string",
            "minLength": 1,
            "title": "Markdown",
            "description": "Article body as Markdown"
          },
          "source": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Source",
            "description": "Optional source hint for the YAML frontmatter sources[] field. Defaults to the url when omitted."
          },
          "vault_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Vault Id",
            "description": "Target vault identifier (W5 / PF-MCP-VAULT-1). Defaults to the active vault when omitted. Clips to a non-active vault are rejected (400) \u2014 activate the vault first."
          }
        },
        "type": "object",
        "required": [
          "url",
          "markdown"
        ],
        "title": "ClipRequest",
        "description": "Request body for POST /clip (F11, ADR-0038).\n\nSent by the Chrome MV3 extension after converting the article to Markdown\nvia Readability + Turndown. The extension owns the conversion; the server\nonly validates, sanitizes, and stores.",
        "example": {
          "markdown": "# Example Article\n\nArticle body...",
          "title": "Example Article",
          "url": "https://example.com/article"
        }
      },
      "ClipResponse": {
        "properties": {
          "file_path": {
            "type": "string",
            "title": "File Path",
            "description": "Saved path relative to vault_root, e.g. \"raw/sources/Example-Article.md\""
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "\"queued\" \u2014 file saved to raw/sources/; watcher ingests asynchronously."
          },
          "overwritten": {
            "type": "boolean",
            "title": "Overwritten",
            "description": "True if a same-named file already existed and was replaced on disk"
          }
        },
        "type": "object",
        "required": [
          "file_path",
          "status",
          "overwritten"
        ],
        "title": "ClipResponse",
        "description": "202 response body for POST /clip (F11, ADR-0038).",
        "example": {
          "file_path": "raw/sources/Example-Article.md",
          "overwritten": false,
          "status": "queued"
        }
      },
      "ConversationCreate": {
        "properties": {
          "vault_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Vault Id",
            "description": "Defaults to settings.vault_id"
          },
          "title": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Title",
            "description": "Optional initial title"
          }
        },
        "type": "object",
        "title": "ConversationCreate",
        "description": "Request body for POST /conversations (ADR-0019 \u00a72.5). vault_id defaults to settings."
      },
      "ConversationListResponse": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/ConversationResponse"
            },
            "type": "array",
            "title": "Items"
          },
          "total": {
            "type": "integer",
            "title": "Total"
          },
          "limit": {
            "type": "integer",
            "title": "Limit"
          },
          "offset": {
            "type": "integer",
            "title": "Offset"
          }
        },
        "type": "object",
        "required": [
          "items",
          "total",
          "limit",
          "offset"
        ],
        "title": "ConversationListResponse"
      },
      "ConversationRenameRequest": {
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 200,
            "minLength": 1,
            "title": "Title",
            "description": "New conversation title (1\u2013200 characters)"
          }
        },
        "type": "object",
        "required": [
          "title"
        ],
        "title": "ConversationRenameRequest",
        "description": "Request body for PATCH /conversations/{id} (R7-3, AC-R7-3-1)."
      },
      "ConversationRenameResponse": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "title": {
            "type": "string",
            "title": "Title"
          }
        },
        "type": "object",
        "required": [
          "id",
          "title"
        ],
        "title": "ConversationRenameResponse",
        "description": "Response for PATCH /conversations/{id}."
      },
      "ConversationResponse": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "vault_id": {
            "type": "string",
            "title": "Vault Id"
          },
          "title": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Title"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "title": "Updated At"
          },
          "preview": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Preview"
          }
        },
        "type": "object",
        "required": [
          "id",
          "vault_id",
          "title",
          "created_at",
          "updated_at"
        ],
        "title": "ConversationResponse",
        "description": "API shape for one conversations row (ADR-0019 \u00a72.5).\n\n`preview` (UXB-1) is a read-only derivation of the conversation's last message: its first\n~80 chars with <think>\u2026</think> and light markdown stripped. It is computed in the list\nhandler (bounded subquery); there is no schema change. None when the conversation has no\nmessages yet."
      },
      "CreateProjectRequest": {
        "properties": {
          "name": {
            "type": "string",
            "title": "Name"
          },
          "path": {
            "type": "string",
            "title": "Path"
          },
          "scenario": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Scenario"
          },
          "output_language": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Output Language"
          }
        },
        "type": "object",
        "required": [
          "name",
          "path"
        ],
        "title": "CreateProjectRequest",
        "description": "Body for POST /projects \u2014 create + scaffold a new vault folder."
      },
      "CreateReviewBody": {
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "stub",
              "generate"
            ],
            "title": "Mode",
            "description": "stub (default) \u2014 deterministic draft, no provider call; generate \u2014 full LLM generation (existing behaviour, explicit opt-in).",
            "default": "stub"
          }
        },
        "type": "object",
        "title": "CreateReviewBody",
        "description": "Optional JSON body for POST /review/queue/{id}/create (and /approve alias).\n\n``mode=\"stub\"`` (default): write a deterministic ``# <title>\\n\\n<description>`` draft\nwithout calling the LLM (llm_wiki parity \u2014 review-create-page.ts Create Page button).\n``mode=\"generate\"``: the existing full-LLM generation path (explicit secondary action)."
      },
      "EmbeddingConfigResponse": {
        "properties": {
          "embedding_url": {
            "type": "string",
            "title": "Embedding Url",
            "description": "HTTP endpoint for embeddings (EMBEDDING_URL env)"
          },
          "embedding_model": {
            "type": "string",
            "title": "Embedding Model",
            "description": "Model name for embeddings (EMBEDDING_MODEL env)"
          },
          "embedding_dim": {
            "type": "integer",
            "title": "Embedding Dim",
            "description": "Vector dimension (EMBEDDING_DIM env)"
          },
          "embeddings_enabled": {
            "type": "boolean",
            "title": "Embeddings Enabled",
            "description": "Whether the embedding data plane is active (EMBEDDINGS_ENABLED env, default true). When false, retrieval degrades to lexical/keyword-only and no embedding service is required at startup (ADR-0030, Feature B). Never exposes the embedding API key."
          }
        },
        "type": "object",
        "required": [
          "embedding_url",
          "embedding_model",
          "embedding_dim",
          "embeddings_enabled"
        ],
        "title": "EmbeddingConfigResponse"
      },
      "GraphCommunityDetailResponse": {
        "properties": {
          "community_id": {
            "type": "integer",
            "title": "Community Id",
            "description": "Louvain community id (0 = largest)"
          },
          "size": {
            "type": "integer",
            "title": "Size",
            "description": "Number of member pages in this community"
          },
          "cohesion": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Cohesion",
            "description": "Intra-edge density [0,1]: intraEdges / (size*(size-1)/2). 0 for singletons (no possible edges). Null until first recompute."
          },
          "cohesion_warning": {
            "type": "boolean",
            "title": "Cohesion Warning",
            "description": "True when cohesion < GRAPH_COHESION_WARN (default 0.2). Signals a loosely-connected / potentially fragmented community.",
            "default": false
          },
          "members": {
            "items": {
              "$ref": "#/components/schemas/GraphCommunityMemberResponse"
            },
            "type": "array",
            "title": "Members",
            "description": "Member pages ordered by degree descending, capped at 100. Each entry carries id, title, page_type, degree."
          }
        },
        "type": "object",
        "required": [
          "community_id",
          "size"
        ],
        "title": "GraphCommunityDetailResponse",
        "description": "Response for GET /graph/communities/{community_id} (R9-5, AC-R9-5-1).\n\nReads from the cached GraphSnapshot \u2014 never triggers a recompute (I2).\nReturns 409 when the snapshot is cold (cache has never run).\n\ncohesion formula:  cohesion = intraEdges / (size*(size-1)/2)\n  intraEdges = number of edges in the snapshot whose both endpoints belong to\n               this community.  possibleEdges = size*(size-1)/2.\n  cohesion in [0,1].  Singletons (size=1) -> cohesion=0, no possible edges.\n  cohesion_warning = True when cohesion < GRAPH_COHESION_WARN (default 0.2, env\n  GRAPH_COHESION_WARN).  See engine.py CommunitySnapshot for the per-recompute\n  computation; this endpoint reads the value stored in the snapshot (no re-derive).",
        "example": {
          "cohesion": 0.6,
          "cohesion_warning": false,
          "community_id": 0,
          "members": [
            {
              "degree": 4,
              "id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
              "page_type": "entity",
              "title": "Alpha"
            }
          ],
          "size": 5
        }
      },
      "GraphCommunityMemberResponse": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id",
            "description": "Page UUID"
          },
          "title": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Title",
            "description": "Page title (may be None if not yet set)"
          },
          "page_type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Page Type",
            "description": "Frontmatter type (entity/concept/etc.)"
          },
          "degree": {
            "type": "integer",
            "title": "Degree",
            "description": "Structural degree within the full graph",
            "default": 0
          }
        },
        "type": "object",
        "required": [
          "id"
        ],
        "title": "GraphCommunityMemberResponse",
        "description": "One member page in a community drill-down response (R9-5).",
        "example": {
          "degree": 4,
          "id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
          "page_type": "entity",
          "title": "Alpha"
        }
      },
      "GraphCommunityResponse": {
        "properties": {
          "id": {
            "type": "integer",
            "title": "Id"
          },
          "size": {
            "type": "integer",
            "title": "Size"
          },
          "cohesion": {
            "type": "number",
            "title": "Cohesion",
            "description": "Intra-edge density [0,1]; 0 for singletons"
          },
          "label": {
            "type": "string",
            "title": "Label",
            "description": "Human-readable community name for Community Mode (F18). Priority: dominant_domain \u2192 top_page.title \u2192 'Comunit\u00e0 {id}'. Computed server-side alongside cohesion (I2).",
            "default": ""
          },
          "dominant_domain": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Dominant Domain",
            "description": "Most-frequent in-vocabulary domain/* tag among community members (F18). Prefix 'domain/' is stripped. None when no valid domain tags are present."
          },
          "top_page": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/GraphCommunityTopPageResponse"
              },
              {
                "type": "null"
              }
            ],
            "description": "Highest-degree member page {id, title, slug} \u2014 used for label fallback and tooltip (F18). None when the community has no members."
          }
        },
        "type": "object",
        "required": [
          "id",
          "size",
          "cohesion"
        ],
        "title": "GraphCommunityResponse",
        "description": "Per-community summary in the GET /graph response (G-P0-2, F18).\n\nid             : re-numbered Louvain community id (0 = largest community).\nsize           : number of member nodes.\ncohesion       : intra-edge density in [0, 1]; 0 for singleton communities.\n                 Low cohesion (<0.1) signals a loosely-connected community\n                 suitable for a warning in the client legend.\nlabel          : human-readable display name for Community Mode (F18).\n                 Priority: dominant_domain \u2192 top_page.title \u2192 \"Comunit\u00e0 {id}\".\n                 Computed server-side in recompute() \u2014 no client computation (I2).\ndominant_domain: most-frequent in-vocab \"domain/<Name>\" tag among members (F18).\n                 None when no valid domain tags are present.\ntop_page       : highest-degree member page {id, title, slug} for disambiguation\n                 and tooltip (F18). None for communities with no members.",
        "example": {
          "cohesion": 0.42,
          "dominant_domain": "SAM",
          "id": 0,
          "label": "SAM",
          "size": 12,
          "top_page": {
            "id": "00000000-0000-0000-0000-000000000001",
            "slug": "software-asset-management",
            "title": "Software Asset Management"
          }
        }
      },
      "GraphCommunityTopPageResponse": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id",
            "description": "Page UUID"
          },
          "title": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Title",
            "description": "Page title"
          },
          "slug": {
            "type": "string",
            "title": "Slug",
            "description": "URL-safe slug derived from title"
          }
        },
        "type": "object",
        "required": [
          "id",
          "slug"
        ],
        "title": "GraphCommunityTopPageResponse",
        "description": "Highest-degree member page of a community \u2014 disambiguation + tooltip (F18)."
      },
      "GraphEdgeResponse": {
        "properties": {
          "source": {
            "type": "string",
            "title": "Source"
          },
          "target": {
            "type": "string",
            "title": "Target"
          },
          "weight": {
            "type": "number",
            "title": "Weight"
          },
          "kind": {
            "type": "string",
            "title": "Kind",
            "description": "Structural edge kind: \"link\" (direct wikilink) | \"source\" (shared provenance). ADR-0016 \u00a74",
            "default": "link"
          }
        },
        "type": "object",
        "required": [
          "source",
          "target",
          "weight"
        ],
        "title": "GraphEdgeResponse",
        "description": "One graph edge in the GET /graph response (ADR-0014 \u00a76, AC-F4-3, ADR-0016 \u00a74).\n\nsource/target are page-id strings (UUID). Undirected \u2014 emitted once per pair.\nkind: structural edge discriminator \u2014 \"link\" (wikilink exists) or \"source\"\n      (shared-source provenance only). ADR-0016 \u00a74."
      },
      "GraphEdgeSignalsResponse": {
        "properties": {
          "weight": {
            "type": "number",
            "title": "Weight",
            "description": "Total 4-signal edge weight (ADR-0012)"
          },
          "breakdown": {
            "additionalProperties": {
              "type": "number"
            },
            "type": "object",
            "title": "Breakdown",
            "description": "Per-signal weight components: {direct_links, shared_sources, adamic_adar, type_affinity}. direct_links = 3*direct_count; shared_sources = 4*shared_count; adamic_adar = 1.5*AA(A,B); type_affinity = matrix(typeA,typeB)."
          },
          "computed_at": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Computed At",
            "description": "ISO datetime when this edge row was last persisted by the graph engine."
          }
        },
        "type": "object",
        "required": [
          "weight",
          "breakdown"
        ],
        "title": "GraphEdgeSignalsResponse",
        "description": "4-signal weight breakdown for a single graph edge (R9-5, AC-R9-5-4).\n\nReads from the persisted edges.signals JSONB column (populated by\nGraphEngine.recompute() at every FA2 run).  Never triggers a recompute (I2).\n\nsignal semantics (ADR-0012):\n  direct_links   = 3.0 * direct_link_count   (wikilink coefficient x3)\n  shared_sources = 4.0 * shared_source_count  (provenance coefficient x4)\n  adamic_adar    = 1.5 * AA(A,B)             (AA coefficient x1.5)\n  type_affinity  = type_affinity_matrix(A,B)  (type-affinity, x1 -- see engine.py)\n  total weight   = sum of all four signals",
        "example": {
          "breakdown": {
            "adamic_adar": 0.7,
            "direct_links": 6.0,
            "shared_sources": 4.0,
            "type_affinity": 0.5
          },
          "computed_at": "2026-07-01T12:00:00Z",
          "weight": 11.2
        }
      },
      "GraphNodeResponse": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "title": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Title"
          },
          "type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Type"
          },
          "x": {
            "type": "number",
            "title": "X"
          },
          "y": {
            "type": "number",
            "title": "Y"
          },
          "size": {
            "type": "number",
            "title": "Size",
            "description": "BASE + GROWTH\u00b7sqrt(structural_degree); BASE=1.0, GROWTH=2.5 (ADR-0016 \u00a72)",
            "default": 1.0
          },
          "degree": {
            "type": "integer",
            "title": "Degree",
            "description": "Structural degree: count of distinct incident structural edges (direct-link or shared-source); drives size (ADR-0016 \u00a72/\u00a74)",
            "default": 0
          },
          "community": {
            "type": "integer",
            "title": "Community",
            "description": "Louvain community id (G-P0-2, I2). Re-numbered by size (0 = largest). -1 when not yet assigned (first recompute pending).",
            "default": -1
          },
          "domain": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Domain",
            "description": "The node page's own dominant in-vocabulary domain (F18, ADR-0054 \u00a72.2). Prefix 'domain/' is stripped. None when the page has no in-vocab domain tags. Tie-break: when \u22652 in-vocab domain tags are present, the one with the smallest index in the active vocabulary list wins (deterministic). Computed inside the graph recompute \u2014 no extra query (I1), cached with the snapshot (I2). Additive field \u2014 backward-compatible (default None)."
          }
        },
        "type": "object",
        "required": [
          "id",
          "title",
          "type",
          "x",
          "y"
        ],
        "title": "GraphNodeResponse",
        "description": "One graph node in the GET /graph response (ADR-0014 \u00a76, AC-F4-3, ADR-0016 \u00a74).\n\nRequired: id, title, type, x, y.\nOptional rendering hints (derived server-side): size, degree.\ncommunity: Louvain community id (G-P0-2); -1 when not yet assigned.\ndomain: the node page's own dominant in-vocabulary domain (F18, ADR-0054 \u00a72.2).\n  Stripped of the \"domain/\" prefix.  None when the page has no in-vocab domain tags.\n  Tie-break: when \u22652 in-vocab domain tags exist, first in vocabulary order wins.\n  Backward-compatible additive field (default None).",
        "example": {
          "community": 0,
          "degree": 3,
          "domain": "SAM",
          "id": "00000000-0000-0000-0000-000000000001",
          "size": 2.1,
          "title": "Alpha",
          "type": "entity",
          "x": 1.23,
          "y": -0.45
        }
      },
      "GraphResponse": {
        "properties": {
          "nodes": {
            "items": {
              "$ref": "#/components/schemas/GraphNodeResponse"
            },
            "type": "array",
            "title": "Nodes"
          },
          "edges": {
            "items": {
              "$ref": "#/components/schemas/GraphEdgeResponse"
            },
            "type": "array",
            "title": "Edges"
          },
          "data_version": {
            "type": "integer",
            "title": "Data Version"
          },
          "cached": {
            "type": "boolean",
            "title": "Cached"
          },
          "communities": {
            "items": {
              "$ref": "#/components/schemas/GraphCommunityResponse"
            },
            "type": "array",
            "title": "Communities",
            "description": "Per-community summary (G-P0-2, I2): id, size, cohesion. Ordered by id (0 = largest). Empty until first graph recompute."
          },
          "total_nodes": {
            "type": "integer",
            "title": "Total Nodes",
            "description": "Count of ALL live (non-deleted) pages in the vault (the full denominator). Use len(nodes) for the in-graph numerator: e.g. '816/986 pages' (GR1). Computed via a bounded indexed COUNT query \u2014 I1.",
            "default": 0
          },
          "total_edges": {
            "type": "integer",
            "title": "Total Edges",
            "description": "Count of ALL link rows for this vault (the full wikilink denominator). Use len(edges) for the in-graph numerator: e.g. '1024/4213 edges' (GR1). Computed via a bounded indexed COUNT query \u2014 I1.",
            "default": 0
          }
        },
        "type": "object",
        "required": [
          "nodes",
          "edges",
          "data_version",
          "cached"
        ],
        "title": "GraphResponse",
        "description": "GET /graph response payload (ADR-0014 \u00a76, AC-F4-3, AC-D4v3-1).\n\ncached     : true on a HIT (no FA2 this request), false on a MISS (FA2 ran inline).\ncommunities: Louvain community summary list (G-P0-2); empty until first recompute.\nHeader X-Graph-Cache: hit|miss mirrors cached (ADR-0014 \u00a75).",
        "example": {
          "cached": true,
          "communities": [
            {
              "cohesion": 1.0,
              "dominant_domain": "SAM",
              "id": 0,
              "label": "SAM",
              "size": 2,
              "top_page": {
                "id": "00000000-0000-0000-0000-000000000001",
                "slug": "alpha",
                "title": "Alpha"
              }
            }
          ],
          "data_version": 7,
          "edges": [
            {
              "kind": "link",
              "source": "00000000-0000-0000-0000-000000000001",
              "target": "00000000-0000-0000-0000-000000000002",
              "weight": 11.0
            }
          ],
          "nodes": [
            {
              "community": 0,
              "degree": 3,
              "domain": "SAM",
              "id": "00000000-0000-0000-0000-000000000001",
              "size": 2.1,
              "title": "Alpha",
              "type": "entity",
              "x": 1.23,
              "y": -0.45
            }
          ],
          "total_edges": 4213,
          "total_nodes": 986
        }
      },
      "HTTPValidationError": {
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ValidationError"
            },
            "type": "array",
            "title": "Detail"
          }
        },
        "type": "object",
        "title": "HTTPValidationError"
      },
      "ImportSchedulePutBody": {
        "properties": {
          "enabled": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Enabled",
            "description": "Enable or disable the scheduler"
          },
          "source_dir": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Source Dir",
            "description": "Container-visible path (e.g. /import); null to clear"
          },
          "frequency": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Frequency",
            "description": "'15m' | '1h' | '6h' | 'daily'"
          },
          "allowed_extensions": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Allowed Extensions",
            "description": "Comma-separated extensions to import; '' \u2192 default wider set. P3-c."
          },
          "excluded_folders": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Excluded Folders",
            "description": "Comma-separated folder names to skip; '' \u2192 none excluded. P3-c."
          },
          "max_size_mb": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Max Size Mb",
            "description": "Max file size in MB; 0 \u2192 no cap. P3-c."
          }
        },
        "type": "object",
        "title": "ImportSchedulePutBody",
        "description": "Request body for PUT /import-schedule (ADR-0020 \u00a74.6)."
      },
      "ImportSchedulePutResponse": {
        "properties": {
          "enabled": {
            "type": "boolean",
            "title": "Enabled",
            "description": "Scheduler is enabled",
            "default": false
          },
          "source_dir": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Source Dir",
            "description": "Container-visible absolute path to scan (e.g. /import)"
          },
          "frequency": {
            "type": "string",
            "title": "Frequency",
            "description": "'15m' | '1h' | '6h' | 'daily'",
            "default": "1h"
          },
          "allowed_extensions": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Allowed Extensions",
            "description": "Comma-separated extensions the scan imports (e.g. '.pdf,.csv'). null \u2192 default wider set (text + all extractable). P3-c."
          },
          "excluded_folders": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Excluded Folders",
            "description": "Comma-separated folder names skipped during the scan. null \u2192 none. P3-c."
          },
          "max_size_mb": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Max Size Mb",
            "description": "Max file size in MB the scan imports; larger skipped. null \u2192 no cap. P3-c."
          },
          "last_run_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Run At",
            "description": "Timestamp of the last completed scan; null if never run"
          },
          "last_status": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Status",
            "description": "ok | error | running | skipped_disabled | dir_missing | null"
          },
          "last_imported_count": {
            "type": "integer",
            "title": "Last Imported Count",
            "description": "Files copied (new/changed) during the last scan",
            "default": 0
          },
          "last_error": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Error",
            "description": "Error detail from the last failed scan; null on success"
          },
          "dir_ok": {
            "type": "boolean",
            "title": "Dir Ok",
            "description": "False if source_dir does not exist/is not readable inside the container",
            "default": true
          },
          "dir_message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Dir Message",
            "description": "Warning message when dir_ok is False; null when ok"
          }
        },
        "type": "object",
        "title": "ImportSchedulePutResponse",
        "description": "PUT /import-schedule response body (ADR-0020 \u00a74.6).\n\nExtends ImportScheduleResponse with dir validation fields (save-then-warn)."
      },
      "ImportScheduleResponse": {
        "properties": {
          "enabled": {
            "type": "boolean",
            "title": "Enabled",
            "description": "Scheduler is enabled",
            "default": false
          },
          "source_dir": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Source Dir",
            "description": "Container-visible absolute path to scan (e.g. /import)"
          },
          "frequency": {
            "type": "string",
            "title": "Frequency",
            "description": "'15m' | '1h' | '6h' | 'daily'",
            "default": "1h"
          },
          "allowed_extensions": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Allowed Extensions",
            "description": "Comma-separated extensions the scan imports (e.g. '.pdf,.csv'). null \u2192 default wider set (text + all extractable). P3-c."
          },
          "excluded_folders": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Excluded Folders",
            "description": "Comma-separated folder names skipped during the scan. null \u2192 none. P3-c."
          },
          "max_size_mb": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Max Size Mb",
            "description": "Max file size in MB the scan imports; larger skipped. null \u2192 no cap. P3-c."
          },
          "last_run_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Run At",
            "description": "Timestamp of the last completed scan; null if never run"
          },
          "last_status": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Status",
            "description": "ok | error | running | skipped_disabled | dir_missing | null"
          },
          "last_imported_count": {
            "type": "integer",
            "title": "Last Imported Count",
            "description": "Files copied (new/changed) during the last scan",
            "default": 0
          },
          "last_error": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Error",
            "description": "Error detail from the last failed scan; null on success"
          }
        },
        "type": "object",
        "title": "ImportScheduleResponse",
        "description": "GET /import-schedule response body (ADR-0020 \u00a74.6).\n\nReturns the current config + last-run status for the vault's import schedule.\nReturns sane defaults (enabled=false, frequency=\"1h\") if no row exists yet."
      },
      "IngestAllResponse": {
        "properties": {
          "started": {
            "type": "boolean",
            "title": "Started"
          },
          "candidate_files": {
            "type": "integer",
            "title": "Candidate Files"
          }
        },
        "type": "object",
        "required": [
          "started",
          "candidate_files"
        ],
        "title": "IngestAllResponse",
        "description": "Response for POST /sources/ingest-all."
      },
      "IngestAllStatusResponse": {
        "properties": {
          "running": {
            "type": "boolean",
            "title": "Running"
          },
          "done": {
            "type": "integer",
            "title": "Done"
          },
          "total": {
            "type": "integer",
            "title": "Total"
          }
        },
        "type": "object",
        "required": [
          "running",
          "done",
          "total"
        ],
        "title": "IngestAllStatusResponse",
        "description": "Response for GET /sources/ingest-all/status."
      },
      "IngestFromTextRequest": {
        "properties": {
          "text": {
            "type": "string",
            "minLength": 1,
            "title": "Text",
            "description": "Raw text to ingest (e.g. an assistant message)"
          },
          "source_hint": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Source Hint",
            "description": "Optional hint for the output filename stem, e.g. a message_id or short slug. Sanitised to basename; falls back to 'chat-<uuid>' when omitted or unsafe."
          },
          "vault_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Vault Id",
            "description": "Defaults to settings.vault_id"
          }
        },
        "type": "object",
        "required": [
          "text"
        ],
        "title": "IngestFromTextRequest",
        "description": "Request body for POST /ingest/from-text (ADR-0019 \u00a72.7, AC-F6-5 save-to-wiki).\n\nWrites ``text`` to ``vault/raw/sources/chat-{message_id}.md`` (or a derived name)\nand runs the same ``ingest_file`` seam (ADR-0003).  No new ingest logic \u2014 only a\nfile-materialisation step.",
        "example": {
          "source_hint": "chat-homelab-notes",
          "text": "# Homelab notes\nDocker services on TrueNAS..."
        }
      },
      "IngestFromTextResponse": {
        "properties": {
          "file_path": {
            "type": "string",
            "title": "File Path",
            "description": "Path written relative to vault_root"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "\"queued\" \u2014 watcher ingests asynchronously"
          },
          "page_id": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "title": "Page Id",
            "description": "Page UUID when ingest completes synchronously (trigger path); null when async (watcher path)."
          }
        },
        "type": "object",
        "required": [
          "file_path",
          "status"
        ],
        "title": "IngestFromTextResponse",
        "description": "202 response for POST /ingest/from-text (ADR-0019 \u00a72.7).",
        "example": {
          "file_path": "raw/sources/chat-homelab-notes.md",
          "status": "queued"
        }
      },
      "IngestRunListResponse": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/IngestRunResponse"
            },
            "type": "array",
            "title": "Items"
          },
          "total": {
            "type": "integer",
            "title": "Total"
          },
          "limit": {
            "type": "integer",
            "title": "Limit"
          },
          "offset": {
            "type": "integer",
            "title": "Offset"
          }
        },
        "type": "object",
        "required": [
          "items",
          "total",
          "limit",
          "offset"
        ],
        "title": "IngestRunListResponse",
        "description": "Paginated list response for GET /ingest/runs (ADR-0018 \u00a77, AC-BE-IR-1).\nOrdered started_at DESC (AC-BE-IR-3)."
      },
      "IngestRunResponse": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "vault_id": {
            "type": "string",
            "title": "Vault Id"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "running | completed | failed | converged_false (ADR-0018 \u00a77)"
          },
          "provider_type": {
            "type": "string",
            "title": "Provider Type",
            "description": "local | api | cli"
          },
          "pages_created": {
            "type": "integer",
            "title": "Pages Created",
            "description": "Wiki pages persisted during this run"
          },
          "page_type_counts": {
            "anyOf": [
              {
                "additionalProperties": {
                  "type": "integer"
                },
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Page Type Counts",
            "description": "Per-PageType created-page counts; null for legacy/unavailable runs"
          },
          "diagnostics": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Diagnostics",
            "description": "Non-convergence diagnostics (1.9.1 W5, NC-1): {stop_reason: 'converged'|'max_iter'|'token_budget', iterations, last_errors, tokens_used, token_budget}. Populated on the orchestrated/block routes for every terminal outcome; null for the delegated/CLI route (no bounded loop) and legacy rows."
          },
          "iterations_used": {
            "type": "integer",
            "title": "Iterations Used",
            "description": "Iterations consumed (aliases max_iter_used; 0 for delegated)"
          },
          "total_cost_usd": {
            "type": "number",
            "title": "Total Cost Usd",
            "description": "Total cost in USD; 0.0 for local/cli; serialised as number (I7)"
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "title": "Started At"
          },
          "completed_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Completed At",
            "description": "Run finish time (aliases finished_at); null for running rows"
          },
          "error_message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Error Message",
            "description": "Error detail for failed runs; null otherwise"
          }
        },
        "type": "object",
        "required": [
          "id",
          "vault_id",
          "status",
          "provider_type",
          "pages_created",
          "iterations_used",
          "total_cost_usd",
          "started_at",
          "completed_at",
          "error_message"
        ],
        "title": "IngestRunResponse",
        "description": "API response shape for one ingest_runs row (ADR-0018 \u00a77, AC-BE-IR-1).\n\nColumn aliases (no DB rename \u2014 ADR-0018 \u00a77 decision):\n  max_iter_used  \u2192 iterations_used\n  finished_at    \u2192 completed_at\ntotal_cost_usd serialised as a float; frontend formats to exactly 4dp (I7).",
        "example": {
          "completed_at": "2026-06-28T10:00:05Z",
          "id": "00000000-0000-0000-0000-000000000001",
          "iterations_used": 2,
          "pages_created": 3,
          "provider_type": "api",
          "started_at": "2026-06-28T10:00:00Z",
          "status": "completed",
          "total_cost_usd": 0.0042,
          "vault_id": "default"
        }
      },
      "IngestTriggerRequest": {
        "properties": {
          "file_path": {
            "type": "string",
            "title": "File Path",
            "description": "Relative path under vault/raw/sources/"
          }
        },
        "type": "object",
        "required": [
          "file_path"
        ],
        "title": "IngestTriggerRequest"
      },
      "IngestTriggerResponse": {
        "properties": {
          "task_id": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "title": "Task Id",
            "description": "Async task UUID (None in v0.2 synchronous mode; filled in v0.3+)"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "\"completed\" or \"skipped\" (I1 mtime/hash fast-path)"
          },
          "page_id": {
            "type": "string",
            "format": "uuid",
            "title": "Page Id",
            "description": "UUID of the ingested page row"
          }
        },
        "type": "object",
        "required": [
          "status",
          "page_id"
        ],
        "title": "IngestTriggerResponse",
        "description": "Typed 202 body for POST /ingest/trigger (AC-D4u \u2014 task_id appears in OpenAPI schema).\n\ntask_id is None in v0.2 (synchronous path); v0.3 fills it with a real async task UUID.\nstatus: \"completed\" | \"skipped\" (I1 fast-path) | \"queued\"/\"running\" (async, v0.3+).",
        "example": {
          "page_id": "00000000-0000-0000-0000-000000000001",
          "status": "completed"
        }
      },
      "LintFindingListResponse": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/LintFindingResponse"
            },
            "type": "array",
            "title": "Items"
          },
          "total": {
            "type": "integer",
            "title": "Total"
          },
          "limit": {
            "type": "integer",
            "title": "Limit"
          },
          "offset": {
            "type": "integer",
            "title": "Offset"
          },
          "severity_totals": {
            "additionalProperties": {
              "type": "integer"
            },
            "type": "object",
            "title": "Severity Totals",
            "description": "COUNT(*) per severity (info/warning/error) over the same vault + status + category predicate as 'total', but IGNORING the severity filter and pagination. Use this to render accurate group-header counts (e.g. 'Warnings (N)') even when a severity filter is active. Keys present only for severities with at least one matching finding."
          }
        },
        "type": "object",
        "required": [
          "items",
          "total",
          "limit",
          "offset"
        ],
        "title": "LintFindingListResponse",
        "description": "Paginated list response for GET /lint/findings (ADR-0037 \u00a76)."
      },
      "LintFindingResponse": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "lint_run_id": {
            "type": "string",
            "format": "uuid",
            "title": "Lint Run Id"
          },
          "vault_id": {
            "type": "string",
            "title": "Vault Id"
          },
          "category": {
            "type": "string",
            "title": "Category",
            "description": "orphan-page | broken-wikilink | missing-xref | contradiction | stale-claim | missing-page"
          },
          "severity": {
            "type": "string",
            "title": "Severity",
            "description": "info | warning | error"
          },
          "target_page_id": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "title": "Target Page Id"
          },
          "target_title": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Target Title"
          },
          "description": {
            "type": "string",
            "title": "Description"
          },
          "proposed_action": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Proposed Action",
            "description": "Fix apply_lint_fix would attempt; null for flag-only findings"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "open | applied | dismissed"
          },
          "resolution_note": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Resolution Note"
          },
          "suggested_target": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Suggested Target",
            "description": "Title of the best-matching existing page (broken-wikilink only). Displayed as the green 'Suggested target:' strip in the lint UI (L2)."
          },
          "suggested_page_id": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "title": "Suggested Page Id",
            "description": "FK \u2192 pages.id for the suggested_target page (L2)."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "reviewed_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Reviewed At"
          }
        },
        "type": "object",
        "required": [
          "id",
          "lint_run_id",
          "vault_id",
          "category",
          "severity",
          "description",
          "status",
          "created_at"
        ],
        "title": "LintFindingResponse",
        "description": "API response shape for one lint_findings row (ADR-0037 \u00a76)."
      },
      "LintRunListResponse": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/LintRunResponse"
            },
            "type": "array",
            "title": "Items"
          },
          "total": {
            "type": "integer",
            "title": "Total"
          },
          "limit": {
            "type": "integer",
            "title": "Limit"
          },
          "offset": {
            "type": "integer",
            "title": "Offset"
          }
        },
        "type": "object",
        "required": [
          "items",
          "total",
          "limit",
          "offset"
        ],
        "title": "LintRunListResponse",
        "description": "Paginated list response for GET /lint/runs (ADR-0037 \u00a76)."
      },
      "LintRunResponse": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "vault_id": {
            "type": "string",
            "title": "Vault Id"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "running | completed | error"
          },
          "max_iter": {
            "type": "integer",
            "title": "Max Iter"
          },
          "token_budget": {
            "type": "integer",
            "title": "Token Budget"
          },
          "iterations_used": {
            "type": "integer",
            "title": "Iterations Used"
          },
          "findings_count": {
            "type": "integer",
            "title": "Findings Count"
          },
          "total_cost_usd": {
            "type": "number",
            "title": "Total Cost Usd"
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "title": "Started At"
          },
          "completed_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Completed At"
          },
          "error_message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Error Message"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          }
        },
        "type": "object",
        "required": [
          "id",
          "vault_id",
          "status",
          "max_iter",
          "token_budget",
          "iterations_used",
          "findings_count",
          "total_cost_usd",
          "started_at",
          "created_at"
        ],
        "title": "LintRunResponse",
        "description": "API response shape for one lint_runs row (ADR-0037 \u00a76)."
      },
      "LintScanRequest": {
        "properties": {
          "vault_id": {
            "type": "string",
            "title": "Vault Id",
            "description": "Vault scope for the scan"
          },
          "max_iter": {
            "anyOf": [
              {
                "type": "integer",
                "maximum": 10.0,
                "minimum": 1.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Max Iter",
            "description": "Max semantic rounds (1..10); null \u2192 LINT_MAX_ITER default"
          },
          "token_budget": {
            "anyOf": [
              {
                "type": "integer",
                "maximum": 1000000.0,
                "minimum": 1000.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Token Budget",
            "description": "Token budget (1_000..1_000_000); null \u2192 LINT_TOKEN_BUDGET default"
          },
          "semantic": {
            "type": "boolean",
            "title": "Semantic",
            "description": "When False, skip the provider pass entirely (deterministic findings only; zero LLM cost). L8 / ADR-0037 B1.",
            "default": true
          }
        },
        "type": "object",
        "required": [
          "vault_id"
        ],
        "title": "LintScanRequest",
        "description": "Request body for POST /lint/scan (ADR-0037 \u00a76).\n\nmax_iter and token_budget are optional \u2014 env defaults (LINT_MAX_ITER / LINT_TOKEN_BUDGET)\napply when omitted. Both are FROZEN onto the lint_runs row before the scan runs (I7).\nServer-side validators cap the range so callers cannot request an unbounded run (I7).\nsemantic: when False, skip the provider pass entirely (deterministic findings only;\nrun records iterations_used=0, cost=0). L8 / ADR-0037 B1.",
        "example": {
          "max_iter": 3,
          "token_budget": 20000,
          "vault_id": "default"
        }
      },
      "LintScanResponse": {
        "properties": {
          "run": {
            "$ref": "#/components/schemas/LintRunResponse"
          },
          "findings": {
            "items": {
              "$ref": "#/components/schemas/LintFindingResponse"
            },
            "type": "array",
            "title": "Findings"
          }
        },
        "type": "object",
        "required": [
          "run",
          "findings"
        ],
        "title": "LintScanResponse",
        "description": "200 response for POST /lint/scan (ADR-0037 \u00a76): the run + its findings."
      },
      "MarkerBatchStatusFile": {
        "properties": {
          "file": {
            "type": "string",
            "title": "File",
            "description": "Original filename (e.g. 'report.pdf')"
          },
          "safe_stem": {
            "type": "string",
            "title": "Safe Stem",
            "description": "Sanitised stem (e.g. 'report')"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "'pending' | 'converting' | 'ok' | 'failed'"
          },
          "detail": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Detail",
            "description": "Error detail when status='failed'; null otherwise"
          },
          "companion_path": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Companion Path",
            "description": "Vault-relative path to the written .extracted.md (raw/sources/<stem>.extracted.md). Set when status='ok'; null otherwise."
          }
        },
        "type": "object",
        "required": [
          "file",
          "safe_stem",
          "status"
        ],
        "title": "MarkerBatchStatusFile",
        "description": "Per-file status entry for GET /ingest/convert-marker/status."
      },
      "MarkerBatchStatusResponse": {
        "properties": {
          "batch_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Batch Id",
            "description": "UUID of the current/most-recent batch; null if no batch has run"
          },
          "running": {
            "type": "boolean",
            "title": "Running",
            "description": "True while the background driver is active"
          },
          "total": {
            "type": "integer",
            "title": "Total",
            "description": "Files in this batch"
          },
          "done": {
            "type": "integer",
            "title": "Done",
            "description": "Files completed (ok + failed)"
          },
          "eta_seconds": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Eta Seconds",
            "description": "Estimated seconds remaining (null when done=0 or batch finished)"
          },
          "files": {
            "items": {
              "$ref": "#/components/schemas/MarkerBatchStatusFile"
            },
            "type": "array",
            "title": "Files",
            "description": "Per-file status entries (empty when no batch has run)"
          }
        },
        "type": "object",
        "required": [
          "running",
          "total",
          "done",
          "files"
        ],
        "title": "MarkerBatchStatusResponse",
        "description": "Response for GET /ingest/convert-marker/status.\n\nReturns the current (or most-recent) batch state with per-file detail.\nWhen no batch has ever run this session, running=false and all counts are 0."
      },
      "MarkerConvertAcceptResponse": {
        "properties": {
          "batch_id": {
            "type": "string",
            "title": "Batch Id",
            "description": "UUID identifying this conversion batch"
          },
          "queued": {
            "items": {
              "$ref": "#/components/schemas/MarkerConvertAcceptedFile"
            },
            "type": "array",
            "title": "Queued",
            "description": "Files accepted and queued for background Marker conversion"
          },
          "total": {
            "type": "integer",
            "title": "Total",
            "description": "Number of files queued (== len(queued))"
          }
        },
        "type": "object",
        "required": [
          "batch_id",
          "queued",
          "total"
        ],
        "title": "MarkerConvertAcceptResponse",
        "description": "202 Accepted response for POST /ingest/convert-marker (W0 async rewrite).\n\nThe Marker calls happen in a background task \u2014 poll\nGET /ingest/convert-marker/status (or GET /ingest/queue .marker_batch) for progress."
      },
      "MarkerConvertAcceptedFile": {
        "properties": {
          "file": {
            "type": "string",
            "title": "File",
            "description": "Original filename as submitted (e.g. 'report.pdf')"
          },
          "safe_stem": {
            "type": "string",
            "title": "Safe Stem",
            "description": "Sanitised stem used for the output paths (e.g. 'report')"
          },
          "pdf_path": {
            "type": "string",
            "title": "Pdf Path",
            "description": "Vault-relative path where the raw PDF was saved (raw/sources/<stem>.pdf)"
          }
        },
        "type": "object",
        "required": [
          "file",
          "safe_stem",
          "pdf_path"
        ],
        "title": "MarkerConvertAcceptedFile",
        "description": "One file entry in the 202 response body of POST /ingest/convert-marker."
      },
      "MarkerHealthResponse": {
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "offline"
            ],
            "title": "Status",
            "description": "\"ok\" when Marker responds 200; \"offline\" when unreachable or non-200."
          },
          "detail": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Detail",
            "description": "Error detail when status='offline'; null when ok."
          }
        },
        "type": "object",
        "required": [
          "status"
        ],
        "title": "MarkerHealthResponse",
        "description": "Response for GET /ingest/marker-health (R11-1 / AC-R11-1-4)."
      },
      "McpAuthRequest": {
        "properties": {
          "rotate_token": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Rotate Token",
            "description": "true \u21d2 generate a new high-entropy token (secrets.token_urlsafe(32)), store its PBKDF2 hash, return plaintext ONCE in generated_token. The plaintext is NEVER stored and NEVER returned again."
          },
          "token": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Token",
            "description": "Owner-supplied explicit token; stored as PBKDF2 hash only. generated_token stays null (owner already knows the value). Not echoed in the response."
          },
          "clear_token": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Clear Token",
            "description": "true \u21d2 set mcp_access_token_hash = NULL. If this leaves token_configured=false AND allow_without_token=false, remote_enabled is clamped OFF (no usable auth posture)."
          },
          "allow_without_token": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Allow Without Token",
            "description": "Persist the allow-without-token flag (ADR-0033 \u00a72.3). Omit to leave unchanged. true: private sources (loopback/CGNAT/RFC1918/link-local) may connect without a bearer token. PUBLIC sources are NEVER exempted."
          }
        },
        "type": "object",
        "title": "McpAuthRequest",
        "description": "Request body for PUT /mcp/auth (ADR-0033 \u00a72.5).\n\nAll fields are optional; omitting a field leaves that aspect unchanged.\nExactly one of rotate_token / token / clear_token should be used per call\n(using multiple is allowed but the last write wins: clear > explicit > rotate).\nallow_without_token can be set independently in the same call."
      },
      "McpAuthStateResponse": {
        "properties": {
          "token_configured": {
            "type": "boolean",
            "title": "Token Configured",
            "description": "True iff DB hash is set OR MCP_AUTH_TOKEN env bootstrap is set."
          },
          "token_source": {
            "type": "string",
            "title": "Token Source",
            "description": "\"db\" | \"env\" | \"none\" \u2014 authoritative token source (ADR-0033 \u00a72.1)."
          },
          "allow_without_token": {
            "type": "boolean",
            "title": "Allow Without Token",
            "description": "The persisted allow-without-token flag after this write."
          },
          "remote_enabled": {
            "type": "boolean",
            "title": "Remote Enabled",
            "description": "The remote_mcp_enabled flag after any allow-aware clamp."
          },
          "mount_path": {
            "type": "string",
            "title": "Mount Path",
            "description": "MCP_MOUNT_PATH constant (I6)."
          },
          "generated_token": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Generated Token",
            "description": "Populated ONLY when rotate_token=true \u2014 the plaintext token shown ONCE. null in all other cases. NEVER stored; NEVER returned by subsequent calls."
          }
        },
        "type": "object",
        "required": [
          "token_configured",
          "token_source",
          "allow_without_token",
          "remote_enabled",
          "mount_path"
        ],
        "title": "McpAuthStateResponse",
        "description": "Response body for PUT /mcp/auth (ADR-0033 \u00a72.5).\n\ntoken_configured, token_source, allow_without_token, remote_enabled, mount_path\nalways reflect the post-write state. generated_token is populated ONLY when\nrotate_token=true was set \u2014 shown ONCE, never returned again by any GET/PUT.\nNEVER contains the token, hash, or salt."
      },
      "McpInfoResponse": {
        "properties": {
          "server_name": {
            "type": "string",
            "title": "Server Name",
            "description": "MCP server name, derived from the live FastMCP object (I6)"
          },
          "transport": {
            "type": "string",
            "title": "Transport",
            "description": "MCP transport type (MCP_TRANSPORT env, default 'stdio'; ADR-0010)"
          },
          "entry_point_command": {
            "type": "string",
            "title": "Entry Point Command",
            "description": "Command to launch the MCP server (MCP_ENTRY_COMMAND env; ADR-0027 \u00a72.3)"
          },
          "tool_count": {
            "type": "integer",
            "title": "Tool Count",
            "description": "Number of tools currently registered in the server"
          },
          "tools": {
            "items": {
              "$ref": "#/components/schemas/McpToolInfo"
            },
            "type": "array",
            "title": "Tools",
            "description": "Introspected tool list from the live registry"
          },
          "http_enabled": {
            "type": "boolean",
            "title": "Http Enabled",
            "description": "Whether the HTTP MCP surface is compiled in and mounted (ADR-0029 \u00a72.2). Always true (ADR-0033 \u00a72.4 always-mount). The token itself is never returned. Alias: token_configured for backward compat."
          },
          "remote_write_enabled": {
            "type": "boolean",
            "title": "Remote Write Enabled",
            "description": "Whether write tools (write_page, resolve_review, trigger_source_rescan) are enabled on the HTTP surface (ADR-0029 \u00a72.3, ADR-0072). Reflects the effective runtime flag: vault_state.remote_mcp_write_enabled when non-NULL (DB-wins), else MCP_REMOTE_WRITE_ENABLED env (default false). Toggle via PUT /mcp/remote-write. Only meaningful when remote_enabled is true."
          },
          "token_configured": {
            "type": "boolean",
            "title": "Token Configured",
            "description": "True iff a token is configured \u2014 DB hash set OR MCP_AUTH_TOKEN env set (ADR-0033 \u00a72.1 precedence). NEVER contains the token value."
          },
          "remote_enabled": {
            "type": "boolean",
            "title": "Remote Enabled",
            "description": "The persisted runtime toggle state from vault_state.remote_mcp_enabled (ADR-0032 \u00a72.1). False by default; can be set via PUT /mcp/remote."
          },
          "mount_path": {
            "type": "string",
            "title": "Mount Path",
            "description": "The mount path for the remote MCP HTTP surface (= MCP_MOUNT_PATH constant). UI builds the connection URL as: window.location.origin + mount_path (ADR-0032 \u00a72.5; I6 \u2014 derived from constant, never hardcoded in handler)."
          },
          "token_source": {
            "type": "string",
            "title": "Token Source",
            "description": "\"db\" | \"env\" | \"none\" \u2014 which token source is authoritative (ADR-0033 \u00a72.1). \"db\": UI-set token (PBKDF2 hash in vault_state). \"env\": MCP_AUTH_TOKEN env bootstrap. \"none\": no token configured. NEVER the token value, hash, or salt."
          },
          "allow_without_token": {
            "type": "boolean",
            "title": "Allow Without Token",
            "description": "Whether token-less access is permitted for PRIVATE sources (loopback/CGNAT/RFC1918/link-local/ULA \u2014 ADR-0033 \u00a72.3). PUBLIC sources (Cloudflare tunnel) are NEVER exempted regardless of this flag."
          }
        },
        "type": "object",
        "required": [
          "server_name",
          "transport",
          "entry_point_command",
          "tool_count",
          "tools",
          "http_enabled",
          "remote_write_enabled",
          "token_configured",
          "remote_enabled",
          "mount_path",
          "token_source",
          "allow_without_token"
        ],
        "title": "McpInfoResponse",
        "description": "Response model for GET /mcp/info (ADR-0027 \u00a72.1; ADR-0029 \u00a72.5; ADR-0032 \u00a72.5; ADR-0033)."
      },
      "McpRemoteStateResponse": {
        "properties": {
          "remote_enabled": {
            "type": "boolean",
            "title": "Remote Enabled",
            "description": "The resulting persisted runtime flag (post-clamp). False when clamped=true."
          },
          "token_configured": {
            "type": "boolean",
            "title": "Token Configured",
            "description": "True iff MCP_AUTH_TOKEN is set (the security floor). NEVER contains the token value."
          },
          "mount_path": {
            "type": "string",
            "title": "Mount Path",
            "description": "Mount path for the remote MCP HTTP surface (= MCP_MOUNT_PATH constant; I6)."
          },
          "clamped": {
            "type": "boolean",
            "title": "Clamped",
            "description": "True iff the request asked enabled=true but MCP_AUTH_TOKEN is unset \u2014 the flag was forced to false (token-floor clamp, ADR-0032 \u00a72.4)."
          }
        },
        "type": "object",
        "required": [
          "remote_enabled",
          "token_configured",
          "mount_path",
          "clamped"
        ],
        "title": "McpRemoteStateResponse",
        "description": "Response model for PUT /mcp/remote (ADR-0032 \u00a72.4).\n\nAlways returned with HTTP 200 (even when clamped \u2014 the posture is reported truthfully).\nThe token itself is NEVER returned (I6)."
      },
      "McpRemoteToggleRequest": {
        "properties": {
          "enabled": {
            "type": "boolean",
            "title": "Enabled",
            "description": "Desired runtime state for the remote MCP HTTP surface."
          }
        },
        "type": "object",
        "required": [
          "enabled"
        ],
        "title": "McpRemoteToggleRequest",
        "description": "Request body for PUT /mcp/remote (ADR-0032 \u00a72.4)."
      },
      "McpRemoteWriteStateResponse": {
        "properties": {
          "remote_write_enabled": {
            "type": "boolean",
            "title": "Remote Write Enabled",
            "description": "The resulting persisted runtime flag (post-clamp). False when clamped=true."
          },
          "token_configured": {
            "type": "boolean",
            "title": "Token Configured",
            "description": "True iff a token is configured (DB hash or env bootstrap \u2014 ADR-0033 \u00a72.1). NEVER contains the token value."
          },
          "clamped": {
            "type": "boolean",
            "title": "Clamped",
            "description": "True iff the request asked enabled=true but neither a token is configured nor allow_without_token is set \u2014 the flag was forced to false (token-floor clamp, ADR-0072 \u00a74)."
          }
        },
        "type": "object",
        "required": [
          "remote_write_enabled",
          "token_configured",
          "clamped"
        ],
        "title": "McpRemoteWriteStateResponse",
        "description": "Response model for PUT /mcp/remote-write (ADR-0072 \u00a74).\n\nAlways returned with HTTP 200 (even when clamped \u2014 the posture is reported truthfully)."
      },
      "McpRemoteWriteToggleRequest": {
        "properties": {
          "enabled": {
            "type": "boolean",
            "title": "Enabled",
            "description": "Desired runtime state for the write tools on the HTTP MCP surface."
          }
        },
        "type": "object",
        "required": [
          "enabled"
        ],
        "title": "McpRemoteWriteToggleRequest",
        "description": "Request body for PUT /mcp/remote-write (ADR-0072 \u00a74)."
      },
      "McpToolInfo": {
        "properties": {
          "name": {
            "type": "string",
            "title": "Name",
            "description": "Tool name as registered in the FastMCP server"
          },
          "description": {
            "type": "string",
            "title": "Description",
            "description": "Full tool description (docstring); UI truncates"
          },
          "input_schema": {
            "additionalProperties": true,
            "type": "object",
            "title": "Input Schema",
            "description": "JSON-Schema object for the tool's input arguments (tool.parameters)"
          }
        },
        "type": "object",
        "required": [
          "name",
          "description",
          "input_schema"
        ],
        "title": "McpToolInfo",
        "description": "Schema for a single tool entry in GET /mcp/info (ADR-0027 \u00a72.1)."
      },
      "OpenProjectRequest": {
        "properties": {
          "path": {
            "type": "string",
            "title": "Path"
          }
        },
        "type": "object",
        "required": [
          "path"
        ],
        "title": "OpenProjectRequest",
        "description": "Body for POST /projects/open \u2014 register an existing vault folder."
      },
      "OpsScheduleEntry": {
        "properties": {
          "op": {
            "type": "string",
            "title": "Op",
            "description": "Operation name: 'lint', 'backfill', 'schema_review', or 'reclassify'"
          },
          "schedule": {
            "type": "string",
            "title": "Schedule",
            "description": "Effective schedule: off|hourly|daily|weekly"
          },
          "last_run_at": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Run At",
            "description": "ISO-8601 timestamp of the last completed run, or null."
          },
          "last_status": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Status",
            "description": "'ok' | 'dormant' | 'error:<msg>' | null (never run)."
          },
          "last_detail": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Detail",
            "description": "Short human outcome of the last run ('12 tagged / 30 processed', 'dormant: no domain vocabulary configured', 'error: ...'), or null. R13-12."
          },
          "in_flight": {
            "type": "boolean",
            "title": "In Flight",
            "description": "True while this op is currently executing."
          }
        },
        "type": "object",
        "required": [
          "op",
          "schedule",
          "in_flight"
        ],
        "title": "OpsScheduleEntry",
        "description": "One row in the GET /ops/schedules response \u2014 state of one schedulable op."
      },
      "OpsSchedulesResponse": {
        "properties": {
          "ops": {
            "items": {
              "$ref": "#/components/schemas/OpsScheduleEntry"
            },
            "type": "array",
            "title": "Ops"
          }
        },
        "type": "object",
        "required": [
          "ops"
        ],
        "title": "OpsSchedulesResponse",
        "description": "Response body for GET /ops/schedules."
      },
      "OutputLanguageRequest": {
        "properties": {
          "language": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Language"
          }
        },
        "type": "object",
        "required": [
          "language"
        ],
        "title": "OutputLanguageRequest",
        "description": "Request body for PUT /vault/meta/output-language."
      },
      "OutputLanguageResponse": {
        "properties": {
          "language": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Language"
          }
        },
        "type": "object",
        "required": [
          "language"
        ],
        "title": "OutputLanguageResponse",
        "description": "Response for GET /vault/meta/output-language."
      },
      "OverviewRegenerateResponse": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status"
          }
        },
        "type": "object",
        "required": [
          "status"
        ],
        "title": "OverviewRegenerateResponse",
        "description": "200 response for POST /ops/overview/regenerate."
      },
      "PageContentPutRequest": {
        "properties": {
          "content": {
            "type": "string",
            "minLength": 1,
            "title": "Content",
            "description": "Full UTF-8 markdown content to write"
          },
          "expected_hash": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expected Hash",
            "description": "Optimistic concurrency guard \u2014 sha256 hex of the content the client last read. When provided and it does NOT match the current on-disk hash, 409 is returned so the editor can warn about a stale edit."
          }
        },
        "type": "object",
        "required": [
          "content"
        ],
        "title": "PageContentPutRequest",
        "description": "Request body for PUT /pages/{id}/content (F1-content-write, ADR-0035)."
      },
      "PageContentPutResponse": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "content_hash": {
            "type": "string",
            "title": "Content Hash"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "title": "Updated At"
          }
        },
        "type": "object",
        "required": [
          "id",
          "content_hash",
          "updated_at"
        ],
        "title": "PageContentPutResponse",
        "description": "Response for PUT /pages/{id}/content (F1-content-write)."
      },
      "PageContentResponse": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "title": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Title"
          },
          "file_path": {
            "type": "string",
            "title": "File Path"
          },
          "content": {
            "type": "string",
            "title": "Content"
          },
          "content_hash": {
            "type": "string",
            "title": "Content Hash"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "title": "Updated At"
          },
          "type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Type",
            "description": "Frontmatter 'type'; NULL if absent (K6)"
          },
          "sources": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Sources",
            "description": "Frontmatter 'sources[]'; NULL if absent (K6)"
          },
          "tags": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Tags",
            "description": "Frontmatter 'tags[]' navigation tags; NULL if absent (K6)"
          }
        },
        "type": "object",
        "required": [
          "id",
          "title",
          "file_path",
          "content",
          "content_hash",
          "updated_at"
        ],
        "title": "PageContentResponse",
        "description": "Response for GET /pages/{id}/content (F1-content-read).\n\nAdditive extension (backward-compatible): page_type (serialised as \"type\"), sources, and\ntags are included so the reader can render a type badge, sources list, and navigation\ntags without a second call to GET /pages/{id}.  All three are nullable (NULL when absent\nfrom YAML frontmatter).  tags is the K6 navigation list (nashsu/llm_wiki parity), mirroring\nthe sources column."
      },
      "PageCreateRequest": {
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 500,
            "minLength": 1,
            "title": "Title",
            "description": "Page title (required)"
          },
          "page_type": {
            "type": "string",
            "title": "Page Type",
            "description": "Wiki page type; one of entity|concept|source|synthesis|comparison|query"
          },
          "dir": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Dir",
            "description": "Target subdirectory under wiki/ (optional; derived from page_type when omitted). Allowed values: ['comparisons', 'concepts', 'entities', 'queries', 'sources', 'synthesis']"
          },
          "content": {
            "type": "string",
            "title": "Content",
            "description": "Initial markdown body (empty is valid)",
            "default": ""
          }
        },
        "type": "object",
        "required": [
          "title",
          "page_type"
        ],
        "title": "PageCreateRequest",
        "description": "Request body for POST /pages (R7-2, AC-R7-2-2, [F1]).\n\nCreates a new wiki page with minimal frontmatter via the shared write_wiki_page seam (I1).\nA 409 is returned when a live page with the same (vault_id, file_path) already exists."
      },
      "PageCreateResponse": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "file_path": {
            "type": "string",
            "title": "File Path"
          },
          "title": {
            "type": "string",
            "title": "Title"
          },
          "page_type": {
            "type": "string",
            "title": "Page Type"
          }
        },
        "type": "object",
        "required": [
          "id",
          "file_path",
          "title",
          "page_type"
        ],
        "title": "PageCreateResponse",
        "description": "Response for POST /pages (201)."
      },
      "PageListResponse": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/PageResponse"
            },
            "type": "array",
            "title": "Items"
          },
          "total": {
            "type": "integer",
            "title": "Total"
          },
          "limit": {
            "type": "integer",
            "title": "Limit"
          },
          "offset": {
            "type": "integer",
            "title": "Offset"
          }
        },
        "type": "object",
        "required": [
          "items",
          "total",
          "limit",
          "offset"
        ],
        "title": "PageListResponse"
      },
      "PageResponse": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "vault_id": {
            "type": "string",
            "title": "Vault Id"
          },
          "file_path": {
            "type": "string",
            "title": "File Path"
          },
          "title": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Title"
          },
          "type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Type"
          },
          "sources": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Sources"
          },
          "content_hash": {
            "type": "string",
            "title": "Content Hash"
          },
          "qdrant_point_id": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "title": "Qdrant Point Id"
          },
          "deleted_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Deleted At"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "title": "Updated At"
          },
          "domain": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Domain",
            "description": "Dominant vocabulary domain derived server-side from 'domain/<name>' tags. null when the page is untagged or no domain vocabulary is configured. Reuses the same derivation logic as GET /stats/sections (stats.py)."
          },
          "community": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Community",
            "description": "Louvain community id persisted by GraphEngine.recompute(). null until the first graph recompute after migration 0020 (G-P0-2, I2)."
          }
        },
        "type": "object",
        "required": [
          "id",
          "vault_id",
          "file_path",
          "title",
          "sources",
          "content_hash",
          "qdrant_point_id",
          "deleted_at",
          "created_at",
          "updated_at"
        ],
        "title": "PageResponse"
      },
      "PatchPositionRequest": {
        "properties": {
          "x": {
            "type": "number",
            "title": "X",
            "description": "New x coordinate (FR space)"
          },
          "y": {
            "type": "number",
            "title": "Y",
            "description": "New y coordinate (FR space)"
          }
        },
        "type": "object",
        "required": [
          "x",
          "y"
        ],
        "title": "PatchPositionRequest",
        "description": "Body for PATCH /pages/{page_id}/position (Feature A)."
      },
      "PatchPositionResponse": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "x": {
            "type": "number",
            "title": "X"
          },
          "y": {
            "type": "number",
            "title": "Y"
          },
          "pinned": {
            "type": "boolean",
            "title": "Pinned"
          }
        },
        "type": "object",
        "required": [
          "id",
          "x",
          "y",
          "pinned"
        ],
        "title": "PatchPositionResponse",
        "description": "Response for PATCH /pages/{page_id}/position (Feature A)."
      },
      "PatchReviewRequest": {
        "properties": {
          "resolved": {
            "type": "boolean",
            "title": "Resolved",
            "description": "True \u2192 resolve (action required); False \u2192 reopen to pending. Default True.",
            "default": true
          },
          "action": {
            "type": "string",
            "title": "Action",
            "description": "Resolution action when resolved=True: skip | dismiss (exact tokens from ops/review.py; 422 on unknown). Ignored when resolved=False (reopen).",
            "default": "skip"
          }
        },
        "type": "object",
        "title": "PatchReviewRequest",
        "description": "Request body for PATCH /review/queue/{id} (B5/D2 \u2014 llm_wiki parity).",
        "example": {
          "action": "skip",
          "resolved": true
        }
      },
      "Project": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "path": {
            "type": "string",
            "title": "Path"
          },
          "created_at": {
            "type": "string",
            "title": "Created At"
          },
          "last_opened_at": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Opened At"
          }
        },
        "type": "object",
        "required": [
          "id",
          "name",
          "path",
          "created_at"
        ],
        "title": "Project",
        "description": "One known project (vault). ``id`` is the vault_id used for its DB/Qdrant rows."
      },
      "ProjectsResponse": {
        "properties": {
          "projects": {
            "items": {
              "$ref": "#/components/schemas/Project"
            },
            "type": "array",
            "title": "Projects"
          },
          "active_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Active Id"
          }
        },
        "type": "object",
        "required": [
          "projects",
          "active_id"
        ],
        "title": "ProjectsResponse",
        "description": "Response for GET /projects \u2014 the registry snapshot."
      },
      "ProviderConfigCreate": {
        "properties": {
          "scope": {
            "type": "string",
            "title": "Scope",
            "description": "global | vault | operation"
          },
          "operation": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Operation",
            "description": "ingest | chat | lint; required when scope='operation'"
          },
          "vault_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Vault Id",
            "description": "Required when scope='vault' or 'operation'"
          },
          "provider_type": {
            "type": "string",
            "title": "Provider Type",
            "description": "local | api | cli"
          },
          "model_id": {
            "type": "string",
            "title": "Model Id",
            "description": "Model name (e.g. claude-sonnet-4-6); lives only in DB rows (AC-F17-8)"
          },
          "base_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Base Url",
            "description": "OpenAI-compatible endpoint; NULL for Anthropic/local default"
          },
          "api_key": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Api Key",
            "description": "WRITE-ONLY (W1). Plaintext provider API key; encrypted at rest (Fernet, SYNAPSE_SECRET_KEY). NEVER returned by any response. Omit to use env-var keys. Requires SYNAPSE_SECRET_KEY configured server-side (else HTTP 400)."
          },
          "reasoning_effort": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Reasoning Effort",
            "description": "auto|off|low|medium|high|max|custom; null/auto = provider default (W1)"
          },
          "max_iter": {
            "type": "integer",
            "maximum": 20.0,
            "minimum": 1.0,
            "title": "Max Iter",
            "description": "Orchestrated-loop cap (I7)",
            "default": 3
          },
          "token_budget": {
            "type": "integer",
            "maximum": 1000000.0,
            "minimum": 1000.0,
            "title": "Token Budget",
            "description": "Loop token budget (I7)",
            "default": 60000
          },
          "is_fallback": {
            "type": "boolean",
            "title": "Is Fallback",
            "description": "Marks the single fallback row",
            "default": false
          }
        },
        "type": "object",
        "required": [
          "scope",
          "provider_type",
          "model_id"
        ],
        "title": "ProviderConfigCreate",
        "description": "Request body for POST /provider/config (F17, W1).\n\napi_key is WRITE-ONLY (W1, \u00a712 amendment): the plaintext is encrypted at rest\n(SYNAPSE_SECRET_KEY / Fernet) and NEVER returned by any response. Omit it to keep env-var\nkeys. model_id must be provided explicitly \u2014 no hardcoded defaults in app code (AC-F17-8)."
      },
      "ProviderConfigListResponse": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/ProviderConfigResponse"
            },
            "type": "array",
            "title": "Items"
          },
          "total": {
            "type": "integer",
            "title": "Total"
          }
        },
        "type": "object",
        "required": [
          "items",
          "total"
        ],
        "title": "ProviderConfigListResponse"
      },
      "ProviderConfigResponse": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "scope": {
            "type": "string",
            "title": "Scope"
          },
          "operation": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Operation"
          },
          "vault_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Vault Id"
          },
          "provider_type": {
            "type": "string",
            "title": "Provider Type"
          },
          "model_id": {
            "type": "string",
            "title": "Model Id"
          },
          "base_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Base Url"
          },
          "api_key_configured": {
            "type": "boolean",
            "title": "Api Key Configured",
            "description": "True iff a UI API key is stored (encrypted) for this row. Never the value."
          },
          "api_key_masked": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Api Key Masked",
            "description": "Non-reversible masked hint ('\u20261234') when a key is stored; null otherwise."
          },
          "reasoning_effort": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Reasoning Effort"
          },
          "max_iter": {
            "type": "integer",
            "title": "Max Iter"
          },
          "token_budget": {
            "type": "integer",
            "title": "Token Budget"
          },
          "is_fallback": {
            "type": "boolean",
            "title": "Is Fallback"
          },
          "created_at": {
            "title": "Created At"
          },
          "updated_at": {
            "title": "Updated At"
          }
        },
        "type": "object",
        "required": [
          "id",
          "scope",
          "operation",
          "vault_id",
          "provider_type",
          "model_id",
          "base_url",
          "api_key_configured",
          "reasoning_effort",
          "max_iter",
          "token_budget",
          "is_fallback",
          "created_at",
          "updated_at"
        ],
        "title": "ProviderConfigResponse",
        "description": "API response shape for a provider_config row (\u00a712 amendment, W1).\n\nNEVER exposes the plaintext key. Instead: api_key_configured (bool) + api_key_masked\n(\"\u2026last4\" hint, or null). reasoning_effort is echoed (config, not a secret)."
      },
      "ProviderConfigUpdate": {
        "properties": {
          "provider_type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Provider Type",
            "description": "local | api | cli"
          },
          "model_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Model Id",
            "description": "Model name (lives only in DB rows)"
          },
          "base_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Base Url",
            "description": "OpenAI-compatible endpoint or null"
          },
          "api_key": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Api Key",
            "description": "WRITE-ONLY (W1). Non-empty \u21d2 replace stored key (encrypted). Empty string \u21d2 clear the stored key (env fallback). Omit to leave unchanged. NEVER returned."
          },
          "reasoning_effort": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Reasoning Effort",
            "description": "auto|off|low|medium|high|max|custom; omit to leave unchanged"
          },
          "max_iter": {
            "anyOf": [
              {
                "type": "integer",
                "maximum": 20.0,
                "minimum": 1.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Max Iter",
            "description": "Orchestrated-loop cap"
          },
          "token_budget": {
            "anyOf": [
              {
                "type": "integer",
                "maximum": 1000000.0,
                "minimum": 1000.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Token Budget",
            "description": "Loop token budget (I7)"
          },
          "is_fallback": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Is Fallback",
            "description": "Marks the single fallback row"
          }
        },
        "type": "object",
        "title": "ProviderConfigUpdate",
        "description": "Request body for PUT /provider/config/{id} (W1). All fields optional \u2014 omitted fields are\nleft unchanged.\n\napi_key semantics (WRITE-ONLY): field ABSENT \u21d2 leave the stored key untouched; a non-empty\nstring \u21d2 re-encrypt and replace; an empty string \"\" \u21d2 CLEAR the stored key (fall back to\nenv). The plaintext is NEVER returned."
      },
      "ProviderTestRequest": {
        "properties": {
          "config_id": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "title": "Config Id",
            "description": "Probe an existing provider_config row (uses its stored key)."
          },
          "provider_type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Provider Type",
            "description": "local | api | cli"
          },
          "base_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Base Url",
            "description": "OpenAI-compatible endpoint (api only)"
          },
          "model": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Model",
            "description": "Model id to probe"
          },
          "api_key": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Api Key",
            "description": "WRITE-ONLY inline key; overrides the stored/env key. Never echoed."
          }
        },
        "type": "object",
        "title": "ProviderTestRequest",
        "description": "Request body for the provider-test endpoints (W1).\n\nProvide EITHER config_id (probe a stored row \u2014 its decrypted key is used) OR inline\n{provider_type, model, base_url?, api_key?}. Inline fields override the stored row when both\nare given. api_key is WRITE-ONLY \u2014 never echoed back."
      },
      "ProviderTestResponse": {
        "properties": {
          "ok": {
            "type": "boolean",
            "title": "Ok",
            "description": "True iff the probe succeeded (connection: HTTP ok; function: reply matched)."
          },
          "latency_ms": {
            "type": "integer",
            "title": "Latency Ms",
            "description": "Wall-clock latency of the probe in milliseconds."
          },
          "detail": {
            "type": "string",
            "title": "Detail",
            "description": "Human-readable outcome. NEVER contains the API key."
          }
        },
        "type": "object",
        "required": [
          "ok",
          "latency_ms",
          "detail"
        ],
        "title": "ProviderTestResponse",
        "description": "Response body for the provider-test endpoints (W1). Never contains a key."
      },
      "QueueBatchProgress": {
        "properties": {
          "running": {
            "type": "boolean",
            "title": "Running",
            "description": "True while an ingest-all batch is in progress"
          },
          "done": {
            "type": "integer",
            "title": "Done",
            "description": "Files processed so far in the current batch"
          },
          "total": {
            "type": "integer",
            "title": "Total",
            "description": "Total files in the current batch"
          },
          "eta_seconds": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Eta Seconds",
            "description": "Estimated seconds remaining for the whole batch (None if unknown)"
          }
        },
        "type": "object",
        "required": [
          "running",
          "done",
          "total"
        ],
        "title": "QueueBatchProgress",
        "description": "Whole-batch progress for a POST /sources/ingest-all run (surfaced in the activity panel).\n\nLets the UI show \"done/total\" + a total ETA for the entire bulk index, not just the\none-file-at-a-time queue view. eta_seconds = remaining files \u00d7 avg per-file duration\n(best-effort; None when no history)."
      },
      "QueueCancelResponse": {
        "properties": {
          "run_id": {
            "type": "string",
            "title": "Run Id"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "'cancelling' \u2014 abort requested; cleanup completes async"
          },
          "cleaned_pages": {
            "type": "integer",
            "title": "Cleaned Pages",
            "description": "Always 0 at request time; cascade cleanup happens at the next loop boundary",
            "default": 0
          }
        },
        "type": "object",
        "required": [
          "run_id",
          "status"
        ],
        "title": "QueueCancelResponse",
        "description": "Response for POST /ingest/runs/{id}/cancel (ADR-0046 \u00a76)."
      },
      "QueuePauseResponse": {
        "properties": {
          "paused": {
            "type": "boolean",
            "title": "Paused",
            "description": "Always true; idempotent"
          }
        },
        "type": "object",
        "required": [
          "paused"
        ],
        "title": "QueuePauseResponse",
        "description": "Response for POST /ingest/queue/pause (ADR-0046 \u00a76)."
      },
      "QueueResumeResponse": {
        "properties": {
          "paused": {
            "type": "boolean",
            "title": "Paused",
            "description": "Always false; idempotent"
          },
          "drained": {
            "type": "integer",
            "title": "Drained",
            "description": "Number of pending entries replayed to the watcher"
          }
        },
        "type": "object",
        "required": [
          "paused",
          "drained"
        ],
        "title": "QueueResumeResponse",
        "description": "Response for POST /ingest/queue/resume (ADR-0046 \u00a76)."
      },
      "QueueRetryResponse": {
        "properties": {
          "run_id_prev": {
            "type": "string",
            "title": "Run Id Prev",
            "description": "UUID of the failed run that was retried"
          },
          "source_path": {
            "type": "string",
            "title": "Source Path"
          },
          "retry_count": {
            "type": "integer",
            "title": "Retry Count",
            "description": "New retry count (1..3); I7 hard cap = 3"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "'queued' \u2014 re-dispatch accepted",
            "default": "queued"
          }
        },
        "type": "object",
        "required": [
          "run_id_prev",
          "source_path",
          "retry_count"
        ],
        "title": "QueueRetryResponse",
        "description": "Response for POST /ingest/runs/{id}/retry (ADR-0046 \u00a76)."
      },
      "QueueSnapshotResponse": {
        "properties": {
          "paused": {
            "type": "boolean",
            "title": "Paused",
            "description": "True when the queue is paused (no new dispatches)"
          },
          "pending": {
            "type": "integer",
            "title": "Pending",
            "description": "Number of FS events parked while queue is paused"
          },
          "processing": {
            "type": "integer",
            "title": "Processing",
            "description": "Number of currently in-flight ingest runs"
          },
          "failed": {
            "type": "integer",
            "title": "Failed",
            "description": "Number of recently-failed tasks retained for retry"
          },
          "completed_since_idle": {
            "type": "integer",
            "title": "Completed Since Idle",
            "description": "Successful completions since the queue last went idle"
          },
          "total": {
            "type": "integer",
            "title": "Total",
            "description": "pending + processing + failed"
          },
          "tasks": {
            "items": {
              "$ref": "#/components/schemas/QueueTaskItem"
            },
            "type": "array",
            "title": "Tasks",
            "description": "All visible tasks (pending+processing+failed)"
          },
          "batch": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/QueueBatchProgress"
              },
              {
                "type": "null"
              }
            ],
            "description": "Whole-batch progress for an in-progress POST /sources/ingest-all (else null)"
          },
          "marker_batch": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/QueueBatchProgress"
              },
              {
                "type": "null"
              }
            ],
            "description": "Whole-batch progress for an in-progress POST /ingest/convert-marker (else null). Poll GET /ingest/convert-marker/status for per-file detail."
          }
        },
        "type": "object",
        "required": [
          "paused",
          "pending",
          "processing",
          "failed",
          "completed_since_idle",
          "total",
          "tasks"
        ],
        "title": "QueueSnapshotResponse",
        "description": "Live activity queue snapshot for GET /ingest/queue (ADR-0046 \u00a76)."
      },
      "QueueTaskItem": {
        "properties": {
          "run_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Run Id",
            "description": "UUID of the in-flight run; null for pending (not yet dispatched) tasks"
          },
          "source_path": {
            "type": "string",
            "title": "Source Path",
            "description": "Relative raw source path (raw/sources/\u2026)"
          },
          "filename": {
            "type": "string",
            "title": "Filename",
            "description": "Basename of source_path"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "pending | processing | failed"
          },
          "retry_count": {
            "type": "integer",
            "title": "Retry Count",
            "description": "Times this source has been retried (I7, max 3)"
          },
          "error": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Error",
            "description": "Error detail for failed tasks; null otherwise"
          },
          "started_at": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Started At",
            "description": "ISO-8601 start time; null for pending tasks"
          },
          "phase": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Phase",
            "description": "Human-facing current phase: queued | analyzing | generating (N/M) | validating | writing | agent running. None for pending/failed tasks."
          },
          "progress": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Progress",
            "description": "Coarse 0..1 progress for orchestrated runs (0.0=queued, 0.2=analyzing, 0.5=generating, 0.8=validating, 0.95=writing). None for delegated/CLI runs (indeterminate spinner) and non-processing tasks."
          },
          "elapsed_seconds": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Elapsed Seconds",
            "description": "Seconds since the run started; null for pending/failed tasks."
          },
          "eta_seconds": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Eta Seconds",
            "description": "Best-effort estimated seconds until completion, derived from the historical average duration for this route (last 50 completed runs). None when no history is available for the route or for non-processing tasks."
          }
        },
        "type": "object",
        "required": [
          "run_id",
          "source_path",
          "filename",
          "status",
          "retry_count",
          "error",
          "started_at"
        ],
        "title": "QueueTaskItem",
        "description": "One task in the live ingest queue snapshot (ADR-0046 \u00a76)."
      },
      "ReclassifyTypesRequest": {
        "properties": {
          "max_pages": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Max Pages",
            "description": "Cap on pages processed this run (clamped server-side)."
          },
          "token_budget": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Token Budget",
            "description": "Token budget for the run (clamped server-side, I7)."
          },
          "force": {
            "type": "boolean",
            "title": "Force",
            "description": "Widen candidates from the suspicious set (NULL/untyped/concept) to ALL non-reserved wiki pages (overview/index are never touched in either mode).",
            "default": false
          }
        },
        "type": "object",
        "title": "ReclassifyTypesRequest",
        "description": "Request body for POST /ops/reclassify-types (SPRINT-v1.2 tail)."
      },
      "ReclassifyTypesStartResponse": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status",
            "description": "'started' \u2014 reclassify runs in background",
            "default": "started"
          },
          "max_pages": {
            "type": "integer",
            "title": "Max Pages",
            "description": "Effective (clamped) page cap for this run"
          },
          "token_budget": {
            "type": "integer",
            "title": "Token Budget",
            "description": "Effective (clamped) token budget for this run"
          },
          "force": {
            "type": "boolean",
            "title": "Force",
            "description": "Whether ALL non-reserved pages are candidates"
          }
        },
        "type": "object",
        "required": [
          "max_pages",
          "token_budget",
          "force"
        ],
        "title": "ReclassifyTypesStartResponse",
        "description": "202 response for POST /ops/reclassify-types."
      },
      "ReclassifyTypesStatusResponse": {
        "properties": {
          "running": {
            "type": "boolean",
            "title": "Running",
            "description": "True while a reclassify run is in flight"
          },
          "last_summary": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Summary",
            "description": "Summary of the most recent completed run (null if never ran)"
          }
        },
        "type": "object",
        "required": [
          "running"
        ],
        "title": "ReclassifyTypesStatusResponse",
        "description": "GET /ops/reclassify-types \u2014 single-flight state + last completed summary."
      },
      "ReconcileFoldersRequest": {
        "properties": {
          "max_pages": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Max Pages",
            "description": "Cap on wiki pages scanned per run (clamped server-side, I7)."
          },
          "apply": {
            "type": "boolean",
            "title": "Apply",
            "description": "False (default) = dry-run only \u2014 returns a plan with no file writes. True = perform actual moves + DB + Qdrant updates.",
            "default": false
          }
        },
        "type": "object",
        "title": "ReconcileFoldersRequest",
        "description": "Request body for POST /ops/reconcile-folders."
      },
      "ReconcileFoldersStartResponse": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status",
            "description": "'started' \u2014 reconcile runs in background",
            "default": "started"
          },
          "max_pages": {
            "type": "integer",
            "title": "Max Pages",
            "description": "Effective (clamped) page cap for this run"
          },
          "apply": {
            "type": "boolean",
            "title": "Apply",
            "description": "Whether moves are applied (False = dry-run)"
          }
        },
        "type": "object",
        "required": [
          "max_pages",
          "apply"
        ],
        "title": "ReconcileFoldersStartResponse",
        "description": "202 response for POST /ops/reconcile-folders."
      },
      "ReconcileFoldersStatusResponse": {
        "properties": {
          "running": {
            "type": "boolean",
            "title": "Running",
            "description": "True while a reconcile run is in flight"
          },
          "last_summary": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Summary",
            "description": "Summary of the most recent completed run (null if never ran). Includes 'plan' (list of proposed moves) in dry-run mode."
          }
        },
        "type": "object",
        "required": [
          "running"
        ],
        "title": "ReconcileFoldersStatusResponse",
        "description": "GET /ops/reconcile-folders \u2014 single-flight state + last completed summary."
      },
      "ReferencedPage": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "title": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Title"
          },
          "type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Type"
          }
        },
        "type": "object",
        "required": [
          "id"
        ],
        "title": "ReferencedPage",
        "description": "Convenience join for a referenced_page_ids entry (ADR-0044 \u00a76.1)."
      },
      "RegenerateGraphResponse": {
        "properties": {
          "reconnected": {
            "type": "integer",
            "title": "Reconnected",
            "description": "Previously-dangling wikilinks reconnected to a live page this pass."
          },
          "remaining_dangling": {
            "type": "integer",
            "title": "Remaining Dangling",
            "description": "Wikilinks still dangling after the reconnect pass."
          },
          "nodes": {
            "type": "integer",
            "title": "Nodes",
            "description": "Node count in the freshly recomputed snapshot."
          },
          "edges": {
            "type": "integer",
            "title": "Edges",
            "description": "Edge count in the freshly recomputed snapshot."
          },
          "data_version": {
            "type": "integer",
            "title": "Data Version",
            "description": "data_version after the operation."
          }
        },
        "type": "object",
        "required": [
          "reconnected",
          "remaining_dangling",
          "nodes",
          "edges",
          "data_version"
        ],
        "title": "RegenerateGraphResponse",
        "description": "Response for POST /graph/recompute (the 'Regenerate graph' action)."
      },
      "RegenerateOverviewResponse": {
        "properties": {
          "regenerated": {
            "type": "boolean",
            "title": "Regenerated",
            "description": "Whether overview.md was rewritten this call."
          },
          "detected_language": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Detected Language",
            "description": "Language the overview was regenerated in (ISO-639-1) or null."
          }
        },
        "type": "object",
        "required": [
          "regenerated"
        ],
        "title": "RegenerateOverviewResponse",
        "description": "Response for POST /overview/regenerate (F3 \u2014 force an overview refresh without re-ingesting).\n\nregenerated: True if the overview.md note was rewritten this call (False = degrade-safe\n             keep-previous, e.g. no provider configured or the provider call failed/timed out).\ndetected_language: the language the overview was requested in (ISO-639-1) \u2014 from\n                   the vault's existing pages; None when undetectable.",
        "example": {
          "detected_language": "it",
          "regenerated": true
        }
      },
      "RelatedPageItem": {
        "properties": {
          "page_id": {
            "type": "string",
            "format": "uuid",
            "title": "Page Id",
            "description": "UUID of the related page"
          },
          "title": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Title",
            "description": "YAML frontmatter title; NULL if absent"
          },
          "type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Type",
            "description": "YAML frontmatter type; NULL if absent"
          },
          "score": {
            "type": "number",
            "title": "Score",
            "description": "4-signal edge weight (higher = more related)"
          }
        },
        "type": "object",
        "required": [
          "page_id",
          "score"
        ],
        "title": "RelatedPageItem",
        "description": "One entry in the GET /pages/{id}/related response.\n\nscore is the stored 4-signal edge weight (ADR-0012):\n  3\u00b7direct_link_count + 4\u00b7shared_source_count + 1.5\u00b7adamic_adar + 1\u00b7same_type\nReuses the persisted edges table \u2014 no recompute (I1/I2)."
      },
      "RelatedPagesResponse": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/RelatedPageItem"
            },
            "type": "array",
            "title": "Items"
          },
          "total": {
            "type": "integer",
            "title": "Total",
            "description": "Total related pages found (before limit)"
          }
        },
        "type": "object",
        "required": [
          "items",
          "total"
        ],
        "title": "RelatedPagesResponse",
        "description": "Response for GET /pages/{id}/related.",
        "example": {
          "items": [
            {
              "page_id": "00000000-0000-0000-0000-000000000002",
              "score": 11.5,
              "title": "Beta Concept",
              "type": "concept"
            }
          ],
          "total": 1
        }
      },
      "ReresolveLinksResponse": {
        "properties": {
          "reconnected": {
            "type": "integer",
            "title": "Reconnected",
            "description": "Previously-dangling links reconnected to a live page in this pass."
          },
          "remaining_dangling": {
            "type": "integer",
            "title": "Remaining Dangling",
            "description": "Links still dangling after the pass (target has no matching live page)."
          }
        },
        "type": "object",
        "required": [
          "reconnected",
          "remaining_dangling"
        ],
        "title": "ReresolveLinksResponse",
        "description": "Response for POST /links/reresolve (F3/K3 cross-ingest connectivity backfill).\n\nreconnected: number of previously-dangling links now bound to a live page.\nremaining_dangling: number of links still dangling after the pass.",
        "example": {
          "reconnected": 42,
          "remaining_dangling": 7
        }
      },
      "ResearchOptimizeTopicRequest": {
        "properties": {
          "topic": {
            "type": "string",
            "minLength": 1,
            "title": "Topic",
            "description": "Seed research topic (non-empty)"
          },
          "vault_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Vault Id",
            "description": "Vault scope used to resolve the provider AND to load overview.md/purpose.md context. null \u2192 the server's default vault_id (settings.vault_id)."
          }
        },
        "type": "object",
        "required": [
          "topic"
        ],
        "title": "ResearchOptimizeTopicRequest",
        "description": "Request body for POST /research/optimize-topic (B5/D3).\n\nA seed topic (typically taken from a Graph Insight) to be rephrased into a domain-specific\nresearch topic + web-search-optimized queries before the user confirms a Deep Research run.",
        "example": {
          "topic": "container networking",
          "vault_id": "default"
        }
      },
      "ResearchOptimizeTopicResponse": {
        "properties": {
          "optimized_topic": {
            "type": "string",
            "title": "Optimized Topic",
            "description": "Domain-specific rephrasing of the seed topic"
          },
          "queries": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Queries",
            "description": "3..5 web-search-optimized SearXNG queries (or [topic] on the fallback path)"
          }
        },
        "type": "object",
        "required": [
          "optimized_topic"
        ],
        "title": "ResearchOptimizeTopicResponse",
        "description": "200 response for POST /research/optimize-topic (B5/D3).\n\noptimized_topic prefills the editable topic field; queries prefill the editable query list.\nOn the no-provider / degraded path optimized_topic echoes the seed topic and queries is\n[topic] \u2014 the dialog still opens (never a 500).",
        "example": {
          "optimized_topic": "Kubernetes container networking (CNI) \u2014 Calico vs Cilium",
          "queries": [
            "Kubernetes CNI comparison Calico Cilium",
            "Calico BGP networking Kubernetes",
            "Cilium eBPF networking Kubernetes"
          ]
        }
      },
      "ResearchRunDetail": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "vault_id": {
            "type": "string",
            "title": "Vault Id"
          },
          "topic": {
            "type": "string",
            "title": "Topic"
          },
          "status": {
            "type": "string",
            "title": "Status"
          },
          "max_iter": {
            "type": "integer",
            "title": "Max Iter"
          },
          "token_budget": {
            "type": "integer",
            "title": "Token Budget"
          },
          "iterations_used": {
            "type": "integer",
            "title": "Iterations Used"
          },
          "queries_used": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Queries Used"
          },
          "sources_fetched": {
            "type": "integer",
            "title": "Sources Fetched"
          },
          "total_cost_usd": {
            "type": "number",
            "title": "Total Cost Usd"
          },
          "synthesis_text": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Synthesis Text"
          },
          "synthesis_page_id": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "title": "Synthesis Page Id"
          },
          "sources": {
            "items": {
              "$ref": "#/components/schemas/ResearchSourceSummary"
            },
            "type": "array",
            "title": "Sources"
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "title": "Started At"
          },
          "completed_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Completed At"
          },
          "error_message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Error Message"
          }
        },
        "type": "object",
        "required": [
          "id",
          "vault_id",
          "topic",
          "status",
          "max_iter",
          "token_budget",
          "iterations_used",
          "queries_used",
          "sources_fetched",
          "total_cost_usd",
          "started_at"
        ],
        "title": "ResearchRunDetail",
        "description": "GET /research/runs/{id} response (ADR-0024 \u00a78.3, AC-F10-4c).\n\nIncludes the full queries_used array and per-source summaries.\nsynthesis_text is null until step 5 completes (AC-F10-4c).\nsources array excludes fetched_content_md blobs by default (size guard, ADR-0024 \u00a78.3)."
      },
      "ResearchRunListResponse": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/ResearchRunSummary"
            },
            "type": "array",
            "title": "Items"
          },
          "total": {
            "type": "integer",
            "title": "Total"
          },
          "limit": {
            "type": "integer",
            "title": "Limit"
          },
          "offset": {
            "type": "integer",
            "title": "Offset"
          }
        },
        "type": "object",
        "required": [
          "items",
          "total",
          "limit",
          "offset"
        ],
        "title": "ResearchRunListResponse",
        "description": "Paginated list response for GET /research/runs (ADR-0024 \u00a78.2)."
      },
      "ResearchRunSummary": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "vault_id": {
            "type": "string",
            "title": "Vault Id"
          },
          "topic": {
            "type": "string",
            "title": "Topic"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "running | converged | max_iter_reached | budget_exhausted | error"
          },
          "iterations_used": {
            "type": "integer",
            "title": "Iterations Used"
          },
          "sources_fetched": {
            "type": "integer",
            "title": "Sources Fetched"
          },
          "total_cost_usd": {
            "type": "number",
            "title": "Total Cost Usd"
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "title": "Started At"
          },
          "completed_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Completed At"
          }
        },
        "type": "object",
        "required": [
          "id",
          "vault_id",
          "topic",
          "status",
          "iterations_used",
          "sources_fetched",
          "total_cost_usd",
          "started_at"
        ],
        "title": "ResearchRunSummary",
        "description": "One item in GET /research/runs (ADR-0024 \u00a78.2, AC-F10-4b).\n\nMirrors the ingest_runs list shape: id, topic, status, cost, timing."
      },
      "ResearchSourceSummary": {
        "properties": {
          "url": {
            "type": "string",
            "title": "Url"
          },
          "title": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Title"
          },
          "relevance_score": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Relevance Score"
          },
          "iteration": {
            "type": "integer",
            "title": "Iteration"
          }
        },
        "type": "object",
        "required": [
          "url",
          "title",
          "iteration"
        ],
        "title": "ResearchSourceSummary",
        "description": "One source row in GET /research/runs/{id} (ADR-0024 \u00a78.3, AC-F10-6b)."
      },
      "ResearchStartRequest": {
        "properties": {
          "vault_id": {
            "type": "string",
            "title": "Vault Id",
            "description": "Vault scope for the run"
          },
          "topic": {
            "type": "string",
            "minLength": 1,
            "title": "Topic",
            "description": "Research topic (non-empty)"
          },
          "max_iter": {
            "anyOf": [
              {
                "type": "integer",
                "maximum": 10.0,
                "minimum": 1.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Max Iter",
            "description": "Max refinement rounds (1..10); null \u2192 DEEP_RESEARCH_MAX_ITER default"
          },
          "token_budget": {
            "anyOf": [
              {
                "type": "integer",
                "maximum": 1000000.0,
                "minimum": 1000.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Token Budget",
            "description": "Token budget (1_000..1_000_000); null \u2192 DEEP_RESEARCH_TOKEN_BUDGET default"
          },
          "queries": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array",
                "maxItems": 10
              },
              {
                "type": "null"
              }
            ],
            "title": "Queries",
            "description": "Optional caller-supplied SearXNG queries (B5/D3). When present (non-empty) the FIRST iteration uses these VERBATIM instead of the provider query-gen round-trip \u2014 the same seed_queries seam a review item uses (bounded to DEEP_RESEARCH_MAX_QUERIES). This is how the 'optimize + confirm' dialog passes the user-edited queries. null/[] \u2192 generate queries from scratch (default, unchanged behavior)."
          }
        },
        "type": "object",
        "required": [
          "vault_id",
          "topic"
        ],
        "title": "ResearchStartRequest",
        "description": "Request body for POST /research/start (ADR-0024 \u00a78.1, AC-F10-4).\n\nmax_iter and token_budget are optional \u2014 env defaults apply when omitted.\nBoth are FROZEN onto the deep_research_runs row before the background task starts\n(AQ-v0.5-4, I7). Server-side validators cap the range so callers cannot request an\nunbounded run (I7 / Do-NOT #1/#2).",
        "example": {
          "max_iter": 3,
          "queries": [
            "Calico CNI BGP mode",
            "Kubernetes NetworkPolicy Calico"
          ],
          "token_budget": 100000,
          "topic": "Kubernetes networking with Calico",
          "vault_id": "default"
        }
      },
      "ResearchStartResponse": {
        "properties": {
          "run_id": {
            "type": "string",
            "format": "uuid",
            "title": "Run Id",
            "description": "UUID of the created deep_research_runs row"
          }
        },
        "type": "object",
        "required": [
          "run_id"
        ],
        "title": "ResearchStartResponse",
        "description": "202 response for POST /research/start (ADR-0024 \u00a78.1).",
        "example": {
          "run_id": "00000000-0000-0000-0000-000000000001"
        }
      },
      "ReviewBulkRequest": {
        "properties": {
          "vault_id": {
            "type": "string",
            "title": "Vault Id",
            "description": "Vault scope (required)"
          },
          "action": {
            "type": "string",
            "title": "Action",
            "description": "skip | dismiss | mark-resolved (ADR-0044 \u00a76)"
          },
          "ids": {
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "type": "array",
            "title": "Ids",
            "description": "Review item ids to act on; capped at REVIEW_BULK_MAX_IDS (I7 \u2014 400 over)"
          }
        },
        "type": "object",
        "required": [
          "vault_id",
          "action",
          "ids"
        ],
        "title": "ReviewBulkRequest",
        "description": "Request body for POST /review/queue/bulk (ADR-0044 \u00a76).",
        "example": {
          "action": "dismiss",
          "ids": [
            "00000000-0000-0000-0000-000000000001"
          ],
          "vault_id": "default"
        }
      },
      "ReviewBulkResponse": {
        "properties": {
          "updated": {
            "type": "integer",
            "title": "Updated",
            "description": "Pending ids mutated to the new terminal status"
          },
          "skipped_terminal": {
            "type": "integer",
            "title": "Skipped Terminal",
            "description": "Ids that were already terminal (or confirm under mark-resolved); never mutated"
          }
        },
        "type": "object",
        "required": [
          "updated",
          "skipped_terminal"
        ],
        "title": "ReviewBulkResponse",
        "description": "200 response for POST /review/queue/bulk (ADR-0044 \u00a76)."
      },
      "ReviewClearResolvedResponse": {
        "properties": {
          "deleted": {
            "type": "integer",
            "title": "Deleted",
            "description": "Terminal rows hard-deleted for the vault (pending untouched)"
          }
        },
        "type": "object",
        "required": [
          "deleted"
        ],
        "title": "ReviewClearResolvedResponse",
        "description": "200 response for DELETE /review/queue/resolved (ADR-0044 \u00a76)."
      },
      "ReviewDeepResearchResponse": {
        "properties": {
          "review_item_id": {
            "type": "string",
            "format": "uuid",
            "title": "Review Item Id"
          },
          "run_id": {
            "type": "string",
            "format": "uuid",
            "title": "Run Id"
          }
        },
        "type": "object",
        "required": [
          "review_item_id",
          "run_id"
        ],
        "title": "ReviewDeepResearchResponse",
        "description": "202 response for POST /review/queue/{id}/deep-research (ADR-0034 \u00a77).",
        "example": {
          "review_item_id": "00000000-0000-0000-0000-000000000001",
          "run_id": "00000000-0000-0000-0000-000000000002"
        }
      },
      "ReviewItemResponse": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "vault_id": {
            "type": "string",
            "title": "Vault Id"
          },
          "item_type": {
            "type": "string",
            "title": "Item Type",
            "description": "missing-page | suggestion | contradiction | duplicate | confirm | purpose-suggestion"
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "pending | created | skipped | dismissed | deep_researched | auto_resolved"
          },
          "proposed_title": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Proposed Title",
            "description": "Title the LLM proposes to create; drives lazy skeleton (ADR-0034 \u00a75.2)"
          },
          "proposed_page_type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Proposed Page Type",
            "description": "entity|concept|query|source|synthesis|comparison; NULL \u2192 heuristic at Create"
          },
          "proposal_origin": {
            "type": "string",
            "title": "Proposal Origin",
            "description": "rule | ai | corpus | system | lint | legacy (ADR-0073)",
            "default": "legacy"
          },
          "proposed_dir": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Proposed Dir",
            "description": "Target wiki/ subdir (display only; recomputed at Create \u2014 ADR-0034 \u00a75.2)"
          },
          "rationale": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Rationale",
            "description": "Why this matters; used as topic hint for Deep Research (ADR-0034 \u00a73.1)"
          },
          "page_id": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "title": "Page Id",
            "description": "Review TARGET: conflicting/context existing page FK (ADR-0034 \u00a73.1)"
          },
          "page_title": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Page Title",
            "description": "Convenience join from pages.title for page_id (UI display)"
          },
          "source_page_id": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "title": "Source Page Id",
            "description": "Provenance: page whose ingest produced this proposal (ADR-0034 \u00a73.1)"
          },
          "created_page_id": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "title": "Created Page Id",
            "description": "Page produced by a successful Create action (ADR-0034 \u00a75); null otherwise"
          },
          "created_page_type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Created Page Type",
            "description": "Effective type joined from created_page_id; null until/no Create"
          },
          "resolution": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Resolution",
            "description": "created|skipped|dismissed|researched|rule_resolved|llm_resolved; null while pending"
          },
          "deep_research_run_id": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "title": "Deep Research Run Id",
            "description": "FK \u2192 deep_research_runs.id; set when Deep-Research fires (AC-F10-5)"
          },
          "content_key": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Content Key",
            "description": "Stable FNV-1a dedup handle (opaque to UI); NULL for confirm (ADR-0044 \u00a73.2)"
          },
          "referenced_page_ids": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Referenced Page Ids",
            "description": "Array of page-id strings this proposal is contextually about (ADR-0044 \u00a72)"
          },
          "referenced_pages": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/ReferencedPage"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Referenced Pages",
            "description": "Convenience join [{id,title,type}] for referenced_page_ids; stale ids filtered at render (ADR-0044 \u00a76.1/\u00a79.2) so the card renders [[title]] links without a round-trip"
          },
          "search_queries": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Search Queries",
            "description": "\u22643 pre-generated search queries; search_queries[0] seeds Deep Research"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "reviewed_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Reviewed At"
          }
        },
        "type": "object",
        "required": [
          "id",
          "vault_id",
          "item_type",
          "status",
          "created_at"
        ],
        "title": "ReviewItemResponse",
        "description": "API response shape for one review_items row (ADR-0034 \u00a77.1; ADR-0044 \u00a76.1 additions).\n\nProjection carries the full proposal model: type, proposed_title, proposed_page_type,\nproposed_dir, rationale, and the three page FK fields (page_id/source_page_id/created_page_id).\npage_title is a convenience join from pages.title for the page_id FK (UI display).\nresolution records how the item was closed (null while pending).\n\nADR-0044 \u00a76.1 adds: content_key (opaque dedup handle), referenced_page_ids (array),\nreferenced_pages (convenience join, stale ids filtered), search_queries (Deep-Research seeds)."
      },
      "ReviewQueueResponse": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/ReviewItemResponse"
            },
            "type": "array",
            "title": "Items"
          },
          "total": {
            "type": "integer",
            "title": "Total"
          },
          "limit": {
            "type": "integer",
            "title": "Limit"
          },
          "offset": {
            "type": "integer",
            "title": "Offset"
          }
        },
        "type": "object",
        "required": [
          "items",
          "total",
          "limit",
          "offset"
        ],
        "title": "ReviewQueueResponse",
        "description": "Paginated response for GET /review/queue (ADR-0034 \u00a77)."
      },
      "ReviewSweepResponse": {
        "properties": {
          "rule_resolved": {
            "type": "integer",
            "title": "Rule Resolved",
            "description": "Items closed by rule-based Pass-1"
          },
          "llm_resolved": {
            "type": "integer",
            "title": "Llm Resolved",
            "description": "Items closed by conservative LLM Pass-2"
          },
          "kept": {
            "type": "integer",
            "title": "Kept",
            "description": "Items that remain pending after the sweep"
          },
          "corpus_proposed": {
            "type": "integer",
            "title": "Corpus Proposed",
            "description": "Synthesis/comparison suggestions seeded from the graph in Pass-3 (SC-D3)",
            "default": 0
          }
        },
        "type": "object",
        "required": [
          "rule_resolved",
          "llm_resolved",
          "kept"
        ],
        "title": "ReviewSweepResponse",
        "description": "200 response for POST /review/queue/sweep (ADR-0034 \u00a77)."
      },
      "RunNowResponse": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status",
            "description": "'started' \u2014 scan running in background",
            "default": "started"
          }
        },
        "type": "object",
        "title": "RunNowResponse",
        "description": "202 response body for POST /import-schedule/run-now (ADR-0020 \u00a74.6)."
      },
      "SaveToWikiRequest": {
        "properties": {
          "vault_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Vault Id",
            "description": "Defaults to settings.vault_id"
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "title": "Title",
            "description": "Page title (required)"
          },
          "content": {
            "type": "string",
            "minLength": 1,
            "title": "Content",
            "description": "Page content (required)"
          },
          "sources": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Sources",
            "description": "Optional source references to attach to the page frontmatter"
          },
          "conversation_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Conversation Id",
            "description": "Optional conversation UUID for provenance reference"
          }
        },
        "type": "object",
        "required": [
          "title",
          "content"
        ],
        "title": "SaveToWikiRequest",
        "description": "Request body for POST /chat/save-to-wiki (G-P0-1; CG-A2/A5, ADR-0067 P3-3).\n\nSaves a cleaned chat assistant answer to the vault. A lightweight classifier routes an open\nQUESTION to ``wiki/queries/<slug>.md`` (type=query) and any answer/analysis \u2014 the common case\n\u2014 to ``wiki/synthesis/<slug>.md`` (type=synthesis). The write itself calls no inference\nprovider (I6); a bounded fire-and-forget wikilink-enrichment pass may run afterwards to\ngraph-connect the saved page (CG-A3).",
        "example": {
          "content": "bge-m3 is a multi-lingual dense embedding model...",
          "conversation_id": "00000000-0000-0000-0000-000000000001",
          "sources": [
            "raw/sources/chat-123.md"
          ],
          "title": "What is bge-m3?"
        }
      },
      "SaveToWikiResponse": {
        "properties": {
          "page_id": {
            "type": "string",
            "format": "uuid",
            "title": "Page Id",
            "description": "UUID of the created/updated wiki page"
          },
          "file_path": {
            "type": "string",
            "title": "File Path",
            "description": "Relative path in the vault. Usually wiki/synthesis/<slug>.md (an answer/analysis); wiki/queries/<slug>.md when the saved content is itself an open question."
          }
        },
        "type": "object",
        "required": [
          "page_id",
          "file_path"
        ],
        "title": "SaveToWikiResponse",
        "description": "201 response for POST /chat/save-to-wiki (G-P0-1; CG-A2/A5, ADR-0067 P3-3).",
        "example": {
          "file_path": "wiki/synthesis/bge-m3-embedding-tradeoffs.md",
          "page_id": "00000000-0000-0000-0000-000000000001"
        }
      },
      "ScenarioApplyResponse": {
        "properties": {
          "applied": {
            "type": "boolean",
            "title": "Applied"
          }
        },
        "type": "object",
        "required": [
          "applied"
        ],
        "title": "ScenarioApplyResponse",
        "description": "Response for POST /scenarios/{scenario_id}/apply (R7-1)."
      },
      "ScenarioItem": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "description": {
            "type": "string",
            "title": "Description"
          }
        },
        "type": "object",
        "required": [
          "id",
          "name",
          "description"
        ],
        "title": "ScenarioItem",
        "description": "One scenario preset descriptor (R7-1 list response)."
      },
      "ScenarioListResponse": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/ScenarioItem"
            },
            "type": "array",
            "title": "Items"
          }
        },
        "type": "object",
        "required": [
          "items"
        ],
        "title": "ScenarioListResponse",
        "description": "Response for GET /scenarios (R7-1)."
      },
      "SearchResponse": {
        "properties": {
          "query": {
            "type": "string",
            "title": "Query"
          },
          "context": {
            "type": "string",
            "title": "Context",
            "description": "Assembled context string with inline [n] markers (\u2264 token_budget, ADR-0022)"
          },
          "results": {
            "items": {
              "$ref": "#/components/schemas/SearchResultItem"
            },
            "type": "array",
            "title": "Results",
            "description": "Citations in rank order (vector seeds first, then expansions by edge weight)"
          },
          "data_version": {
            "type": "integer",
            "title": "Data Version",
            "description": "Snapshot read BEFORE assembly \u2014 proves the call is read-only (AC-F5-5)"
          },
          "approx_tokens": {
            "type": "integer",
            "title": "Approx Tokens",
            "description": "char/4 estimate of context length"
          },
          "token_budget": {
            "type": "integer",
            "title": "Token Budget",
            "description": "20% of context_window used as the retrieval slice"
          }
        },
        "type": "object",
        "required": [
          "query",
          "context",
          "results",
          "data_version",
          "approx_tokens",
          "token_budget"
        ],
        "title": "SearchResponse",
        "description": "GET /search response (ADR-0022 \u00a72.5, AC-F5-6).\n\nread-only \u2014 never bumps data_version (AC-F5-5).\n0-hit \u2192 200 with empty results + empty context (AC-F5-7a).",
        "example": {
          "approx_tokens": 512,
          "context": "[1] Homelab Setup\nDocker Compose ...\n",
          "data_version": 5,
          "query": "homelab docker services",
          "results": [
            {
              "id": "00000000-0000-0000-0000-000000000001",
              "n": 1,
              "phase": "vector",
              "score": 0.87,
              "slug": "homelab-setup",
              "title": "Homelab Setup"
            }
          ],
          "token_budget": 6553
        }
      },
      "SearchResultItem": {
        "properties": {
          "n": {
            "type": "integer",
            "title": "N",
            "description": "1-based citation index, contiguous from 1"
          },
          "id": {
            "type": "string",
            "title": "Id",
            "description": "UUID of the pages row (== Qdrant point id, ADR-0002)"
          },
          "title": {
            "type": "string",
            "title": "Title",
            "description": "Frontmatter title or filename stem (never empty, \u00a72.6)"
          },
          "slug": {
            "type": "string",
            "title": "Slug",
            "description": "slugify(title) \u2014 derived, not a DB column (\u00a72.6)"
          },
          "score": {
            "type": "number",
            "title": "Score",
            "description": "Cosine similarity (vector) or edge weight (expansion)"
          },
          "phase": {
            "type": "string",
            "title": "Phase",
            "description": "\"vector\" | \"expansion\""
          }
        },
        "type": "object",
        "required": [
          "n",
          "id",
          "title",
          "slug",
          "score",
          "phase"
        ],
        "title": "SearchResultItem",
        "description": "One citation entry in the GET /search response (ADR-0022 \u00a72.5, AC-F5-6).\n\nMaps to Citation.{n, ref.id, ref.title, ref.slug, score, phase}.",
        "example": {
          "id": "00000000-0000-0000-0000-000000000001",
          "n": 1,
          "phase": "vector",
          "score": 0.87,
          "slug": "homelab-setup",
          "title": "Homelab Setup"
        }
      },
      "SourceContentResponse": {
        "properties": {
          "path": {
            "type": "string",
            "title": "Path"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "ext": {
            "type": "string",
            "title": "Ext"
          },
          "size_bytes": {
            "type": "integer",
            "title": "Size Bytes"
          },
          "mtime": {
            "type": "string",
            "title": "Mtime"
          },
          "category": {
            "type": "string",
            "title": "Category"
          },
          "is_text": {
            "type": "boolean",
            "title": "Is Text"
          },
          "text": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Text"
          },
          "ingested": {
            "type": "boolean",
            "title": "Ingested"
          },
          "page_ids": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Page Ids"
          }
        },
        "type": "object",
        "required": [
          "path",
          "name",
          "ext",
          "size_bytes",
          "mtime",
          "category",
          "is_text",
          "ingested",
          "page_ids"
        ],
        "title": "SourceContentResponse",
        "description": "Response for GET /sources/content."
      },
      "SourceDeleteResponse": {
        "properties": {
          "deleted_source": {
            "type": "string",
            "title": "Deleted Source"
          },
          "files_deleted": {
            "type": "integer",
            "title": "Files Deleted",
            "default": 1
          },
          "pages_cascaded": {
            "type": "integer",
            "title": "Pages Cascaded",
            "default": 0
          },
          "pages_deleted": {
            "type": "integer",
            "title": "Pages Deleted",
            "default": 0
          }
        },
        "type": "object",
        "required": [
          "deleted_source"
        ],
        "title": "SourceDeleteResponse",
        "description": "Response for DELETE /sources.\n\nSingle-file delete:  deleted_source=<rel_path>, files_deleted=1, pages_cascaded=N,\n                     pages_deleted=N (backward-compat alias; always equals pages_cascaded).\nDirectory delete:    deleted_source=<rel_dir>,  files_deleted=K, pages_cascaded=M,\n                     pages_deleted=M."
      },
      "SourceDerivedPage": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "title": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Title"
          },
          "page_type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Page Type"
          },
          "file_path": {
            "type": "string",
            "title": "File Path"
          }
        },
        "type": "object",
        "required": [
          "id",
          "file_path"
        ],
        "title": "SourceDerivedPage",
        "description": "One derived page row for GET /sources/derived-pages and GET /sources/content."
      },
      "SourceEntry": {
        "properties": {
          "path": {
            "type": "string",
            "title": "Path"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "is_dir": {
            "type": "boolean",
            "title": "Is Dir"
          },
          "ext": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Ext"
          },
          "size_bytes": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Size Bytes"
          },
          "mtime": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Mtime"
          }
        },
        "type": "object",
        "required": [
          "path",
          "name",
          "is_dir"
        ],
        "title": "SourceEntry",
        "description": "One file or directory entry in the sources tree."
      },
      "SourcesListResponse": {
        "properties": {
          "entries": {
            "items": {
              "$ref": "#/components/schemas/SourceEntry"
            },
            "type": "array",
            "title": "Entries"
          },
          "total": {
            "type": "integer",
            "title": "Total"
          },
          "truncated": {
            "type": "boolean",
            "title": "Truncated"
          }
        },
        "type": "object",
        "required": [
          "entries",
          "total",
          "truncated"
        ],
        "title": "SourcesListResponse",
        "description": "Response for GET /sources."
      },
      "StatusResponse": {
        "properties": {
          "vault_id": {
            "type": "string",
            "title": "Vault Id"
          },
          "data_version": {
            "type": "integer",
            "title": "Data Version"
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "title": "Started At"
          },
          "uptime_seconds": {
            "type": "number",
            "title": "Uptime Seconds"
          },
          "version": {
            "type": "string",
            "title": "Version",
            "description": "Backend package version from pyproject.toml via importlib.metadata (ADR-0054 \u00a76). Read at runtime \u2014 never a hardcoded literal."
          },
          "review_pending": {
            "type": "integer",
            "title": "Review Pending",
            "description": "Count of pending review-queue items \u2014 feeds the NavRail badge via the existing /status poll (no dedicated poller, I3). Additive, non-breaking.",
            "default": 0
          },
          "supports_vision": {
            "type": "boolean",
            "title": "Supports Vision",
            "description": "B2-C1: True when the active chat provider reports capabilities().supports_vision. The frontend uses this to gate the attach-image button. Additive, non-breaking \u2014 defaults False so existing clients without the field read as vision-disabled.",
            "default": false
          }
        },
        "type": "object",
        "required": [
          "vault_id",
          "data_version",
          "started_at",
          "uptime_seconds",
          "version"
        ],
        "title": "StatusResponse",
        "example": {
          "data_version": 3,
          "review_pending": 5,
          "started_at": "2026-06-28T10:00:00Z",
          "supports_vision": false,
          "uptime_seconds": 42.7,
          "vault_id": "default",
          "version": "1.2.0"
        }
      },
      "SynthesizeRequest": {
        "properties": {
          "max_pages": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Max Pages",
            "description": "Cap on pages auto-written this run (synthesis+comparison; clamped)."
          },
          "max_candidates": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Max Candidates",
            "description": "Cap on all evaluated clusters, including Review/skip paths (I7)."
          },
          "token_budget": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Token Budget",
            "description": "Token budget for the run (clamped server-side, I7)."
          },
          "force": {
            "type": "boolean",
            "title": "Force",
            "description": "Regenerate/update the same keyed corpus pages; never create duplicates.",
            "default": false
          },
          "mode": {
            "type": "string",
            "enum": [
              "auto",
              "review-only"
            ],
            "title": "Mode",
            "description": "auto writes high-confidence pages; review-only enqueues eligible clusters.",
            "default": "auto"
          }
        },
        "type": "object",
        "title": "SynthesizeRequest",
        "description": "Request body for POST /ops/synthesize (ADR-0074)."
      },
      "SynthesizeStartResponse": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status",
            "description": "'started' \u2014 synthesize runs in background",
            "default": "started"
          },
          "max_pages": {
            "type": "integer",
            "title": "Max Pages",
            "description": "Effective (clamped) page cap for this run"
          },
          "max_candidates": {
            "type": "integer",
            "title": "Max Candidates",
            "description": "Effective total candidate-evaluation cap"
          },
          "token_budget": {
            "type": "integer",
            "title": "Token Budget",
            "description": "Effective (clamped) token budget for this run"
          },
          "force": {
            "type": "boolean",
            "title": "Force",
            "description": "Echo of the force flag"
          },
          "mode": {
            "type": "string",
            "enum": [
              "auto",
              "review-only"
            ],
            "title": "Mode",
            "description": "Effective run mode"
          }
        },
        "type": "object",
        "required": [
          "max_pages",
          "max_candidates",
          "token_budget",
          "force",
          "mode"
        ],
        "title": "SynthesizeStartResponse",
        "description": "202 response for POST /ops/synthesize."
      },
      "SynthesizeStatusResponse": {
        "properties": {
          "running": {
            "type": "boolean",
            "title": "Running",
            "description": "True while a synthesize run is in flight"
          },
          "current": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Current",
            "description": "Effective bounds/mode of the active run; null while idle"
          },
          "last_summary": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Summary",
            "description": "Summary of the most recent completed run (null if never ran)"
          }
        },
        "type": "object",
        "required": [
          "running"
        ],
        "title": "SynthesizeStatusResponse",
        "description": "GET /ops/synthesize \u2014 single-flight state + last completed summary."
      },
      "SystemUpdateResponse": {
        "properties": {
          "triggered": {
            "type": "boolean",
            "title": "Triggered",
            "description": "True when Watchtower accepted the update request."
          },
          "message": {
            "type": "string",
            "title": "Message",
            "description": "Human-facing status line."
          }
        },
        "type": "object",
        "required": [
          "triggered",
          "message"
        ],
        "title": "SystemUpdateResponse",
        "description": "Result of poking Watchtower to pull+recreate the labelled containers."
      },
      "UpdateStatusResponse": {
        "properties": {
          "current_version": {
            "type": "string",
            "title": "Current Version",
            "description": "Running backend version."
          },
          "latest_version": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Latest Version",
            "description": "Latest published GitHub Release tag (X.Y.Z), or null if unknown."
          },
          "update_available": {
            "type": "boolean",
            "title": "Update Available",
            "description": "True when latest_version is semver-newer than current_version."
          },
          "update_supported": {
            "type": "boolean",
            "title": "Update Supported",
            "description": "True when the Watchtower HTTP API is configured, i.e. POST /ops/system-update can actually trigger an update. When false the UI hides the 'update' button."
          }
        },
        "type": "object",
        "required": [
          "current_version",
          "update_available",
          "update_supported"
        ],
        "title": "UpdateStatusResponse",
        "description": "Deployment update availability (read-only; safe to poll)."
      },
      "UploadResponse": {
        "properties": {
          "file_path": {
            "type": "string",
            "title": "File Path",
            "description": "Saved path relative to vault_root, e.g. \"raw/sources/notes.md\""
          },
          "status": {
            "type": "string",
            "title": "Status",
            "description": "\"queued\" \u2014 file saved to raw/sources/; watcher ingests asynchronously."
          },
          "overwritten": {
            "type": "boolean",
            "title": "Overwritten",
            "description": "True if a same-name file already existed and was replaced on disk"
          }
        },
        "type": "object",
        "required": [
          "file_path",
          "status",
          "overwritten"
        ],
        "title": "UploadResponse",
        "description": "202 response body for POST /ingest/upload (ADR-0020 \u00a72.1, M4-EXT non-blocking).\n\nfile_path:  saved path relative to vault_root (e.g. \"raw/sources/notes.md\")\nstatus:     always \"queued\" \u2014 the watcher picks up the file asynchronously.\noverwritten: true if a same-name file already existed and was replaced on disk.\n\npage_id is not returned because ingest is async (watcher-driven); poll GET /ingest/runs\nor GET /pages to confirm the page exists after ingest completes (~15-30s).",
        "example": {
          "file_path": "raw/sources/notes.md",
          "overwritten": false,
          "status": "queued"
        }
      },
      "ValidationError": {
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "integer"
                }
              ]
            },
            "type": "array",
            "title": "Location"
          },
          "msg": {
            "type": "string",
            "title": "Message"
          },
          "type": {
            "type": "string",
            "title": "Error Type"
          },
          "input": {
            "title": "Input"
          },
          "ctx": {
            "type": "object",
            "title": "Context"
          }
        },
        "type": "object",
        "required": [
          "loc",
          "msg",
          "type"
        ],
        "title": "ValidationError"
      },
      "VaultMetaFile": {
        "properties": {
          "name": {
            "type": "string",
            "title": "Name"
          },
          "path": {
            "type": "string",
            "title": "Path"
          },
          "title": {
            "type": "string",
            "title": "Title"
          },
          "content": {
            "type": "string",
            "title": "Content"
          }
        },
        "type": "object",
        "required": [
          "name",
          "path",
          "title",
          "content"
        ],
        "title": "VaultMetaFile",
        "description": "One vault meta file entry."
      },
      "VaultMetaResponse": {
        "properties": {
          "files": {
            "items": {
              "$ref": "#/components/schemas/VaultMetaFile"
            },
            "type": "array",
            "title": "Files"
          }
        },
        "type": "object",
        "required": [
          "files"
        ],
        "title": "VaultMetaResponse",
        "description": "Response body for GET /vault/meta."
      },
      "VaultMetaWriteRequest": {
        "properties": {
          "content": {
            "type": "string",
            "title": "Content"
          }
        },
        "type": "object",
        "required": [
          "content"
        ],
        "title": "VaultMetaWriteRequest",
        "description": "Request body for PUT /vault/meta/{name} \u2014 the full new file content."
      },
      "VendorInfo": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id",
            "description": "Stable vendor id (catalog key)."
          },
          "display_name": {
            "type": "string",
            "title": "Display Name",
            "description": "Human-readable vendor name for the UI."
          },
          "provider_type": {
            "type": "string",
            "title": "Provider Type",
            "description": "Backend family: api | local | cli (maps to I6 routing)."
          },
          "default_base_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Default Base Url",
            "description": "OpenAI-compatible endpoint the ApiProvider keys off. null = Anthropic-native (anthropic), no HTTP endpoint (local/cli), or must be set per deployment (azure-openai). The user can always override in the row."
          },
          "needs_api_key": {
            "type": "boolean",
            "title": "Needs Api Key",
            "description": "True iff this vendor requires an API key (api vendors); false for local/cli."
          },
          "model_presets": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Model Presets",
            "description": "Known model ids for the dropdown; the user may type a custom id."
          },
          "notes": {
            "type": "string",
            "title": "Notes",
            "description": "Short UI help text for the vendor row.",
            "default": ""
          }
        },
        "type": "object",
        "required": [
          "id",
          "display_name",
          "provider_type",
          "needs_api_key"
        ],
        "title": "VendorInfo",
        "description": "One vendor row in the Settings catalog (W1)."
      },
      "VendorListResponse": {
        "properties": {
          "vendors": {
            "items": {
              "$ref": "#/components/schemas/VendorInfo"
            },
            "type": "array",
            "title": "Vendors",
            "description": "The fixed one-row-per-vendor catalog."
          }
        },
        "type": "object",
        "required": [
          "vendors"
        ],
        "title": "VendorListResponse",
        "description": "Response body for GET /provider/vendors (W1)."
      },
      "WebSearchConfigRequest": {
        "properties": {
          "set_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Set Url",
            "description": "Set the SearXNG base URL in vault_state (DB wins over env). Must be a valid http(s) URL. Set to null to clear the DB URL (falls back to SEARXNG_URL env)."
          },
          "set_categories": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Set Categories",
            "description": "Comma-separated SearXNG categories (e.g. 'general,news'). Empty string \"\" clears to default. Omit to leave unchanged."
          },
          "set_max_queries": {
            "anyOf": [
              {
                "type": "integer",
                "maximum": 50.0,
                "minimum": 1.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Set Max Queries",
            "description": "Max SearXNG queries per deep-research iteration (1\u201350). Omit to leave unchanged."
          },
          "clear": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Clear",
            "description": "If true, clear ALL DB overrides (url, categories, max_queries). After clear, all three fall back to env / code defaults. Applied FIRST; then set_* fields are applied."
          }
        },
        "type": "object",
        "title": "WebSearchConfigRequest",
        "description": "Request body for PUT /web-search/config (ADR-0041 \u00a72.4).\n\nAll fields are optional; omitting a field leaves that aspect unchanged.\nNo provider field \u2014 SearXNG is the ONLY web-search backend (I9).\nPassing any non-SearXNG provider name is rejected with 422 (I9 guard)."
      },
      "WebSearchConfigResponse": {
        "properties": {
          "configured": {
            "type": "boolean",
            "title": "Configured",
            "description": "True iff a SearXNG URL is available (DB or env). POST /research/start returns 503 when false."
          },
          "url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Url",
            "description": "Resolved SearXNG base URL (DB wins over env; ADR-0041 \u00a72.2). None when neither DB nor env is set. NOT a secret \u2014 returned in full (unlike clip/mcp tokens)."
          },
          "categories": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Categories",
            "description": "Resolved SearXNG categories list (DB wins over env/default; ADR-0041 \u00a72.2). Empty list when neither DB nor env sets this \u2014 SearXNG uses its own default."
          },
          "max_queries": {
            "type": "integer",
            "title": "Max Queries",
            "description": "Resolved max SearXNG queries per deep-research iteration (DB wins over DEEP_RESEARCH_MAX_QUERIES env; ADR-0041 \u00a72.2)."
          },
          "source": {
            "type": "string",
            "title": "Source",
            "description": "\"db\" | \"env\" | \"none\" \u2014 which URL source is authoritative (ADR-0041 \u00a72.2). \"db\": URL set via PUT /web-search/config. \"env\": SEARXNG_URL env var. \"none\": no URL configured."
          }
        },
        "type": "object",
        "required": [
          "configured",
          "url",
          "categories",
          "max_queries",
          "source"
        ],
        "title": "WebSearchConfigResponse",
        "description": "Response model for GET /web-search/config (ADR-0041 \u00a72.3).\n\nThe SearXNG URL is NOT a secret \u2014 it IS returned (unlike the clip token).\nsource values: \"db\" | \"env\" | \"none\"."
      },
      "WebSearchConfigStateResponse": {
        "properties": {
          "configured": {
            "type": "boolean",
            "title": "Configured",
            "description": "True iff a SearXNG URL is now available."
          },
          "url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Url",
            "description": "Resolved SearXNG URL post-write (not a secret)."
          },
          "categories": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Categories",
            "description": "Resolved categories list post-write."
          },
          "max_queries": {
            "type": "integer",
            "title": "Max Queries",
            "description": "Resolved max_queries post-write."
          },
          "source": {
            "type": "string",
            "title": "Source",
            "description": "\"db\" | \"env\" | \"none\" \u2014 URL source post-write."
          }
        },
        "type": "object",
        "required": [
          "configured",
          "url",
          "categories",
          "max_queries",
          "source"
        ],
        "title": "WebSearchConfigStateResponse",
        "description": "Response body for PUT /web-search/config (ADR-0041 \u00a72.4).\n\nAlways reflects post-write posture."
      },
      "WebSearchProviderKeyRequest": {
        "properties": {
          "provider": {
            "type": "string",
            "title": "Provider",
            "description": "tavily | serpapi | firecrawl | brave"
          },
          "key": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Key",
            "description": "API key to store; omit when clearing"
          },
          "clear": {
            "type": "boolean",
            "title": "Clear",
            "description": "True to remove the stored key (env resumes)",
            "default": false
          }
        },
        "type": "object",
        "required": [
          "provider"
        ],
        "title": "WebSearchProviderKeyRequest",
        "description": "PUT /web-search/provider-keys \u2014 set (key) or clear (clear=true) one provider's key."
      },
      "WebSearchProviderKeyState": {
        "properties": {
          "configured": {
            "type": "boolean",
            "title": "Configured",
            "description": "True if a key is set (DB or env)"
          },
          "source": {
            "type": "string",
            "title": "Source",
            "description": "'db' | 'env' | 'none'"
          }
        },
        "type": "object",
        "required": [
          "configured",
          "source"
        ],
        "title": "WebSearchProviderKeyState",
        "description": "Masked posture for one cloud web-search provider \u2014 NEVER the key value."
      },
      "WebSearchProviderKeysResponse": {
        "properties": {
          "secrets_available": {
            "type": "boolean",
            "title": "Secrets Available",
            "description": "True if SYNAPSE_SECRET_KEY is set (required to store keys via the UI)"
          },
          "providers": {
            "additionalProperties": {
              "$ref": "#/components/schemas/WebSearchProviderKeyState"
            },
            "type": "object",
            "title": "Providers",
            "description": "Per-provider masked posture (tavily/serpapi/firecrawl/brave)"
          }
        },
        "type": "object",
        "required": [
          "secrets_available",
          "providers"
        ],
        "title": "WebSearchProviderKeysResponse",
        "description": "GET /web-search/provider-keys \u2014 masked posture for all cloud providers."
      },
      "ErrorEnvelope": {
        "type": "object",
        "title": "ErrorEnvelope",
        "required": [
          "error"
        ],
        "description": "Stable error envelope for every error response (ADR-0086).",
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "status",
              "details"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable, machine-readable snake_case error code (e.g. not_found, validation_error). Public contract."
              },
              "message": {
                "type": "string",
                "description": "Human-readable message."
              },
              "status": {
                "type": "integer",
                "description": "HTTP status code."
              },
              "details": {
                "nullable": true,
                "description": "Optional structured payload (field-level validation errors for 422s); null for simple errors."
              }
            }
          }
        }
      }
    },
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer"
      }
    }
  }
}
