diff --git a/backend/app/api/v1/query_sets.py b/backend/app/api/v1/query_sets.py index 77b798c9..846570d2 100644 --- a/backend/app/api/v1/query_sets.py +++ b/backend/app/api/v1/query_sets.py @@ -107,11 +107,12 @@ async def _detail(db: AsyncSession, row: QuerySet) -> QuerySetDetail: ) -def _summary(row: QuerySet) -> QuerySetSummary: +def _summary(row: QuerySet, query_count: int) -> QuerySetSummary: return QuerySetSummary( id=row.id, name=row.name, cluster_id=row.cluster_id, + query_count=query_count, created_at=row.created_at, ) @@ -200,8 +201,11 @@ async def list_query_sets( cursor_value = getattr(last, parsed_sort.col_name) next_cursor = _sort_encode_cursor(cursor_value, last.id) has_more = True + # One batched GROUP BY aggregate for the whole page — no per-row + # count (see QuerySetSummary docstring + repo.count_queries_for_sets). + counts = await repo.count_queries_for_sets(db, [r.id for r in rows]) return QuerySetListResponse( - data=[_summary(r) for r in rows], + data=[_summary(r, counts.get(r.id, 0)) for r in rows], next_cursor=next_cursor, has_more=has_more, ) diff --git a/backend/app/api/v1/query_templates.py b/backend/app/api/v1/query_templates.py index 5c3532d9..cbe33f12 100644 --- a/backend/app/api/v1/query_templates.py +++ b/backend/app/api/v1/query_templates.py @@ -111,6 +111,9 @@ def _summary(row: QueryTemplate) -> QueryTemplateSummary: name=row.name, engine_type=row.engine_type, version=row.version, + # declared_params is a JSONB column already loaded on the row, so + # len() is free — no extra query, no N+1 (see QueryTemplateSummary). + param_count=len(row.declared_params), created_at=row.created_at, ) diff --git a/backend/app/api/v1/schemas.py b/backend/app/api/v1/schemas.py index b00e3040..400fdc04 100644 --- a/backend/app/api/v1/schemas.py +++ b/backend/app/api/v1/schemas.py @@ -467,12 +467,21 @@ class QueryTemplateDetail(BaseModel): class QueryTemplateSummary(BaseModel): - """List-view shape; drops ``body`` + ``declared_params`` for brevity.""" + """List-view shape; drops ``body`` + the full ``declared_params`` dict. + + Surfaces ``param_count`` (= ``len(declared_params)``) so the + templates list can show each template's tuning surface at a glance. + ``param_count`` is free to compute — ``declared_params`` is a JSONB + column already loaded on the row (not a child relationship), so the + count is ``len(row.declared_params)`` with no extra query and no + N+1 risk. The full dict remains on ``QueryTemplateDetail``. + """ id: str name: str engine_type: EngineTypeWire version: int + param_count: int created_at: datetime @@ -513,11 +522,21 @@ class QuerySetDetail(BaseModel): class QuerySetSummary(BaseModel): - """List-view shape; omits ``query_count`` to avoid N+1 counts at list time.""" + """List-view shape. + + ``query_count`` is the number of queries in the set. It is resolved + via a single batched ``GROUP BY query_set_id`` aggregate per page + (``repo.count_queries_for_sets``), NOT a per-row count — so the + list endpoint stays at a fixed 2 queries (the page + the count + aggregate) regardless of page size. This is the same no-N+1 pattern + ``feat_studies_convergence_visibility`` (PR #421) used for the + studies-list ``trial_count`` field. + """ id: str name: str cluster_id: str + query_count: int created_at: datetime diff --git a/backend/app/db/repo/__init__.py b/backend/app/db/repo/__init__.py index c823e8f4..f8cefcf1 100644 --- a/backend/app/db/repo/__init__.py +++ b/backend/app/db/repo/__init__.py @@ -107,6 +107,7 @@ update_query, ) from backend.app.db.repo.query_set import ( + count_queries_for_sets, count_queries_in_set, count_query_sets, create_query_set, @@ -183,6 +184,7 @@ "TrialsSummary", "aggregate_trials_summary", "bulk_create_queries", + "count_queries_for_sets", "count_queries_in_set", "count_query_sets", "count_query_templates", diff --git a/backend/app/db/repo/query_set.py b/backend/app/db/repo/query_set.py index d95f3d91..24c7434b 100644 --- a/backend/app/db/repo/query_set.py +++ b/backend/app/db/repo/query_set.py @@ -103,6 +103,40 @@ async def count_queries_in_set(db: AsyncSession, query_set_id: str) -> int: return int((await db.execute(stmt)).scalar_one()) +async def count_queries_for_sets(db: AsyncSession, query_set_ids: Sequence[str]) -> dict[str, int]: + """Batched query counts for a page of query sets. + + One ``GROUP BY query_set_id`` aggregate returning the COUNT(*) of + queries per set. Powers the query-sets-list ``query_count`` field + without a per-row count (the no-N+1 pattern mirroring + ``repo.count_trials_for_studies`` from + ``feat_studies_convergence_visibility``). + + Sets whose id is in the input but have zero queries are returned + with ``0`` (backfilled) so callers can index by id without a + ``KeyError``. Empty input returns an empty dict (no query issued). + """ + if not query_set_ids: + return {} + # Label the aggregate ``query_count`` (NOT ``count``): SQLAlchemy + # ``Row`` objects are tuple-like and expose a built-in ``.count()`` + # method, so ``row.count`` would resolve to that bound method, not + # the labeled column. ``query_count`` has no such collision. + stmt = ( + select( + Query.query_set_id.label("query_set_id"), + func.count(Query.id).label("query_count"), + ) + .where(Query.query_set_id.in_(list(query_set_ids))) + .group_by(Query.query_set_id) + ) + rows = (await db.execute(stmt)).all() + result: dict[str, int] = {row.query_set_id: int(row.query_count) for row in rows} + for qsid in query_set_ids: + result.setdefault(qsid, 0) + return result + + # --------------------------------------------------------------------------- # chore_e2e_test_rows_isolation Story 1.1 — hard-delete for test-only cleanup # --------------------------------------------------------------------------- diff --git a/backend/tests/contract/test_list_count_fields_contract.py b/backend/tests/contract/test_list_count_fields_contract.py new file mode 100644 index 00000000..00a5bca8 --- /dev/null +++ b/backend/tests/contract/test_list_count_fields_contract.py @@ -0,0 +1,71 @@ +# SPDX-FileCopyrightText: 2026 soundminds.ai +# +# SPDX-License-Identifier: Apache-2.0 + +"""Contract assertions for the list-summary count fields (feat_list_count_columns). + +Asserts the OpenAPI schema documents: + +* ``QuerySetSummary.query_count`` as a required integer. +* ``QueryTemplateSummary.param_count`` as a required integer. + +These guard the wire contract the frontend's generated ``types.ts`` +consumes — if a future edit drops either field from the summary model, +the freshness gate would catch the snapshot drift but this test pins the +*shape* (type + required-ness) explicitly. +""" + +from __future__ import annotations + +from collections.abc import AsyncIterator + +import httpx +import pytest +import pytest_asyncio +from asgi_lifespan import LifespanManager + +from backend.tests.conftest import postgres_reachable + +_skip_if_no_pg = pytest.mark.skipif( + not postgres_reachable(), + reason="Postgres not reachable — router resolves get_db at boot", +) + + +@pytest_asyncio.fixture +async def async_client() -> AsyncIterator[httpx.AsyncClient]: + from backend.app.main import app + + async with LifespanManager(app): + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=app), + base_url="http://test", + timeout=30.0, + ) as client: + yield client + + +@_skip_if_no_pg +async def test_query_set_summary_documents_query_count( + async_client: httpx.AsyncClient, +) -> None: + resp = await async_client.get("/openapi.json") + assert resp.status_code == 200 + schema = resp.json()["components"]["schemas"]["QuerySetSummary"] + props = schema["properties"] + assert "query_count" in props, "QuerySetSummary missing query_count" + assert props["query_count"]["type"] == "integer" + assert "query_count" in schema["required"], "query_count must be required" + + +@_skip_if_no_pg +async def test_query_template_summary_documents_param_count( + async_client: httpx.AsyncClient, +) -> None: + resp = await async_client.get("/openapi.json") + assert resp.status_code == 200 + schema = resp.json()["components"]["schemas"]["QueryTemplateSummary"] + props = schema["properties"] + assert "param_count" in props, "QueryTemplateSummary missing param_count" + assert props["param_count"]["type"] == "integer" + assert "param_count" in schema["required"], "param_count must be required" diff --git a/backend/tests/integration/test_list_count_fields.py b/backend/tests/integration/test_list_count_fields.py new file mode 100644 index 00000000..632d5509 --- /dev/null +++ b/backend/tests/integration/test_list_count_fields.py @@ -0,0 +1,168 @@ +# SPDX-FileCopyrightText: 2026 soundminds.ai +# +# SPDX-License-Identifier: Apache-2.0 + +"""Integration tests for the list-summary count fields (feat_list_count_columns). + +Two additions to the list endpoints: + +* ``GET /api/v1/query-sets`` items carry ``query_count`` — the number of + queries in the set, resolved via one batched ``GROUP BY`` aggregate per + page (``repo.count_queries_for_sets``), NOT a per-row count. +* ``GET /api/v1/query-templates`` items carry ``param_count`` — + ``len(declared_params)``, free off the already-loaded JSONB column. + +All assertions go through the real FastAPI request pipeline via +``async_client`` so the Pydantic response model + serialization are +exercised end-to-end against a real Postgres. +""" + +from __future__ import annotations + +import uuid +from typing import Any + +import httpx +import pytest +import uuid_utils + +from backend.app.db import repo +from backend.app.db.session import get_session_factory +from backend.tests.conftest import postgres_reachable + +pytestmark = [ + pytest.mark.integration, + pytest.mark.skipif( + not postgres_reachable(), + reason="Postgres not reachable — see docs/03_runbooks/local-dev.md", + ), +] + + +async def _seed_query_set(num_queries: int) -> str: + """Seed cluster → query_set → N queries; return the query_set id.""" + factory = get_session_factory() + async with factory() as db: + cluster = await repo.create_cluster( + db, + id=str(uuid.uuid4()), + name=f"lcc-c-{uuid.uuid4().hex[:8]}", + engine_type="elasticsearch", + environment="dev", + base_url="http://stub:9200", + auth_kind="es_basic", + credentials_ref="ref", + ) + qs = await repo.create_query_set( + db, + id=str(uuid_utils.uuid7()), + name=f"lcc-qs-{uuid.uuid4().hex[:8]}", + cluster_id=cluster.id, + ) + for i in range(num_queries): + await repo.create_query( + db, + id=str(uuid_utils.uuid7()), + query_set_id=qs.id, + query_text=f"q-{i}", + reference_answer=None, + query_metadata=None, + ) + await db.commit() + return str(qs.id) + + +async def _seed_template(declared_params: dict[str, str]) -> str: + """Seed a query_template with the given declared_params; return its id.""" + factory = get_session_factory() + async with factory() as db: + tmpl = await repo.create_query_template( + db, + id=str(uuid_utils.uuid7()), + name=f"lcc-tmpl-{uuid.uuid4().hex[:8]}", + engine_type="elasticsearch", + body='{"query": {"match_all": {}}}', + declared_params=declared_params, + version=1, + parent_id=None, + ) + await db.commit() + return str(tmpl.id) + + +def _find_item(items: list[dict[str, Any]], item_id: str) -> dict[str, Any]: + for it in items: + if it["id"] == item_id: + return it + raise AssertionError(f"id {item_id} not found in list response") + + +# --------------------------------------------------------------------------- +# query-sets: query_count +# --------------------------------------------------------------------------- + + +async def test_query_sets_list_includes_query_count( + async_client: httpx.AsyncClient, +) -> None: + """A set with 3 queries reports query_count == 3 in the list.""" + set_id = await _seed_query_set(num_queries=3) + resp = await async_client.get("/api/v1/query-sets", params={"limit": 200}) + assert resp.status_code == 200, resp.text + item = _find_item(resp.json()["data"], set_id) + assert item["query_count"] == 3 + + +async def test_query_sets_list_zero_queries_reports_zero( + async_client: httpx.AsyncClient, +) -> None: + """A set with no queries reports query_count == 0 (backfilled, not missing).""" + set_id = await _seed_query_set(num_queries=0) + resp = await async_client.get("/api/v1/query-sets", params={"limit": 200}) + assert resp.status_code == 200, resp.text + item = _find_item(resp.json()["data"], set_id) + assert item["query_count"] == 0 + + +async def test_query_sets_list_counts_are_per_set( + async_client: httpx.AsyncClient, +) -> None: + """Two sets with different cardinalities each get their own count. + + Guards the batched ``GROUP BY`` mapping — a regression that collapsed + all sets to one count (or mis-keyed the dict) would fail here. + """ + five_id = await _seed_query_set(num_queries=5) + one_id = await _seed_query_set(num_queries=1) + resp = await async_client.get("/api/v1/query-sets", params={"limit": 200}) + assert resp.status_code == 200, resp.text + items = resp.json()["data"] + assert _find_item(items, five_id)["query_count"] == 5 + assert _find_item(items, one_id)["query_count"] == 1 + + +# --------------------------------------------------------------------------- +# query-templates: param_count +# --------------------------------------------------------------------------- + + +async def test_templates_list_includes_param_count( + async_client: httpx.AsyncClient, +) -> None: + """A template with 3 declared params reports param_count == 3.""" + tmpl_id = await _seed_template({"a": "term", "b": "term", "c": "term"}) + resp = await async_client.get("/api/v1/query-templates", params={"limit": 200}) + assert resp.status_code == 200, resp.text + item = _find_item(resp.json()["data"], tmpl_id) + assert item["param_count"] == 3 + + +async def test_templates_list_zero_params_reports_zero( + async_client: httpx.AsyncClient, +) -> None: + """A template with no declared params reports param_count == 0.""" + tmpl_id = await _seed_template({}) + resp = await async_client.get("/api/v1/query-templates", params={"limit": 200}) + assert resp.status_code == 200, resp.text + item = _find_item(resp.json()["data"], tmpl_id) + assert item["param_count"] == 0 diff --git a/ui/openapi.json b/ui/openapi.json index 54c8d9c5..381f00ac 100644 --- a/ui/openapi.json +++ b/ui/openapi.json @@ -1 +1 @@ -{"components":{"schemas":{"BulkQueriesResponse":{"description":"``POST /api/v1/query-sets/{id}/queries`` response.","properties":{"added":{"title":"Added","type":"integer"}},"required":["added"],"title":"BulkQueriesResponse","type":"object"},"CIShape":{"description":"Bootstrap percentile CI on the winner's per-query metric values.","properties":{"high":{"title":"High","type":"number"},"low":{"title":"Low","type":"number"},"method":{"const":"bootstrap_n1000","title":"Method","type":"string"},"n_samples":{"title":"N Samples","type":"integer"}},"required":["low","high","method","n_samples"],"title":"CIShape","type":"object"},"CalibrationResponse":{"description":"Calibration endpoint response.\n\nMirrors :class:`backend.app.eval.calibration.CalibrationResult` —\npersisted as ``judgment_lists.calibration`` JSONB.","properties":{"cohens_kappa":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cohens Kappa"},"n_samples":{"title":"N Samples","type":"integer"},"per_class":{"additionalProperties":{"type":"number"},"title":"Per Class","type":"object"},"warning":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Warning"},"weighted_kappa":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Weighted Kappa"}},"required":["cohens_kappa","weighted_kappa","per_class","n_samples","warning"],"title":"CalibrationResponse","type":"object"},"CalibrationSample":{"description":"One row in :class:`CalibrationSamplesRequest`.","properties":{"doc_id":{"maxLength":512,"minLength":1,"title":"Doc Id","type":"string"},"query_id":{"maxLength":36,"minLength":1,"title":"Query Id","type":"string"},"rating":{"enum":[0,1,2,3],"title":"Rating","type":"integer"}},"required":["query_id","doc_id","rating"],"title":"CalibrationSample","type":"object"},"CalibrationSamplesRequest":{"description":"Body for ``POST /api/v1/judgment-lists/{id}/calibration`` (Story 3.5).","properties":{"human_samples":{"items":{"$ref":"#/components/schemas/CalibrationSample"},"minItems":1,"title":"Human Samples","type":"array"}},"required":["human_samples"],"title":"CalibrationSamplesRequest","type":"object"},"CategoricalParam":{"additionalProperties":false,"description":"Discrete choice parameter.\n\nOptuna ``suggest_categorical`` handles strings, ints, floats, and bools\nas choices.","properties":{"choices":{"items":{"anyOf":[{"type":"string"},{"type":"integer"},{"type":"number"},{"type":"boolean"}]},"minItems":1,"title":"Choices","type":"array"},"type":{"const":"categorical","title":"Type","type":"string"}},"required":["type","choices"],"title":"CategoricalParam","type":"object"},"ClusterAggregateHealth":{"description":"Aggregate counts for the ``elasticsearch_clusters`` /healthz field (Story 3.5).\n\nPer spec §2: probes only the *registered* user clusters (from the DB),\nNOT the local Compose ES/OpenSearch — those have their own subsystem\nfields. ``status`` is a count derived from the cached ``cluster:health:*``\nentries; missing-cache or red/unreachable clusters are counted as\n``unreachable``.","properties":{"healthy":{"title":"Healthy","type":"integer"},"registered":{"title":"Registered","type":"integer"},"unreachable":{"title":"Unreachable","type":"integer"}},"required":["registered","healthy","unreachable"],"title":"ClusterAggregateHealth","type":"object"},"ClusterDetail":{"description":"``GET /api/v1/clusters/{id}`` response.","properties":{"auth_kind":{"enum":["es_apikey","es_basic","opensearch_basic","opensearch_sigv4","solr_basic","solr_apikey"],"title":"Auth Kind","type":"string"},"base_url":{"title":"Base Url","type":"string"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"engine_config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Engine Config"},"engine_type":{"enum":["elasticsearch","opensearch","solr"],"title":"Engine Type","type":"string"},"environment":{"enum":["prod","staging","dev"],"title":"Environment","type":"string"},"health_check":{"$ref":"#/components/schemas/HealthCheckResult"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"target_filter":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Filter"}},"required":["id","name","engine_type","environment","base_url","auth_kind","created_at","health_check"],"title":"ClusterDetail","type":"object"},"ClusterListResponse":{"description":"Paginated list response.","properties":{"data":{"items":{"$ref":"#/components/schemas/ClusterSummary"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"ClusterListResponse","type":"object"},"ClusterSummary":{"description":"List-view; drops engine_config + notes for brevity.","properties":{"auth_kind":{"enum":["es_apikey","es_basic","opensearch_basic","opensearch_sigv4","solr_basic","solr_apikey"],"title":"Auth Kind","type":"string"},"base_url":{"title":"Base Url","type":"string"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"engine_type":{"enum":["elasticsearch","opensearch","solr"],"title":"Engine Type","type":"string"},"environment":{"enum":["prod","staging","dev"],"title":"Environment","type":"string"},"health_check":{"$ref":"#/components/schemas/HealthCheckResult"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"target_filter":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Filter"}},"required":["id","name","engine_type","environment","base_url","auth_kind","created_at","health_check"],"title":"ClusterSummary","type":"object"},"ConfidenceShape":{"description":"The top-level shape exposed via ``StudyDetail.confidence``.\n\nEvery sub-field is independently nullable per FR-7 — degraded paths\nsuppress only the sub-fields they affect, never the whole shape (the\norchestrator returns whole-object ``None`` only when the winner trial\nrow itself is missing).","properties":{"ci_95":{"anyOf":[{"$ref":"#/components/schemas/CIShape"},{"type":"null"}]},"convergence":{"anyOf":[{"$ref":"#/components/schemas/ConvergenceShape"},{"type":"null"}]},"headline":{"$ref":"#/components/schemas/HeadlineShape"},"late_trial_stddev":{"anyOf":[{"$ref":"#/components/schemas/LateTrialStddevShape"},{"type":"null"}]},"per_query_outcomes":{"anyOf":[{"$ref":"#/components/schemas/PerQueryOutcomesShape"},{"type":"null"}]},"runner_up_gap":{"anyOf":[{"$ref":"#/components/schemas/RunnerUpGapShape"},{"type":"null"}]}},"required":["headline","ci_95","runner_up_gap","late_trial_stddev","convergence","per_query_outcomes"],"title":"ConfidenceShape","type":"object"},"ConfigRepoDetail":{"description":"``GET /api/v1/config-repos/{id}`` response + ``POST`` 201 body.","properties":{"auth_ref":{"title":"Auth Ref","type":"string"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"default_branch":{"title":"Default Branch","type":"string"},"id":{"title":"Id","type":"string"},"last_merged_proposal":{"anyOf":[{"$ref":"#/components/schemas/ProposalSummary"},{"type":"null"}]},"name":{"title":"Name","type":"string"},"pr_base_branch":{"title":"Pr Base Branch","type":"string"},"provider":{"const":"github","title":"Provider","type":"string"},"repo_url":{"title":"Repo Url","type":"string"},"webhook_registration_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Webhook Registration Error"},"webhook_secret_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Webhook Secret Ref"}},"required":["id","name","provider","repo_url","default_branch","pr_base_branch","auth_ref","webhook_secret_ref","webhook_registration_error","created_at"],"title":"ConfigRepoDetail","type":"object"},"ConfigReposListResponse":{"description":"``GET /api/v1/config-repos`` response.","properties":{"data":{"items":{"$ref":"#/components/schemas/ConfigRepoDetail"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"ConfigReposListResponse","type":"object"},"ConnectionTestRequest":{"description":"Body for ``POST /api/v1/clusters/test-connection`` (infra_adapter_solr Story A9).\n\nSame shape as ``CreateClusterRequest`` minus the persisted-only fields\n(``name``, ``environment``, ``notes``, ``target_filter``). ``engine_type``\n+ ``auth_kind`` are typed as ``str`` (not Literal) so a bad value yields\nthe project-standard 400 envelope rather than a raw 422 — same convention\nas ``CreateClusterRequest``.","properties":{"auth_kind":{"maxLength":64,"minLength":1,"title":"Auth Kind","type":"string"},"base_url":{"maxLength":512,"minLength":1,"title":"Base Url","type":"string"},"credentials_ref":{"maxLength":128,"minLength":1,"title":"Credentials Ref","type":"string"},"engine_config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Engine Config"},"engine_type":{"maxLength":64,"minLength":1,"title":"Engine Type","type":"string"}},"required":["engine_type","base_url","auth_kind","credentials_ref"],"title":"ConnectionTestRequest","type":"object"},"ConnectionTestResult":{"description":"Response for ``POST /api/v1/clusters/test-connection``.\n\nAlways 200 — reachable vs unreachable surfaces via ``reachable`` +\n``status`` fields. The endpoint is a diagnostic, never a mutation,\nso it never returns 503; invalid engine×auth pairings 400 BEFORE the\nnetwork call. (Cycle-delta F1.)","properties":{"engine_capabilities":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Engine Capabilities"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"reachable":{"title":"Reachable","type":"boolean"},"status":{"enum":["green","yellow","red","unreachable"],"title":"Status","type":"string"},"version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Version"}},"required":["reachable","status"],"title":"ConnectionTestResult","type":"object"},"ConvergenceShape":{"description":"Where the winner sits in the Optuna trial sequence + the classified regime.","properties":{"best_at_trial":{"title":"Best At Trial","type":"integer"},"regime":{"enum":["early_held","late_rising","noisy"],"title":"Regime","type":"string"},"total_trials":{"title":"Total Trials","type":"integer"}},"required":["best_at_trial","total_trials","regime"],"title":"ConvergenceShape","type":"object"},"ConversationDetail":{"description":"``GET /api/v1/conversations/{id}`` response.","properties":{"created_at":{"format":"date-time","title":"Created At","type":"string"},"id":{"title":"Id","type":"string"},"messages":{"items":{"$ref":"#/components/schemas/MessageWire"},"title":"Messages","type":"array"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"}},"required":["id","title","created_at","messages"],"title":"ConversationDetail","type":"object"},"ConversationSummary":{"description":"``GET /api/v1/conversations`` row + ``POST`` 201 body.\n\n``last_message_preview`` is the most recent user / assistant message's\n``content.text``, truncated at the repo layer to 120 chars (with ``…``\nsuffix when cut). Tool-role rows and assistant rows whose ``content.kind``\nis ``system_notice`` are skipped. ``None`` for brand-new conversations\nwith no qualifying messages — see ``chore_chat_last_message_preview``.\n\n``last_message_at`` is the ``created_at`` of that same row, or ``None``\nfor empty conversations. The list page uses it to render \"when did\nanyone last touch this thread\" instead of the conversation's\n``created_at``.","properties":{"created_at":{"format":"date-time","title":"Created At","type":"string"},"id":{"title":"Id","type":"string"},"last_message_at":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Last Message At"},"last_message_preview":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Message Preview"},"message_count":{"title":"Message Count","type":"integer"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"}},"required":["id","title","created_at","message_count"],"title":"ConversationSummary","type":"object"},"ConversationsListResponse":{"description":"``GET /api/v1/conversations`` response.","properties":{"data":{"items":{"$ref":"#/components/schemas/ConversationSummary"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"ConversationsListResponse","type":"object"},"CreateClusterRequest":{"description":"Request body for ``POST /api/v1/clusters``.\n\nSee module docstring for the deliberate ``str`` vs ``Literal`` split.","properties":{"auth_kind":{"maxLength":64,"minLength":1,"title":"Auth Kind","type":"string"},"base_url":{"maxLength":512,"minLength":1,"title":"Base Url","type":"string"},"credentials_ref":{"maxLength":128,"minLength":1,"title":"Credentials Ref","type":"string"},"engine_config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Engine Config"},"engine_type":{"maxLength":64,"minLength":1,"title":"Engine Type","type":"string"},"environment":{"enum":["prod","staging","dev"],"title":"Environment","type":"string"},"name":{"maxLength":128,"minLength":1,"pattern":"^[a-z0-9][a-z0-9-]*$","title":"Name","type":"string"},"notes":{"anyOf":[{"maxLength":2000,"type":"string"},{"type":"null"}],"title":"Notes"},"target_filter":{"anyOf":[{"maxLength":256,"minLength":1,"type":"string"},{"type":"null"}],"description":"Optional glob pattern (fnmatch.fnmatchcase: *, ?, [seq], [!seq]; no brace expansion). Scopes GET /clusters/{id}/targets to matching index names. Null = no filter.","title":"Target Filter"}},"required":["name","engine_type","environment","base_url","auth_kind","credentials_ref"],"title":"CreateClusterRequest","type":"object"},"CreateConfigRepoRequest":{"description":"Body of ``POST /api/v1/config-repos`` (FR-3).\n\n``provider`` is server-derived from ``repo_url`` (cycle-2 F4 from\nspec review) — NOT in the payload. The validator enforces a strict\nGitHub URL pattern; non-GitHub URLs surface as 400\n``UNSUPPORTED_PROVIDER`` at the router layer.","properties":{"auth_ref":{"maxLength":128,"minLength":1,"pattern":"^[a-zA-Z0-9_-]+$","title":"Auth Ref","type":"string"},"default_branch":{"default":"main","maxLength":128,"minLength":1,"title":"Default Branch","type":"string"},"name":{"maxLength":128,"minLength":1,"pattern":"^[a-z0-9][a-z0-9-]*$","title":"Name","type":"string"},"pr_base_branch":{"default":"main","maxLength":128,"minLength":1,"title":"Pr Base Branch","type":"string"},"repo_url":{"maxLength":512,"minLength":1,"title":"Repo Url","type":"string"},"webhook_secret_ref":{"anyOf":[{"maxLength":128,"pattern":"^[a-zA-Z0-9_-]+$","type":"string"},{"type":"null"}],"title":"Webhook Secret Ref"}},"required":["name","repo_url","auth_ref"],"title":"CreateConfigRepoRequest","type":"object"},"CreateConversationRequest":{"description":"``POST /api/v1/conversations`` body.","properties":{"title":{"anyOf":[{"maxLength":200,"type":"string"},{"type":"null"}],"title":"Title"}},"title":"CreateConversationRequest","type":"object"},"CreateJudgmentListFromUbiRequest":{"description":"Body for ``POST /api/v1/judgments/generate-from-ubi`` (Story 3.2 / FR-3).\n\nMirrors :class:`backend.app.services.agent_judgments_dispatch.UbiJudgmentGenerationRequest`.\nThe ``@model_validator(mode=\"after\")`` enforces the conditional\nrequiredness of ``current_template_id`` + ``rubric`` per the hybrid\nconverter: REQUIRED when ``converter == 'hybrid_ubi_llm'`` (the LLM-\nfill path needs both); FORBIDDEN otherwise (pure UBI never calls\nthe LLM so accepting them silently would mask operator error).","properties":{"cluster_id":{"maxLength":36,"minLength":1,"title":"Cluster Id","type":"string"},"converter":{"enum":["ctr_threshold","dwell_time","hybrid_ubi_llm"],"title":"Converter","type":"string"},"converter_config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Converter Config"},"current_template_id":{"anyOf":[{"maxLength":36,"minLength":36,"type":"string"},{"type":"null"}],"title":"Current Template Id"},"description":{"anyOf":[{"maxLength":2000,"type":"string"},{"type":"null"}],"title":"Description"},"llm_fill_threshold":{"anyOf":[{"minimum":1.0,"type":"integer"},{"type":"null"}],"default":20,"title":"Llm Fill Threshold"},"mapping_strategy":{"default":"reject","enum":["reject","first_match","most_recent"],"title":"Mapping Strategy","type":"string"},"min_impressions_threshold":{"anyOf":[{"minimum":1.0,"type":"integer"},{"type":"null"}],"default":100,"title":"Min Impressions Threshold"},"name":{"maxLength":256,"minLength":1,"title":"Name","type":"string"},"query_set_id":{"maxLength":36,"minLength":1,"title":"Query Set Id","type":"string"},"rubric":{"anyOf":[{"minLength":1,"type":"string"},{"type":"null"}],"title":"Rubric"},"since":{"format":"date-time","title":"Since","type":"string"},"target":{"maxLength":256,"minLength":1,"title":"Target","type":"string"},"until":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Until"}},"required":["name","query_set_id","cluster_id","target","since","converter"],"title":"CreateJudgmentListFromUbiRequest","type":"object"},"CreateJudgmentListGenerateRequest":{"description":"Body for ``POST /api/v1/judgments/generate`` (Story 3.1).","properties":{"cluster_id":{"maxLength":36,"minLength":1,"title":"Cluster Id","type":"string"},"current_template_id":{"maxLength":36,"minLength":1,"title":"Current Template Id","type":"string"},"description":{"anyOf":[{"maxLength":2000,"type":"string"},{"type":"null"}],"title":"Description"},"name":{"maxLength":256,"minLength":1,"title":"Name","type":"string"},"query_set_id":{"maxLength":36,"minLength":1,"title":"Query Set Id","type":"string"},"rubric":{"minLength":1,"title":"Rubric","type":"string"},"target":{"maxLength":256,"minLength":1,"title":"Target","type":"string"}},"required":["name","query_set_id","cluster_id","target","current_template_id","rubric"],"title":"CreateJudgmentListGenerateRequest","type":"object"},"CreateProposalRequest":{"description":"Body of ``POST /api/v1/proposals`` (manual proposal creation, FR-4 / AC-6).","properties":{"cluster_id":{"maxLength":36,"minLength":1,"title":"Cluster Id","type":"string"},"config_diff":{"additionalProperties":true,"title":"Config Diff","type":"object"},"metric_delta":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Metric Delta"},"template_id":{"maxLength":36,"minLength":1,"title":"Template Id","type":"string"}},"required":["cluster_id","template_id","config_diff"],"title":"CreateProposalRequest","type":"object"},"CreateQuerySetRequest":{"description":"``POST /api/v1/query-sets`` body.\n\n``cluster_id`` is required because Phase 1's shipped schema has\n``query_sets.cluster_id NOT NULL``. Spec FR-3 wording (``cluster_id?``)\nis documented drift tracked at\n``docs/00_overview/planned_features/chore_spec_query_set_cluster_id_drift/idea.md``.","properties":{"cluster_id":{"maxLength":36,"minLength":1,"title":"Cluster Id","type":"string"},"description":{"anyOf":[{"maxLength":2000,"type":"string"},{"type":"null"}],"title":"Description"},"name":{"maxLength":256,"minLength":1,"title":"Name","type":"string"}},"required":["name","cluster_id"],"title":"CreateQuerySetRequest","type":"object"},"CreateQueryTemplateRequest":{"description":"Request body for ``POST /api/v1/query-templates``.","properties":{"body":{"minLength":1,"title":"Body","type":"string"},"declared_params":{"additionalProperties":{"type":"string"},"title":"Declared Params","type":"object"},"engine_type":{"enum":["elasticsearch","opensearch","solr"],"title":"Engine Type","type":"string"},"name":{"maxLength":256,"minLength":1,"title":"Name","type":"string"},"parent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Id"}},"required":["name","engine_type","body"],"title":"CreateQueryTemplateRequest","type":"object"},"CreateStudyRequest":{"description":"``POST /api/v1/studies`` body.\n\n``search_space`` is validated post-Pydantic-parse via\n:class:`backend.app.domain.study.search_space.SearchSpace` so\n:exc:`pydantic.ValidationError` produces the spec's 400\n``INVALID_SEARCH_SPACE`` (per Story 3.3 task 2).\n\nfeat_digest_executable_followups Story 4.2 — optional ``parent`` field\nrecords the parent proposal + followup-index lineage when the study\nwas spawned from a digest \"Run this followup\" action (FR-11).","properties":{"cluster_id":{"maxLength":36,"minLength":1,"title":"Cluster Id","type":"string"},"config":{"$ref":"#/components/schemas/StudyConfigSpec"},"judgment_list_id":{"maxLength":36,"minLength":1,"title":"Judgment List Id","type":"string"},"name":{"maxLength":256,"minLength":1,"title":"Name","type":"string"},"objective":{"$ref":"#/components/schemas/ObjectiveSpec"},"parent":{"anyOf":[{"$ref":"#/components/schemas/ParentFollowupRef"},{"type":"null"}]},"parent_study_id":{"anyOf":[{"maxLength":36,"minLength":36,"type":"string"},{"type":"null"}],"description":"feat_study_clone_from_previous FR-7 — when the operator clones an existing study via the study-detail Clone button, this carries the source study's id. Server validates existence (404 PARENT_STUDY_NOT_FOUND) and same-cluster (422 PARENT_STUDY_WRONG_CLUSTER) before persisting to studies.parent_study_id. Independent of the proposal-lineage 'parent' field (D-5); both may be set.","title":"Parent Study Id"},"query_set_id":{"maxLength":36,"minLength":1,"title":"Query Set Id","type":"string"},"search_space":{"additionalProperties":true,"title":"Search Space","type":"object"},"target":{"maxLength":256,"minLength":1,"title":"Target","type":"string"},"template_id":{"maxLength":36,"minLength":1,"title":"Template Id","type":"string"}},"required":["name","cluster_id","target","template_id","query_set_id","judgment_list_id","search_space","objective","config"],"title":"CreateStudyRequest","type":"object"},"CurvePoint":{"description":"One point on the best-so-far curve.\n\n``trial_number`` is the trial's ``optuna_trial_number`` (the canonical\n\"trial order within the study\" field — see ``auto_followup.py`` module\ndocstring for why we sort by this rather than ``started_at``).\n``best_so_far`` is the running extremum of ``primary_metric`` over all\nearlier trials, sign-corrected to the study's optimization direction.","properties":{"best_so_far":{"title":"Best So Far","type":"number"},"trial_number":{"title":"Trial Number","type":"integer"}},"required":["trial_number","best_so_far"],"title":"CurvePoint","type":"object"},"DigestResponse":{"description":"Body of ``GET /api/v1/studies/{id}/digest`` (FR-3 / AC-3).\n\nfeat_digest_executable_followups Story 4.1 — ``suggested_followups`` is\nnow a discriminated-union list (NarrowFollowup | WidenFollowup |\nTextFollowup), populated by the digest handler via\n``parse_followup_list(digest.suggested_followups, ...)`` so legacy or\nmalformed JSONB payloads never crash the response.","properties":{"generated_at":{"format":"date-time","title":"Generated At","type":"string"},"generated_by":{"title":"Generated By","type":"string"},"id":{"title":"Id","type":"string"},"narrative":{"title":"Narrative","type":"string"},"parameter_importance":{"additionalProperties":{"type":"number"},"title":"Parameter Importance","type":"object"},"recommended_config":{"additionalProperties":true,"title":"Recommended Config","type":"object"},"study_id":{"title":"Study Id","type":"string"},"suggested_followups":{"items":{"$ref":"#/components/schemas/FollowupItem"},"title":"Suggested Followups","type":"array"}},"required":["id","study_id","narrative","parameter_importance","recommended_config","suggested_followups","generated_by","generated_at"],"title":"DigestResponse","type":"object"},"Document":{"description":"A single document by ID — return shape of ``SearchAdapter.get_document``.\n\nMirrors :class:`ScoredHit` minus ``score`` (browsing doesn't need scoring).\n``source`` is ``None`` when the engine's index has ``_source: false`` mapping.","properties":{"doc_id":{"minLength":1,"title":"Doc Id","type":"string"},"source":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Source"}},"required":["doc_id"],"title":"Document","type":"object"},"DocumentListResponse":{"description":"``GET /api/v1/clusters/{cluster_id}/targets/{target}/documents`` response.\n\n``next_cursor`` opaque-encodes the ES ``hits[-1].sort`` array of the\nlast visible row when ``has_more`` is True (see\n``backend.app.api.v1._documents_cursor``). The ``X-Total-Count`` header\non the response carries the engine's ``hits.total.value``.","properties":{"data":{"items":{"$ref":"#/components/schemas/DocumentSummary"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"DocumentListResponse","type":"object"},"DocumentSummary":{"description":"One row in the documents list (per FR-3 / FR-8).\n\n``source`` is the *truncated* preview emitted by\n``backend.app.services.documents.truncate_source_for_list``. The detail\nendpoint returns the untruncated ``Document.source``.","properties":{"doc_id":{"minLength":1,"title":"Doc Id","type":"string"},"source":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Source"}},"required":["doc_id","source"],"title":"DocumentSummary","type":"object"},"FieldSpec":{"description":"One field returned by ``get_schema``.","properties":{"analyzer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Analyzer"},"doc_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Doc Count"},"name":{"title":"Name","type":"string"},"type":{"title":"Type","type":"string"}},"required":["name","type"],"title":"FieldSpec","type":"object"},"FloatParam":{"additionalProperties":false,"description":"Continuous float parameter.\n\n``log=True`` enables log-uniform sampling\n(Optuna's ``suggest_float(..., log=True)``); requires ``low > 0``.","properties":{"high":{"title":"High","type":"number"},"log":{"default":false,"title":"Log","type":"boolean"},"low":{"title":"Low","type":"number"},"type":{"const":"float","title":"Type","type":"string"}},"required":["type","low","high"],"title":"FloatParam","type":"object"},"FollowupItem":{"discriminator":{"mapping":{"narrow":"#/components/schemas/NarrowFollowup","swap_template":"#/components/schemas/SwapTemplateFollowup","text":"#/components/schemas/TextFollowup","widen":"#/components/schemas/WidenFollowup"},"propertyName":"kind"},"oneOf":[{"$ref":"#/components/schemas/NarrowFollowup"},{"$ref":"#/components/schemas/WidenFollowup"},{"$ref":"#/components/schemas/TextFollowup"},{"$ref":"#/components/schemas/SwapTemplateFollowup"}]},"GenerateJudgmentsResponse":{"description":"Response of ``POST /api/v1/judgments/generate``.\n\nPer GPT-5.5 cycle 1 F5 — the endpoint registers a typed\n``response_model`` so OpenAPI introspection + contract tests can verify\nthe wire shape.","properties":{"judgment_list_id":{"title":"Judgment List Id","type":"string"},"status":{"const":"generating","title":"Status","type":"string"}},"required":["judgment_list_id","status"],"title":"GenerateJudgmentsResponse","type":"object"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"HeadlineShape":{"description":"Top-line metric value + N(queries) used in the CI.\n\n``metric`` uses ``str`` (not ``ObjectiveMetric``) to avoid a circular\nimport: ``schemas.py`` imports ``ConfidenceShape`` from here, so this\nmodule cannot import back from ``schemas.py``. The upstream value is\nalready validated by the existing ``ObjectiveMetric`` Literal at the\ncreate-study endpoint (``schemas.py:214``).","properties":{"k":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"K"},"metric":{"title":"Metric","type":"string"},"n_queries":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"N Queries"},"value":{"title":"Value","type":"number"}},"required":["metric","value","k","n_queries"],"title":"HeadlineShape","type":"object"},"HealthCheckResult":{"description":"Wire shape of the per-cluster health probe (mirrors ``HealthStatus``).","properties":{"checked_at":{"title":"Checked At","type":"string"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"status":{"enum":["green","yellow","red","unreachable"],"title":"Status","type":"string"},"version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Version"}},"required":["status","checked_at"],"title":"HealthCheckResult","type":"object"},"HealthResponse":{"description":"The /healthz response body. Same shape for HTTP 200 and 503.","properties":{"openai_capabilities":{"$ref":"#/components/schemas/OpenAICapabilities"},"openai_endpoint":{"description":"Configured OPENAI_BASE_URL","title":"Openai Endpoint","type":"string"},"status":{"enum":["ok","degraded"],"title":"Status","type":"string"},"subsystems":{"$ref":"#/components/schemas/Subsystems"},"uptime_seconds":{"description":"Seconds since the API process started","title":"Uptime Seconds","type":"integer"},"version":{"description":"Application version (relyloop_git_sha)","title":"Version","type":"string"}},"required":["status","subsystems","openai_endpoint","openai_capabilities","version","uptime_seconds"],"title":"HealthResponse","type":"object"},"ImportJudgmentItem":{"description":"One row in :class:`ImportJudgmentListRequest`.","properties":{"doc_id":{"maxLength":512,"minLength":1,"title":"Doc Id","type":"string"},"notes":{"anyOf":[{"maxLength":2000,"type":"string"},{"type":"null"}],"title":"Notes"},"query_id":{"maxLength":36,"minLength":1,"title":"Query Id","type":"string"},"rating":{"enum":[0,1,2,3],"title":"Rating","type":"integer"}},"required":["query_id","doc_id","rating"],"title":"ImportJudgmentItem","type":"object"},"ImportJudgmentListRequest":{"description":"Body for ``POST /api/v1/judgment-lists/import`` (Story 3.2).","properties":{"cluster_id":{"maxLength":36,"minLength":1,"title":"Cluster Id","type":"string"},"description":{"anyOf":[{"maxLength":2000,"type":"string"},{"type":"null"}],"title":"Description"},"judgments":{"items":{"$ref":"#/components/schemas/ImportJudgmentItem"},"maxItems":100000,"minItems":1,"title":"Judgments","type":"array"},"name":{"maxLength":256,"minLength":1,"title":"Name","type":"string"},"query_set_id":{"maxLength":36,"minLength":1,"title":"Query Set Id","type":"string"},"rubric":{"minLength":1,"title":"Rubric","type":"string"},"target":{"maxLength":256,"minLength":1,"title":"Target","type":"string"}},"required":["name","query_set_id","cluster_id","target","rubric","judgments"],"title":"ImportJudgmentListRequest","type":"object"},"IntParam":{"additionalProperties":false,"description":"Integer parameter inclusive of both bounds.","properties":{"high":{"title":"High","type":"integer"},"low":{"title":"Low","type":"integer"},"type":{"const":"int","title":"Type","type":"string"}},"required":["type","low","high"],"title":"IntParam","type":"object"},"JudgmentListDetail":{"description":"``GET /api/v1/judgment-lists/{id}`` response.\n\nNote: ``generation_params`` is populated for UBI lists (feat_ubi_judgments\nStory 1.1's JSONB column) and NULL for LLM lists. The Story 4.3 UI\n(```` + ````) reads the\npayload to discriminate UBI/hybrid lists and to reconstruct the\noriginal request for the ambiguous-skip \"Re-run with most_recent\"\naffordance.","properties":{"calibration":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Calibration"},"cluster_id":{"title":"Cluster Id","type":"string"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"current_template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Template Id"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"failed_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failed Reason"},"generation_params":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Generation Params"},"id":{"title":"Id","type":"string"},"judgment_count":{"title":"Judgment Count","type":"integer"},"name":{"title":"Name","type":"string"},"query_set_id":{"title":"Query Set Id","type":"string"},"rubric":{"title":"Rubric","type":"string"},"source_breakdown":{"$ref":"#/components/schemas/_SourceBreakdown"},"status":{"enum":["generating","complete","failed"],"title":"Status","type":"string"},"target":{"title":"Target","type":"string"}},"required":["id","name","description","query_set_id","cluster_id","target","current_template_id","rubric","status","failed_reason","judgment_count","source_breakdown","calibration","generation_params","created_at"],"title":"JudgmentListDetail","type":"object"},"JudgmentListJudgmentsResponse":{"description":"``GET /api/v1/judgment-lists/{id}/judgments`` response.","properties":{"data":{"items":{"$ref":"#/components/schemas/JudgmentRow"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"JudgmentListJudgmentsResponse","type":"object"},"JudgmentListListResponse":{"description":"``GET /api/v1/judgment-lists`` response.","properties":{"data":{"items":{"$ref":"#/components/schemas/JudgmentListSummary"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"JudgmentListListResponse","type":"object"},"JudgmentListRef":{"description":"One entry in the ``QUERY_HAS_JUDGMENTS`` 409 envelope.\n\nLives in ``detail.judgment_lists``. Maps from the repo-layer\n:class:`backend.app.db.repo.judgment.JudgmentListRefRow` at the\nrouter boundary.","properties":{"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"}},"required":["id","name"],"title":"JudgmentListRef","type":"object"},"JudgmentListSummary":{"description":"List-view row on ``GET /api/v1/judgment-lists``.","properties":{"cluster_id":{"title":"Cluster Id","type":"string"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"query_set_id":{"title":"Query Set Id","type":"string"},"status":{"enum":["generating","complete","failed"],"title":"Status","type":"string"},"target":{"title":"Target","type":"string"}},"required":["id","name","description","query_set_id","cluster_id","target","status","created_at"],"title":"JudgmentListSummary","type":"object"},"JudgmentRow":{"description":"``GET /api/v1/judgment-lists/{id}/judgments`` row + PATCH response.","properties":{"confidence":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Confidence"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"doc_id":{"title":"Doc Id","type":"string"},"id":{"title":"Id","type":"string"},"judgment_list_id":{"title":"Judgment List Id","type":"string"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"query_id":{"title":"Query Id","type":"string"},"rater_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rater Ref"},"rating":{"enum":[0,1,2,3],"title":"Rating","type":"integer"},"source":{"enum":["llm","human","click"],"title":"Source","type":"string"}},"required":["id","judgment_list_id","query_id","doc_id","rating","source","rater_ref","confidence","notes","created_at"],"title":"JudgmentRow","type":"object"},"LateTrialStddevShape":{"description":"Sample stddev of ``primary_metric`` over the late-trial window.","properties":{"min_window_required":{"title":"Min Window Required","type":"integer"},"value":{"title":"Value","type":"number"},"window_size":{"title":"Window Size","type":"integer"}},"required":["value","window_size","min_window_required"],"title":"LateTrialStddevShape","type":"object"},"MessageWire":{"description":"One row of ``GET /api/v1/conversations/{id}.messages``.","properties":{"content":{"additionalProperties":true,"title":"Content","type":"object"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"id":{"title":"Id","type":"string"},"role":{"enum":["user","assistant","tool"],"title":"Role","type":"string"},"tool_calls":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Tool Calls"}},"required":["id","role","content","created_at"],"title":"MessageWire","type":"object"},"NarrowFollowup":{"additionalProperties":false,"description":"A 'narrow' followup — re-run with a tighter range than the parent.","properties":{"kind":{"const":"narrow","title":"Kind","type":"string"},"rationale":{"title":"Rationale","type":"string"},"search_space":{"$ref":"#/components/schemas/SearchSpace"}},"required":["kind","rationale","search_space"],"title":"NarrowFollowup","type":"object"},"ObjectiveSpec":{"description":"Wire shape of ``studies.objective`` (write-side validated at create).\n\n``k`` is required for ``ndcg`` / ``precision`` / ``recall`` (per\nstandard IR-evaluation conventions: those metrics are computed at a\ncutoff rank). ``map`` accepts ``k`` optionally; ``mrr`` / ``err`` ignore\nit. The model_validator enforces this so a malformed objective\nsurfaces as 400 ``INVALID_SEARCH_SPACE`` / 422 ``VALIDATION_ERROR``\nat study-create time rather than failing later inside ``run_trial``\nwhen the worker computes the metric.","properties":{"direction":{"default":"maximize","enum":["maximize","minimize"],"title":"Direction","type":"string"},"k":{"anyOf":[{"enum":[1,3,5,10,20,50,100],"type":"integer"},{"type":"null"}],"title":"K"},"metric":{"enum":["ndcg","map","precision","recall","mrr"],"title":"Metric","type":"string"}},"required":["metric"],"title":"ObjectiveSpec","type":"object"},"OpenAICapabilities":{"description":"Cached results of the OpenAI capability check (Story 3.3 populates Redis).\n\nStep 1 (``models_endpoint``) is reported first because it gates the rest:\nwhen it fails, the other three are reported as ``\"untested\"``. The\n``models_endpoint_status_code`` field is required-but-nullable\n(per ``bug_openai_capability_check_incapable_on_valid_key`` spec §19 D-3/D-8)\n— always present in the JSON, ``null`` when not applicable. This lets\noperators distinguish ``401 -> bad key``, ``429 -> quota``,\n``5xx -> upstream outage``, ``null -> network unreachable / cache miss``.","properties":{"chat":{"description":"Chat completion probe result","enum":["ok","fail","untested"],"title":"Chat","type":"string"},"function_calling":{"description":"Function-calling probe result (tool_choice=required)","enum":["ok","fail","untested"],"title":"Function Calling","type":"string"},"models_endpoint":{"description":"GET /models probe outcome. 'ok' / 'fail' are projected from CapabilityResult.models_endpoint; 'untested' is the cache-miss default, matching the existing chat / function_calling / structured_output cache-miss handling.","enum":["ok","fail","untested"],"title":"Models Endpoint","type":"string"},"models_endpoint_status_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"HTTP status code from the GET /models probe when it HTTP-failed (>= 400). null for the success path, network-class failure (timeout / DNS / connection-refused), or cache miss. Required-but-nullable: the JSON key is always present with explicit null when no value, never omitted.","title":"Models Endpoint Status Code"},"structured_output":{"description":"JSON-schema response_format probe result","enum":["ok","fail","untested"],"title":"Structured Output","type":"string"}},"required":["models_endpoint","models_endpoint_status_code","chat","function_calling","structured_output"],"title":"OpenAICapabilities","type":"object"},"OpenPrResponse":{"description":"Body of ``POST /api/v1/proposals/{id}/open_pr`` (FR-1).\n\nReturned with HTTP 202 on successful enqueue. Status is always\n``'pending'`` at enqueue time; the worker flips it to ``'pr_opened'``\nafter the PR is open.","properties":{"message":{"title":"Message","type":"string"},"proposal_id":{"title":"Proposal Id","type":"string"},"status":{"const":"pending","title":"Status","type":"string"}},"required":["proposal_id","status","message"],"title":"OpenPrResponse","type":"object"},"OverrideJudgmentRequest":{"description":"Body for ``PATCH /api/v1/judgment-lists/{id}/judgments/{judgment_id}``.\n\n``rating`` is INTENTIONALLY unbounded at the Pydantic layer — spec §8.5\nrequires out-of-range failures to surface as 400 ``INVALID_RATING`` (not\nPydantic's default 422 ``VALIDATION_ERROR``). The handler validates the\nvalue manually and raises the domain code (per GPT-5.5 cycle 1 F4).","properties":{"notes":{"anyOf":[{"maxLength":2000,"type":"string"},{"type":"null"}],"title":"Notes"},"rating":{"title":"Rating","type":"integer"}},"required":["rating"],"title":"OverrideJudgmentRequest","type":"object"},"ParentFollowupRef":{"description":"Optional lineage payload on ``POST /api/v1/studies``.\n\nfeat_digest_executable_followups FR-11 — when the operator clicks\n\"Run this followup\" on a proposal's digest card, the create-study\npayload carries the parent proposal's id + the 0-based index into\nthe digest's ``suggested_followups`` array so the spawned study\nremembers where it came from.\n\n``proposal_id`` is a UUIDv7 (36-char hex). The exact-length bound\nforces malformed strings to surface as 422 ``VALIDATION_ERROR``\nrather than reach the DB FK check and emerge as a 404\n``PROPOSAL_NOT_FOUND``.","properties":{"followup_index":{"minimum":0.0,"title":"Followup Index","type":"integer"},"proposal_id":{"maxLength":36,"minLength":36,"title":"Proposal Id","type":"string"}},"required":["proposal_id","followup_index"],"title":"ParentFollowupRef","type":"object"},"PerQueryOutcomesShape":{"description":"Per-query outcome counts + the top-5 named regressors and improvers.","properties":{"comparison_against":{"enum":["runner_up","baseline"],"title":"Comparison Against","type":"string"},"improved":{"title":"Improved","type":"integer"},"regressed":{"title":"Regressed","type":"integer"},"top_improvers":{"default":[],"items":{"$ref":"#/components/schemas/RegressorRowShape"},"title":"Top Improvers","type":"array"},"top_regressors":{"items":{"$ref":"#/components/schemas/RegressorRowShape"},"title":"Top Regressors","type":"array"},"unchanged":{"title":"Unchanged","type":"integer"}},"required":["improved","unchanged","regressed","comparison_against","top_regressors"],"title":"PerQueryOutcomesShape","type":"object"},"ProposalDetail":{"description":"Body of the proposal detail endpoints.\n\nUsed by ``GET /api/v1/proposals/{id}``, ``POST /api/v1/proposals``,\nand ``POST /api/v1/proposals/{id}/reject``.","properties":{"cluster":{"$ref":"#/components/schemas/_ClusterEmbed"},"config_diff":{"additionalProperties":true,"title":"Config Diff","type":"object"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"digest":{"anyOf":[{"$ref":"#/components/schemas/_DigestEmbed"},{"type":"null"}]},"id":{"title":"Id","type":"string"},"is_currently_live":{"default":false,"title":"Is Currently Live","type":"boolean"},"metric_delta":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Metric Delta"},"pr_merged_at":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Pr Merged At"},"pr_open_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pr Open Error"},"pr_state":{"anyOf":[{"enum":["open","closed","merged"],"type":"string"},{"type":"null"}],"title":"Pr State"},"pr_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pr Url"},"rejected_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rejected Reason"},"status":{"enum":["pending","pr_opened","pr_merged","rejected"],"title":"Status","type":"string"},"study_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Study Id"},"study_summary":{"anyOf":[{"$ref":"#/components/schemas/_StudySummary"},{"type":"null"}]},"study_trial_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Study Trial Id"},"template":{"$ref":"#/components/schemas/_TemplateEmbed"}},"required":["id","study_id","study_summary","study_trial_id","cluster","template","config_diff","metric_delta","status","pr_url","pr_state","pr_merged_at","pr_open_error","rejected_reason","digest","created_at"],"title":"ProposalDetail","type":"object"},"ProposalSummary":{"description":"Row in the ``GET /api/v1/proposals`` list response.","properties":{"cluster":{"$ref":"#/components/schemas/_ClusterEmbed"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"id":{"title":"Id","type":"string"},"is_currently_live":{"default":false,"title":"Is Currently Live","type":"boolean"},"metric_delta":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Metric Delta"},"pr_state":{"anyOf":[{"enum":["open","closed","merged"],"type":"string"},{"type":"null"}],"title":"Pr State"},"pr_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pr Url"},"status":{"enum":["pending","pr_opened","pr_merged","rejected"],"title":"Status","type":"string"},"study_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Study Id"},"template":{"$ref":"#/components/schemas/_TemplateEmbed"}},"required":["id","study_id","cluster","template","status","pr_state","pr_url","metric_delta","created_at"],"title":"ProposalSummary","type":"object"},"ProposalsListResponse":{"description":"Body of ``GET /api/v1/proposals``.","properties":{"data":{"items":{"$ref":"#/components/schemas/ProposalSummary"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"ProposalsListResponse","type":"object"},"QueryHasJudgmentsDetail":{"description":"The ``detail`` object of a 409 ``QUERY_HAS_JUDGMENTS`` response.\n\nExtends the canonical ``{error_code, message, retryable}`` envelope\nwith two structured fields the frontend consumes directly\n(``judgment_lists`` + ``overflow_count``). Wired into the FastAPI\nroute's ``responses={409: {\"model\": QueryHasJudgmentsEnvelope}}`` so\nthe OpenAPI schema documents the contract.","properties":{"error_code":{"const":"QUERY_HAS_JUDGMENTS","title":"Error Code","type":"string"},"judgment_lists":{"items":{"$ref":"#/components/schemas/JudgmentListRef"},"title":"Judgment Lists","type":"array"},"message":{"title":"Message","type":"string"},"overflow_count":{"title":"Overflow Count","type":"integer"},"retryable":{"const":false,"title":"Retryable","type":"boolean"}},"required":["error_code","message","retryable","judgment_lists","overflow_count"],"title":"QueryHasJudgmentsDetail","type":"object"},"QueryHasJudgmentsEnvelope":{"description":"Top-level 409 wrapper (FastAPI nests under ``detail`` for HTTPException).","properties":{"detail":{"$ref":"#/components/schemas/QueryHasJudgmentsDetail"}},"required":["detail"],"title":"QueryHasJudgmentsEnvelope","type":"object"},"QueryListResponse":{"description":"``GET /api/v1/query-sets/{set_id}/queries`` response.","properties":{"data":{"items":{"$ref":"#/components/schemas/QueryRow"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"QueryListResponse","type":"object"},"QueryRow":{"description":"Wire row returned by the per-query GET + PATCH endpoints.\n\nUsed by both ``GET /api/v1/query-sets/{set_id}/queries`` and\n``PATCH /api/v1/query-sets/{set_id}/queries/{query_id}``.\n``judgment_count`` is a derived field — single batched GROUP BY in the\nrouter via :func:`backend.app.db.repo.judgment.count_judgments_per_query`.","properties":{"id":{"title":"Id","type":"string"},"judgment_count":{"title":"Judgment Count","type":"integer"},"query_metadata":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Query Metadata"},"query_text":{"title":"Query Text","type":"string"},"reference_answer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reference Answer"}},"required":["id","query_text","reference_answer","query_metadata","judgment_count"],"title":"QueryRow","type":"object"},"QuerySetDetail":{"description":"``GET /api/v1/query-sets/{id}`` response.","properties":{"cluster_id":{"title":"Cluster Id","type":"string"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"query_count":{"title":"Query Count","type":"integer"}},"required":["id","name","description","cluster_id","query_count","created_at"],"title":"QuerySetDetail","type":"object"},"QuerySetListResponse":{"description":"``GET /api/v1/query-sets`` response.","properties":{"data":{"items":{"$ref":"#/components/schemas/QuerySetSummary"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"QuerySetListResponse","type":"object"},"QuerySetSummary":{"description":"List-view shape; omits ``query_count`` to avoid N+1 counts at list time.","properties":{"cluster_id":{"title":"Cluster Id","type":"string"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"}},"required":["id","name","cluster_id","created_at"],"title":"QuerySetSummary","type":"object"},"QueryTemplateDetail":{"description":"``GET /api/v1/query-templates/{id}`` response.","properties":{"body":{"title":"Body","type":"string"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"declared_params":{"additionalProperties":{"type":"string"},"title":"Declared Params","type":"object"},"engine_type":{"enum":["elasticsearch","opensearch","solr"],"title":"Engine Type","type":"string"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"parent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Id"},"version":{"title":"Version","type":"integer"}},"required":["id","name","engine_type","body","declared_params","version","parent_id","created_at"],"title":"QueryTemplateDetail","type":"object"},"QueryTemplateListResponse":{"description":"``GET /api/v1/query-templates`` response.","properties":{"data":{"items":{"$ref":"#/components/schemas/QueryTemplateSummary"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"QueryTemplateListResponse","type":"object"},"QueryTemplateSummary":{"description":"List-view shape; drops ``body`` + ``declared_params`` for brevity.","properties":{"created_at":{"format":"date-time","title":"Created At","type":"string"},"engine_type":{"enum":["elasticsearch","opensearch","solr"],"title":"Engine Type","type":"string"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"version":{"title":"Version","type":"integer"}},"required":["id","name","engine_type","version","created_at"],"title":"QueryTemplateSummary","type":"object"},"RegressorRowShape":{"description":"One row in the named-regressors or named-improvers table.\n\nUsed for BOTH the ``top_regressors`` and ``top_improvers`` lists.\nThe wire shape is identical — ``delta = winner_score - comparison_score``\nis negative on the regressor list, positive on the improver list. The\nclass name is historical (regressors shipped first); reusing the same\ntype keeps the schema and the per-row renderer compact.","properties":{"comparison_score":{"title":"Comparison Score","type":"number"},"delta":{"title":"Delta","type":"number"},"query_id":{"title":"Query Id","type":"string"},"query_text":{"title":"Query Text","type":"string"},"winner_score":{"title":"Winner Score","type":"number"}},"required":["query_id","query_text","winner_score","comparison_score","delta"],"title":"RegressorRowShape","type":"object"},"RejectProposalRequest":{"description":"Body of ``POST /api/v1/proposals/{id}/reject`` (FR-4 / AC-5).","properties":{"reason":{"anyOf":[{"maxLength":500,"type":"string"},{"type":"null"}],"title":"Reason"}},"title":"RejectProposalRequest","type":"object"},"ReseedStatusResponse":{"additionalProperties":false,"description":"Polling-endpoint response for ``GET /api/v1/_test/demo/reseed/status``.\n\nPer ``bug_demo_reseed_fake_metric_regression`` D-2. Lives in Redis as a\nsingle JSON blob keyed by :data:`DEMO_RESEED_STATUS_KEY` so the\nhandler reads it in one round-trip.","properties":{"current_step":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Step"},"failed_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failed Reason"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Finished At"},"scenarios_completed":{"default":0,"title":"Scenarios Completed","type":"integer"},"scenarios_skipped":{"items":{"type":"string"},"title":"Scenarios Skipped","type":"array"},"scenarios_total":{"default":0,"title":"Scenarios Total","type":"integer"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"status":{"enum":["idle","running","complete","failed"],"title":"Status","type":"string"},"steps":{"items":{"type":"string"},"title":"Steps","type":"array"},"summary":{"anyOf":[{"$ref":"#/components/schemas/ReseedSummary"},{"type":"null"}]}},"required":["status"],"title":"ReseedStatusResponse","type":"object"},"ReseedSummary":{"additionalProperties":false,"description":"Returned by :func:`reseed_demo_state` on success.\n\nPer spec §9 Required invariants, every counter is exactly 4 on the\nhappy path; ``duration_ms`` is wall-clock from orchestration start\nto the rename commit.","properties":{"clusters_created":{"title":"Clusters Created","type":"integer"},"duration_ms":{"title":"Duration Ms","type":"integer"},"proposals_created":{"title":"Proposals Created","type":"integer"},"query_sets_created":{"title":"Query Sets Created","type":"integer"},"studies_completed":{"title":"Studies Completed","type":"integer"}},"required":["clusters_created","query_sets_created","studies_completed","proposals_created","duration_ms"],"title":"ReseedSummary","type":"object"},"RunQueryHit":{"description":"One hit in the ``run_query`` response.","properties":{"doc_id":{"title":"Doc Id","type":"string"},"score":{"title":"Score","type":"number"},"source":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Source"}},"required":["doc_id","score"],"title":"RunQueryHit","type":"object"},"RunQueryRequest":{"description":"``POST /api/v1/clusters/{id}/run_query`` body.","properties":{"query_dsl":{"additionalProperties":true,"title":"Query Dsl","type":"object"},"target":{"maxLength":256,"minLength":1,"title":"Target","type":"string"},"top_k":{"default":10,"maximum":1000.0,"minimum":1.0,"title":"Top K","type":"integer"}},"required":["target","query_dsl"],"title":"RunQueryRequest","type":"object"},"RunQueryResponse":{"description":"``POST /api/v1/clusters/{id}/run_query`` response.","properties":{"hits":{"items":{"$ref":"#/components/schemas/RunQueryHit"},"title":"Hits","type":"array"}},"required":["hits"],"title":"RunQueryResponse","type":"object"},"RunnerUpGapShape":{"description":"Runner-up trial's metric vs the winner.\n\nThe whole shape is suppressed to ``None`` when there are <2 complete\ntrials (FR-2 + FR-7); ``classification`` is non-null whenever this shape\nis present.","properties":{"classification":{"enum":["robust_plateau","sharp_peak"],"title":"Classification","type":"string"},"runner_up_metric":{"title":"Runner Up Metric","type":"number"},"top10_within":{"title":"Top10 Within","type":"number"},"value":{"title":"Value","type":"number"}},"required":["value","classification","top10_within","runner_up_metric"],"title":"RunnerUpGapShape","type":"object"},"Schema":{"description":"An index / collection's field schema.","properties":{"fields":{"items":{"$ref":"#/components/schemas/FieldSpec"},"title":"Fields","type":"array"},"name":{"title":"Name","type":"string"}},"required":["name","fields"],"title":"Schema","type":"object"},"SearchSpace":{"additionalProperties":false,"description":"Pydantic model for the ``studies.search_space`` JSONB column.\n\nWire format::\n\n {\n \"params\": {\n \"boost_title\": {\"type\": \"float\", \"low\": 0.1, \"high\": 10.0, \"log\": true},\n \"min_should_match\": {\"type\": \"int\", \"low\": 1, \"high\": 5},\n \"operator\": {\"type\": \"categorical\", \"choices\": [\"and\", \"or\"]},\n }\n }","properties":{"params":{"additionalProperties":{"discriminator":{"mapping":{"categorical":"#/components/schemas/CategoricalParam","float":"#/components/schemas/FloatParam","int":"#/components/schemas/IntParam"},"propertyName":"type"},"oneOf":[{"$ref":"#/components/schemas/FloatParam"},{"$ref":"#/components/schemas/IntParam"},{"$ref":"#/components/schemas/CategoricalParam"}]},"minProperties":1,"title":"Params","type":"object"}},"required":["params"],"title":"SearchSpace","type":"object"},"SeedAutoFollowupChainRequest":{"additionalProperties":false,"description":"Payload for ``POST /api/v1/_test/auto-followup/seed-chain``.\n\nSeeds ``depth + 1`` linked studies (root → … → leaf) so E2E tests can\ncover the chain-panel parent-link / children-table / cascade-radio paths\nthat the public ``POST /api/v1/studies`` endpoint can't drive\n(``parent_study_id`` is set only by the auto-followup worker).\n\nCloses ``chore_auto_followup_e2e_chain_seed_helper`` (idea #2).","properties":{"cluster_id":{"minLength":1,"title":"Cluster Id","type":"string"},"depth":{"description":"Number of chain hops to seed. depth=1 → root + leaf (2 nodes). depth=2 → root + 1 middle + leaf (3 nodes).","maximum":5.0,"minimum":1.0,"title":"Depth","type":"integer"},"in_flight_leaf":{"default":true,"description":"When True (default), the deepest node is left at status='queued'. When False, it's driven to 'completed' too. Default True matches the primary E2E use case: cascade-radio coverage where the middle node needs an in-flight child.","title":"In Flight Leaf","type":"boolean"},"in_flight_middle":{"default":true,"description":"When True (default), the immediate parent of the leaf is left at status='queued' so the Cancel button is enabled (canCancel = running || queued per study-action-bar.tsx:46). Required for the cancel-modal cascade-radio test. When False, all intermediates are completed (more realistic chain state but cancel modal won't open on the middle).","title":"In Flight Middle","type":"boolean"},"judgment_list_id":{"minLength":1,"title":"Judgment List Id","type":"string"},"query_set_id":{"minLength":1,"title":"Query Set Id","type":"string"},"template_id":{"minLength":1,"title":"Template Id","type":"string"}},"required":["cluster_id","query_set_id","template_id","judgment_list_id","depth"],"title":"SeedAutoFollowupChainRequest","type":"object"},"SeedAutoFollowupChainResponse":{"description":"IDs of every node in the seeded chain, in parent→child order.","properties":{"leaf_id":{"title":"Leaf Id","type":"string"},"middle_ids":{"items":{"type":"string"},"title":"Middle Ids","type":"array"},"root_id":{"title":"Root Id","type":"string"}},"required":["root_id","middle_ids","leaf_id"],"title":"SeedAutoFollowupChainResponse","type":"object"},"SeedCompletedStudyRequest":{"additionalProperties":false,"description":"Payload for ``POST /api/v1/_test/studies/seed-completed``.\n\nAll four FK fields are required; the caller is responsible for\nseeding the parent rows first (typically via the public\n``seedFullChain`` E2E helper).","properties":{"cluster_id":{"minLength":1,"title":"Cluster Id","type":"string"},"extra_trial_metrics":{"anyOf":[{"items":{"type":"number"},"type":"array"},{"type":"null"}],"description":"Optional list of additional complete-trial `primary_metric` values (numbered from 2 upward) seeded on top of the default winner (0.487) + runner-up (0.412). Used to push the study past the convergence classifier's usable-trial floor (5) so the `` renders a real verdict + curve instead of the too_few_trials null state (feat_study_convergence_indicator). Every value MUST be < 0.487 so the winner / best_metric / proposal / digest stay anchored to the unchanged 0.412 -> 0.487 story. Omit for the default 2-trial shape.","title":"Extra Trial Metrics"},"judgment_list_id":{"minLength":1,"title":"Judgment List Id","type":"string"},"query_set_id":{"minLength":1,"title":"Query Set Id","type":"string"},"runner_up_per_query":{"anyOf":[{"additionalProperties":{"additionalProperties":true,"type":"object"},"type":"object"},{"type":"null"}],"description":"Optional per-query metrics for the runner-up trial; pairs with `winner_per_query`.","title":"Runner Up Per Query"},"suggested_followups":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"description":"feat_digest_executable_followups Story 6.1 — optional structured FollowupItem list (`[{kind, rationale, search_space}]`) to seed on the digest. When omitted, the seeder writes two default text-kind items. The E2E Run-followup spec passes a `narrow` item so it can drive the per-card Run button + modal prefill flow.","title":"Suggested Followups"},"template_id":{"minLength":1,"title":"Template Id","type":"string"},"winner_per_query":{"anyOf":[{"additionalProperties":{"additionalProperties":true,"type":"object"},"type":"object"},{"type":"null"}],"description":"Optional per-query metrics dict to populate on the winner trial. Shape: `{query_id: {metric_token: float}}` where metric_token matches what `scoring.score()` emits (e.g. `ndcg@10`). Set alongside `runner_up_per_query` to drive the ConfidencePanel happy path on `/studies/[id]`. When omitted, the seeded trials have `per_query_metrics IS NULL` (the pre-feat_pr_metric_confidence shape).","title":"Winner Per Query"},"with_pending_proposal":{"default":true,"description":"When true (default), also insert a `status='pending'` proposal linked to the study so the digest panel's Open PR button renders enabled. Set false to test the AC-11 aria-disabled-button + tooltip path.","title":"With Pending Proposal","type":"boolean"}},"required":["cluster_id","query_set_id","template_id","judgment_list_id"],"title":"SeedCompletedStudyRequest","type":"object"},"SeedCompletedStudyResponse":{"description":"IDs of the inserted rows; mirrors :class:`SeededStudyTriple`.","properties":{"digest_id":{"title":"Digest Id","type":"string"},"proposal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proposal Id"},"study_id":{"title":"Study Id","type":"string"}},"required":["study_id","digest_id","proposal_id"],"title":"SeedCompletedStudyResponse","type":"object"},"SendMessageRequest":{"description":"``POST /api/v1/conversations/{id}/messages`` body (Story 3.2).","properties":{"content":{"$ref":"#/components/schemas/SendMessageRequestContent"},"role":{"const":"user","default":"user","title":"Role","type":"string"}},"required":["content"],"title":"SendMessageRequest","type":"object"},"SendMessageRequestContent":{"description":"Sub-shape inside :class:`SendMessageRequest`.","properties":{"text":{"maxLength":20000,"minLength":1,"title":"Text","type":"string"}},"required":["text"],"title":"SendMessageRequestContent","type":"object"},"StudyChainLink":{"description":"One link in the rolled-up overnight-chain summary (feat_overnight_autopilot §8.3).","properties":{"auto_followup_depth_remaining":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Auto Followup Depth Remaining"},"baseline_metric":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Baseline Metric"},"best_metric":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Best Metric"},"completed_at":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Completed At"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"delta_from_prev":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Delta From Prev"},"direction":{"enum":["maximize","minimize"],"title":"Direction","type":"string"},"failed_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failed Reason"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"proposal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proposal Id"},"status":{"enum":["queued","running","completed","cancelled","failed"],"title":"Status","type":"string"}},"required":["id","name","status","best_metric","baseline_metric","direction","delta_from_prev","proposal_id","auto_followup_depth_remaining","failed_reason","created_at","completed_at"],"title":"StudyChainLink","type":"object"},"StudyChainResponse":{"description":"``GET /api/v1/studies/{id}/chain`` response (feat_overnight_autopilot §8.3).","properties":{"anchor_study_id":{"title":"Anchor Study Id","type":"string"},"best_link_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Best Link Id"},"best_metric":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Best Metric"},"cumulative_lift":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cumulative Lift"},"direction":{"enum":["maximize","minimize"],"title":"Direction","type":"string"},"links":{"items":{"$ref":"#/components/schemas/StudyChainLink"},"title":"Links","type":"array"},"proposal_id_for_best_link":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proposal Id For Best Link"},"stop_reason":{"enum":["depth_exhausted","no_lift","budget","parent_failed","cancelled","in_flight"],"title":"Stop Reason","type":"string"}},"required":["anchor_study_id","best_link_id","best_metric","cumulative_lift","direction","stop_reason","proposal_id_for_best_link","links"],"title":"StudyChainResponse","type":"object"},"StudyConfigSpec":{"description":"Wire shape of ``studies.config`` (write-side).\n\nThe model_validator below enforces that at least one stop condition is\nset — otherwise the study has no terminating condition (FR-4).\n``parallelism`` / ``trial_timeout_s`` are optional; when absent the\nworker reads ``Settings.studies_default_parallelism`` /\n``studies_default_timeout_s`` at job time. The API layer does NOT\nmaterialize these fields into the stored row — see Story 1.5 +\nStory 3.3's ``config.model_dump(exclude_none=True, exclude_unset=True)``\ncontract.","properties":{"auto_followup_depth":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Auto Followup Depth"},"baseline_params":{"anyOf":[{"additionalProperties":{"anyOf":[{"type":"string"},{"type":"integer"},{"type":"number"},{"type":"boolean"},{"type":"null"}]},"type":"object"},{"type":"null"}],"title":"Baseline Params"},"max_trials":{"anyOf":[{"maximum":100000.0,"minimum":1.0,"type":"integer"},{"type":"null"}],"title":"Max Trials"},"parallelism":{"anyOf":[{"maximum":64.0,"minimum":1.0,"type":"integer"},{"type":"null"}],"title":"Parallelism"},"pruner":{"anyOf":[{"enum":["median","none"],"type":"string"},{"type":"null"}],"title":"Pruner"},"sampler":{"anyOf":[{"enum":["tpe","random"],"type":"string"},{"type":"null"}],"title":"Sampler"},"secondary_metrics":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Secondary Metrics"},"seed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Seed"},"time_budget_min":{"anyOf":[{"exclusiveMinimum":0.0,"type":"number"},{"type":"null"}],"title":"Time Budget Min"},"trial_timeout_s":{"anyOf":[{"maximum":3600.0,"minimum":5.0,"type":"integer"},{"type":"null"}],"title":"Trial Timeout S"}},"title":"StudyConfigSpec","type":"object"},"StudyConvergenceShape":{"description":"Verdict + supporting numerics for the UI panel and the digest narrative.\n\nMirrors the ``ConfidenceShape`` pattern from ``confidence.py``: the\ndomain module owns the Pydantic model, and ``backend.app.api.v1.schemas``\nre-exports it for the ``StudyDetail.convergence`` field. The\n``best_so_far_curve`` is the chart's data series; ``verdict`` is the\nbadge label.\n\n**Name discipline (plan §0).** The bare class name ``ConvergenceShape``\nis already taken by :class:`backend.app.domain.study.confidence.ConvergenceShape`\n(a different concept — winner-trial *timing*, not metric plateau).\n``StudyConvergenceShape`` is the study-level analogue; the confidence\nsub-shape stays on its inner module. The two coexist on ``StudyDetail``\n(``confidence.convergence`` is the inner one; ``convergence`` is this\none), and FastAPI emits both under their bare class names in the\nOpenAPI schema — no fully-qualified disambiguation noise leaks to the\nfrontend.","properties":{"best_so_far_curve":{"items":{"$ref":"#/components/schemas/CurvePoint"},"title":"Best So Far Curve","type":"array"},"direction":{"enum":["maximize","minimize"],"title":"Direction","type":"string"},"epsilon":{"title":"Epsilon","type":"number"},"improvement_in_window":{"title":"Improvement In Window","type":"number"},"total_complete_trials":{"title":"Total Complete Trials","type":"integer"},"verdict":{"enum":["converged","still_improving","too_few_trials"],"title":"Verdict","type":"string"},"warmup_floor":{"title":"Warmup Floor","type":"integer"},"window_size":{"title":"Window Size","type":"integer"}},"required":["verdict","direction","window_size","epsilon","warmup_floor","total_complete_trials","improvement_in_window","best_so_far_curve"],"title":"StudyConvergenceShape","type":"object"},"StudyDetail":{"description":"``GET /api/v1/studies/{id}`` response + ``POST/cancel`` response.","properties":{"baseline_metric":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Baseline Metric"},"baseline_trial_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Baseline Trial Id"},"best_metric":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Best Metric"},"best_trial_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Best Trial Id"},"cluster_id":{"title":"Cluster Id","type":"string"},"completed_at":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Completed At"},"confidence":{"anyOf":[{"$ref":"#/components/schemas/ConfidenceShape"},{"type":"null"}]},"config":{"additionalProperties":true,"title":"Config","type":"object"},"convergence":{"anyOf":[{"$ref":"#/components/schemas/StudyConvergenceShape"},{"type":"null"}]},"created_at":{"format":"date-time","title":"Created At","type":"string"},"failed_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failed Reason"},"id":{"title":"Id","type":"string"},"judgment_list_id":{"title":"Judgment List Id","type":"string"},"name":{"title":"Name","type":"string"},"objective":{"additionalProperties":true,"title":"Objective","type":"object"},"optuna_study_name":{"title":"Optuna Study Name","type":"string"},"parent_study_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Study Id"},"query_set_id":{"title":"Query Set Id","type":"string"},"search_space":{"additionalProperties":true,"title":"Search Space","type":"object"},"started_at":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Started At"},"status":{"enum":["queued","running","completed","cancelled","failed"],"title":"Status","type":"string"},"target":{"title":"Target","type":"string"},"template_id":{"title":"Template Id","type":"string"},"trials_summary":{"$ref":"#/components/schemas/TrialsSummaryShape"}},"required":["id","name","cluster_id","target","template_id","query_set_id","judgment_list_id","search_space","objective","config","status","failed_reason","optuna_study_name","parent_study_id","baseline_metric","baseline_trial_id","best_metric","best_trial_id","created_at","started_at","completed_at","trials_summary"],"title":"StudyDetail","type":"object"},"StudyListResponse":{"description":"``GET /api/v1/studies`` response.","properties":{"data":{"items":{"$ref":"#/components/schemas/StudySummary"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"StudyListResponse","type":"object"},"StudySummary":{"description":"List-view shape.","properties":{"best_metric":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Best Metric"},"cluster_id":{"title":"Cluster Id","type":"string"},"completed_at":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Completed At"},"convergence_verdict":{"anyOf":[{"enum":["converged","still_improving","too_few_trials"],"type":"string"},{"type":"null"}],"title":"Convergence Verdict"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"direction":{"default":"maximize","enum":["maximize","minimize"],"title":"Direction","type":"string"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"status":{"enum":["queued","running","completed","cancelled","failed"],"title":"Status","type":"string"},"trial_count":{"default":0,"title":"Trial Count","type":"integer"}},"required":["id","name","cluster_id","status","best_metric","created_at","completed_at"],"title":"StudySummary","type":"object"},"Subsystems":{"description":"Per-subsystem reachability/configuration state. Wire values per spec §7.4.","properties":{"db":{"description":"Postgres reachability","enum":["ok","down"],"title":"Db","type":"string"},"elasticsearch":{"description":"Local Elasticsearch container reachability","enum":["reachable","unreachable"],"title":"Elasticsearch","type":"string"},"elasticsearch_clusters":{"$ref":"#/components/schemas/ClusterAggregateHealth","description":"Aggregate health of user-registered clusters (infra_adapter_elastic Story 3.5 / spec §2). registered=0 → all-zero counts; informational only — does NOT trigger overall `degraded`."},"openai":{"description":"OpenAI key + capability state. 'incapable' added per FR-2 vs. spec §7.4 enum table — see implementation_plan.md §13 Review log.","enum":["configured","missing_key","incapable"],"title":"Openai","type":"string"},"opensearch":{"description":"Local OpenSearch container reachability","enum":["reachable","unreachable"],"title":"Opensearch","type":"string"},"redis":{"description":"Redis reachability","enum":["ok","down"],"title":"Redis","type":"string"},"solr":{"default":"not_configured","description":"Local Apache Solr container reachability. 'not_configured' when SOLR_HOST is unset (operator opted out of running the Solr service). Added by infra_adapter_solr Story A10 / spec FR-12a.","enum":["reachable","unreachable","not_configured"],"title":"Solr","type":"string"}},"required":["db","redis","openai","elasticsearch","opensearch","elasticsearch_clusters"],"title":"Subsystems","type":"object"},"SwapTemplateFollowup":{"additionalProperties":false,"description":"A 'swap_template' followup — re-run against a different query template.\n\nCarries the LLM-proposed bounds for params shared with the parent template\nin ``search_space``. The digest worker calls\n:func:`backend.app.domain.study.template_swap.remap_search_space_for_swap_target`\nafter parsing to merge these bounds with heuristic defaults for any\nswap-target params not shared with the parent.\n\nOwner: ``feat_digest_executable_followups_swap_template`` (Tier B).","properties":{"kind":{"const":"swap_template","title":"Kind","type":"string"},"rationale":{"title":"Rationale","type":"string"},"search_space":{"$ref":"#/components/schemas/SearchSpace"},"template_id":{"maxLength":36,"minLength":36,"title":"Template Id","type":"string"}},"required":["kind","rationale","template_id","search_space"],"title":"SwapTemplateFollowup","type":"object"},"TargetInfo":{"description":"One target (index / collection) on a cluster.","properties":{"doc_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Doc Count"},"name":{"title":"Name","type":"string"}},"required":["name"],"title":"TargetInfo","type":"object"},"TargetListResponse":{"description":"Response for ``GET /api/v1/clusters/{cluster_id}/targets`` (FR-1).\n\nUnpaginated by design — see feature_spec.md §7.1 \"pagination shape\nrationale\". The single-resource lookup pattern matches\n``/clusters/{id}/schema`` rather than the queryable ``/clusters`` list.\n``EntitySelectListPage``'s ``next_cursor`` and ``has_more`` fields\nare optional, so this bare ``data``-only shape consumes correctly on\nthe frontend without pretending to be a cursor endpoint.","properties":{"data":{"items":{"$ref":"#/components/schemas/TargetInfo"},"title":"Data","type":"array"}},"required":["data"],"title":"TargetListResponse","type":"object"},"TextFollowup":{"additionalProperties":false,"description":"A free-form textual suggestion — no auto-prefill, operator interprets.","properties":{"kind":{"const":"text","title":"Kind","type":"string"},"rationale":{"title":"Rationale","type":"string"},"search_space":{"title":"Search Space","type":"null"}},"required":["kind","rationale"],"title":"TextFollowup","type":"object"},"TrialDetail":{"description":"``GET /api/v1/studies/{id}/trials`` response row.","properties":{"duration_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration Ms"},"ended_at":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Ended At"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"id":{"title":"Id","type":"string"},"is_baseline":{"default":false,"title":"Is Baseline","type":"boolean"},"metrics":{"additionalProperties":true,"title":"Metrics","type":"object"},"optuna_trial_number":{"title":"Optuna Trial Number","type":"integer"},"params":{"additionalProperties":true,"title":"Params","type":"object"},"primary_metric":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Primary Metric"},"started_at":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Started At"},"status":{"enum":["complete","failed","pruned"],"title":"Status","type":"string"},"study_id":{"title":"Study Id","type":"string"}},"required":["id","study_id","optuna_trial_number","params","primary_metric","metrics","duration_ms","status","error","started_at","ended_at"],"title":"TrialDetail","type":"object"},"TrialListResponse":{"description":"``GET /api/v1/studies/{id}/trials`` response.","properties":{"data":{"items":{"$ref":"#/components/schemas/TrialDetail"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"TrialListResponse","type":"object"},"TrialsSummaryShape":{"description":"The ``trials_summary`` field embedded in :class:`StudyDetail`.","properties":{"best_primary_metric":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Best Primary Metric"},"complete":{"title":"Complete","type":"integer"},"failed":{"title":"Failed","type":"integer"},"pruned":{"title":"Pruned","type":"integer"},"total":{"title":"Total","type":"integer"}},"required":["total","complete","failed","pruned","best_primary_metric"],"title":"TrialsSummaryShape","type":"object"},"UbiReadinessResponse":{"description":"``GET /api/v1/clusters/{cluster_id}/ubi-readiness`` response (FR-7).\n\n``covered_pairs_pct`` and ``head_covered`` are nullable — MVP2's\nrung classifier uses event-count thresholds (the SearchAdapter\nProtocol doesn't expose an exact ``_count`` endpoint). The fields\nare reserved on the wire so a future ``infra_adapter_count_method``\ncan fill them without breaking the contract. See\n:mod:`backend.app.services.ubi_readiness` for the rationale.","properties":{"checked_at":{"format":"date-time","title":"Checked At","type":"string"},"covered_pairs_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Covered Pairs Pct"},"head_covered":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Head Covered"},"rung":{"enum":["rung_0","rung_1","rung_2","rung_3"],"title":"Rung","type":"string"}},"required":["rung","covered_pairs_pct","head_covered","checked_at"],"title":"UbiReadinessResponse","type":"object"},"UpdateQueryRequest":{"additionalProperties":false,"description":"``PATCH /api/v1/query-sets/{set_id}/queries/{query_id}`` body.\n\nWhole-object replace on ``query_metadata`` (NOT deep-merge); explicit\n``null`` removes a nullable field; omitted key = no change. Empty\nbody ``{}`` validates as a no-op (AC-28).\n\n``query_text`` is NOT NULL on the underlying table, so explicit-null\nis rejected by the ``@model_validator`` below (a 422 surfaces sooner\nthan the SQL ``NotNullViolation``).","properties":{"query_metadata":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Query Metadata"},"query_text":{"anyOf":[{"maxLength":4000,"minLength":1,"type":"string"},{"type":"null"}],"title":"Query Text"},"reference_answer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reference Answer"}},"title":"UpdateQueryRequest","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"},"WidenFollowup":{"additionalProperties":false,"description":"A 'widen' followup — re-run with a broader range than the parent.","properties":{"kind":{"const":"widen","title":"Kind","type":"string"},"rationale":{"title":"Rationale","type":"string"},"search_space":{"$ref":"#/components/schemas/SearchSpace"}},"required":["kind","rationale","search_space"],"title":"WidenFollowup","type":"object"},"_ClusterEmbed":{"description":"Inline cluster summary on proposal responses.","properties":{"engine_type":{"title":"Engine Type","type":"string"},"environment":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Environment"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"}},"required":["id","name","engine_type"],"title":"_ClusterEmbed","type":"object"},"_DigestEmbed":{"description":"Inline digest summary on the proposal-detail response.\n\nfeat_digest_executable_followups Story 4.1 — ``suggested_followups`` is\nnow a discriminated-union list (see ``DigestResponse``).","properties":{"generated_at":{"format":"date-time","title":"Generated At","type":"string"},"id":{"title":"Id","type":"string"},"narrative":{"title":"Narrative","type":"string"},"parameter_importance":{"additionalProperties":{"type":"number"},"title":"Parameter Importance","type":"object"},"recommended_config":{"additionalProperties":true,"title":"Recommended Config","type":"object"},"suggested_followups":{"items":{"$ref":"#/components/schemas/FollowupItem"},"title":"Suggested Followups","type":"array"}},"required":["id","narrative","parameter_importance","recommended_config","suggested_followups","generated_at"],"title":"_DigestEmbed","type":"object"},"_SourceBreakdown":{"description":"Source-breakdown sub-shape on :class:`JudgmentListDetail`.\n\nEvolved 2026-05-29 by ``feat_ubi_judgments`` FR-10 — now three terms\n(``llm + human + click == judgment_count``). The cycle-2 F6\n\"click folds into human\" contract is superseded the moment UBI ships\nclick rows; the UI's source-breakdown card now renders all three\nbuckets separately so operators see the mix at a glance.","properties":{"click":{"title":"Click","type":"integer"},"human":{"title":"Human","type":"integer"},"llm":{"title":"Llm","type":"integer"}},"required":["llm","human","click"],"title":"_SourceBreakdown","type":"object"},"_StudySummary":{"description":"Inline study summary on the proposal-detail response.","properties":{"best_metric":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Best Metric"},"best_trial_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Best Trial Id"},"id":{"title":"Id","type":"string"},"judgment_list":{"additionalProperties":true,"title":"Judgment List","type":"object"},"name":{"title":"Name","type":"string"},"query_set":{"additionalProperties":true,"title":"Query Set","type":"object"},"status":{"title":"Status","type":"string"}},"required":["id","name","status","best_metric","best_trial_id","query_set","judgment_list"],"title":"_StudySummary","type":"object"},"_TemplateEmbed":{"description":"Inline template summary on proposal responses.","properties":{"engine_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Engine Type"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"version":{"title":"Version","type":"integer"}},"required":["id","name","version"],"title":"_TemplateEmbed","type":"object"}}},"info":{"description":"Open-source automated relevance tuning for enterprise search platforms","title":"RelyLoop","version":"0.1.0"},"openapi":"3.1.0","paths":{"/api/v1/_test/auto-followup/seed-chain":{"post":{"description":"Test-only endpoint. Returns 404 unless `ENVIRONMENT=development`. Inserts a chain of `depth + 1` studies where each child carries the prior node's id as `parent_study_id`. The public POST /studies endpoint does NOT accept `parent_study_id` (it's set only by the auto-followup worker via `repo.create_study(parent_study_id=...)`), so this endpoint is the only way to drive deterministic E2E coverage of chain-panel parent-link / children-table / cascade-radio paths. Closes chore_auto_followup_e2e_chain_seed_helper.","operationId":"seed_auto_followup_chain_endpoint_api_v1__test_auto_followup_seed_chain_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeedAutoFollowupChainRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeedAutoFollowupChainResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Seed an auto-followup chain of N+1 linked studies","tags":["test-only"]}},"/api/v1/_test/demo/reseed":{"post":{"description":"Enqueues an Arq job that wipes the demo Postgres tables + ES/OS indices, then re-seeds the 4 demo scenarios from ``scripts/seed_meaningful_demos.py`` using REAL studies (real Optuna trials, real metrics per scenario). Returns 202 + an initial ``ReseedStatusResponse`` immediately; the frontend polls ``GET /api/v1/_test/demo/reseed/status`` for progress.\n\nPer ``bug_demo_reseed_fake_metric_regression``. Replaces the previous synchronous path that called ``/_test/studies/seed-completed`` and produced identical ``best_metric=0.487`` rows for every scenario.","operationId":"reseed_demo_api_v1__test_demo_reseed_post","responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReseedStatusResponse"}}},"description":"Successful Response"}},"summary":"Enqueue a demo-state reseed (dev-only, async)","tags":["test-only"]}},"/api/v1/_test/demo/reseed/status":{"get":{"description":"Returns the current reseed status from Redis. When no reseed has ever run (or the result TTL'd out), returns ``{status: 'idle'}`` rather than 404 so the frontend's polling loop is trivially safe.","operationId":"reseed_demo_status_api_v1__test_demo_reseed_status_get","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReseedStatusResponse"}}},"description":"Successful Response"}},"summary":"Poll the current demo-reseed progress (dev-only)","tags":["test-only"]}},"/api/v1/_test/digests/{digest_id}":{"delete":{"description":"FR-2: Hard-delete the digest row. No FK children — no preflight needed.","operationId":"delete_test_digest_api_v1__test_digests__digest_id__delete","parameters":[{"in":"path","name":"digest_id","required":true,"schema":{"title":"Digest Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Hard-delete a digest (test-only)","tags":["test-only"]}},"/api/v1/_test/judgment-lists/{judgment_list_id}":{"delete":{"description":"FR-4 — hard-delete the judgment_list row.\n\nJudgments cascade-delete via existing FK. Preflight-checks ``studies``\n(non-cascade); 409 if any study references the judgment_list.","operationId":"delete_test_judgment_list_api_v1__test_judgment_lists__judgment_list_id__delete","parameters":[{"in":"path","name":"judgment_list_id","required":true,"schema":{"title":"Judgment List Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Hard-delete a judgment_list (test-only)","tags":["test-only"]}},"/api/v1/_test/proposals/{proposal_id}":{"delete":{"description":"FR-1: Hard-delete the proposal row. No FK children — no preflight needed.","operationId":"delete_test_proposal_api_v1__test_proposals__proposal_id__delete","parameters":[{"in":"path","name":"proposal_id","required":true,"schema":{"title":"Proposal Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Hard-delete a proposal (test-only)","tags":["test-only"]}},"/api/v1/_test/query-sets/{query_set_id}":{"delete":{"description":"FR-5 — hard-delete the query_set row.\n\nQueries cascade-delete via existing FK. Preflight-checks ``studies``\n+ ``judgment_lists`` (both non-cascade); 409 with resource-specific\ncode if either references.","operationId":"delete_test_query_set_api_v1__test_query_sets__query_set_id__delete","parameters":[{"in":"path","name":"query_set_id","required":true,"schema":{"title":"Query Set Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Hard-delete a query_set (test-only)","tags":["test-only"]}},"/api/v1/_test/query-templates/{template_id}":{"delete":{"description":"FR-6 — hard-delete the query_template row.\n\nNo FK children cascade with template. Preflight-checks ``studies``,\n``proposals``, and ``judgment_lists.current_template_id`` in\n**fixed priority order: STUDY > PROPOSAL > JUDGMENT_LIST** (per\nspec §FR-6) — first match wins.","operationId":"delete_test_query_template_api_v1__test_query_templates__template_id__delete","parameters":[{"in":"path","name":"template_id","required":true,"schema":{"title":"Template Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Hard-delete a query_template (test-only)","tags":["test-only"]}},"/api/v1/_test/studies/seed-completed":{"post":{"description":"Test-only endpoint. Returns 404 unless `ENVIRONMENT=development`. Inserts a study (driven through queued → running → completed via the legal state-machine transitions), 2 trials (one winner, one comparison), a digest, and optionally a pending proposal in a single transaction. Used by the Playwright E2E suite to cover the digest-panel surfaces (7 tooltip placements + AC-7 body content + AC-11 Open PR enabled/disabled branches) without waiting on the orchestrator + Optuna workers.","operationId":"seed_completed_study_api_v1__test_studies_seed_completed_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeedCompletedStudyRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeedCompletedStudyResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Seed a completed study + digest + (optional) pending proposal","tags":["test-only"]}},"/api/v1/_test/studies/{study_id}":{"delete":{"description":"FR-3 — hard-delete the study row.\n\nTrials cascade-delete via existing FK. Preflight-checks ``proposals``\n+ ``digests`` (both non-cascade); 409 if any dependent rows reference\nthe study.","operationId":"delete_test_study_api_v1__test_studies__study_id__delete","parameters":[{"in":"path","name":"study_id","required":true,"schema":{"title":"Study Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Hard-delete a study (test-only)","tags":["test-only"]}},"/api/v1/clusters":{"get":{"description":"List clusters with cursor pagination + ``X-Total-Count`` header.\n\n``?q=`` is a Postgres FTS match against the cluster's ``search_vector``\n(name + base_url); 2–200 chars. Filter-only — ordering unchanged per\nspec FR-1. ``?sort=`` is one of the values in\n:data:`~backend.app.api.v1.schemas.ClusterSortKey`; the cursor is\nsort-aware so the keyset predicate matches the active ORDER BY\n(feat_data_table_primitive Stories 1.2 + 1.3).","operationId":"list_clusters_api_v1_clusters_get","parameters":[{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"since","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Since"}},{"in":"query","name":"q","required":false,"schema":{"anyOf":[{"maxLength":200,"minLength":2,"type":"string"},{"type":"null"}],"title":"Q"}},{"in":"query","name":"sort","required":false,"schema":{"anyOf":[{"enum":["name:asc","name:desc","created_at:asc","created_at:desc","environment:asc","environment:desc"],"type":"string"},{"type":"null"}],"title":"Sort"}},{"in":"query","name":"engine_type","required":false,"schema":{"anyOf":[{"enum":["elasticsearch","opensearch","solr"],"type":"string"},{"type":"null"}],"title":"Engine Type"}},{"in":"query","name":"environment","required":false,"schema":{"anyOf":[{"enum":["prod","staging","dev"],"type":"string"},{"type":"null"}],"title":"Environment"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClusterListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Clusters","tags":["clusters"]},"post":{"description":"Register a cluster (FR-5 / AC-1).","operationId":"create_cluster_api_v1_clusters_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateClusterRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClusterDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Create Cluster","tags":["clusters"]}},"/api/v1/clusters/test-connection":{"post":{"description":"Probe a cluster config WITHOUT persisting (infra_adapter_solr Story A9).\n\nPowers the registration modal's \"Test connection\" button. Always 200 —\ntransport failures surface as ``reachable=false`` with ``error`` set.\nInvalid engine×auth pairings 400 BEFORE the network call.","operationId":"test_connection_api_v1_clusters_test_connection_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionTestRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionTestResult"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Test Connection","tags":["clusters"]}},"/api/v1/clusters/{cluster_id}":{"delete":{"description":"Soft-delete a cluster (AC-8). Returns 204 with no body.","operationId":"delete_cluster_api_v1_clusters__cluster_id__delete","parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"title":"Cluster Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Delete Cluster","tags":["clusters"]},"get":{"description":"Return cluster row + cached/fresh health probe.","operationId":"get_cluster_detail_api_v1_clusters__cluster_id__get","parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"title":"Cluster Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClusterDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Cluster Detail","tags":["clusters"]}},"/api/v1/clusters/{cluster_id}/reprobe":{"post":{"description":"Re-run cluster capability probe (Story A9 / spec FR-2 + AC-14).\n\nConcurrent calls serialize on ``SELECT … FOR UPDATE``. On probe failure\nthe row's engine_config is NOT updated (the transaction rolls back).","operationId":"reprobe_cluster_api_v1_clusters__cluster_id__reprobe_post","parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"title":"Cluster Id","type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClusterDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Reprobe Cluster","tags":["clusters"]}},"/api/v1/clusters/{cluster_id}/run_query":{"post":{"description":"Execute one query DSL fragment against the cluster (FR-6 / AC-3).","operationId":"run_query_api_v1_clusters__cluster_id__run_query_post","parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"title":"Cluster Id","type":"string"}},{"in":"query","name":"timeout_s","required":false,"schema":{"default":5.0,"maximum":30.0,"minimum":1.0,"title":"Timeout S","type":"number"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunQueryRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunQueryResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Run Query","tags":["clusters"]}},"/api/v1/clusters/{cluster_id}/schema":{"get":{"description":"Return the field schema for ``target`` (FR-4 / AC-2).","operationId":"get_cluster_schema_api_v1_clusters__cluster_id__schema_get","parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"title":"Cluster Id","type":"string"}},{"in":"query","name":"target","required":true,"schema":{"maxLength":256,"minLength":1,"title":"Target","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Schema"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Cluster Schema","tags":["clusters"]}},"/api/v1/clusters/{cluster_id}/targets":{"get":{"description":"List targets (indices/collections) on the cluster (FR-1 / AC-1).\n\nThin passthrough to ``ElasticAdapter.list_targets()`` (which filters out\nsystem indices whose names start with ``.``). Mirrors the ``get_cluster_schema``\npattern: ``get_cluster`` → ``acquire_adapter`` async context → adapter call\n→ translate exceptions via the ``_err()`` helper to the spec §7.5 envelope.\n\nError mapping:\n* cluster missing or soft-deleted → 404 ``CLUSTER_NOT_FOUND`` (retryable=false)\n* adapter raises ``TargetsForbiddenError`` (ACL 401/403) → 403\n ``TARGETS_FORBIDDEN`` (retryable=false) — frontend auto-engages manual mode\n* adapter raises ``ClusterUnreachableError`` (5xx / connection failure) → 503\n ``CLUSTER_UNREACHABLE`` (retryable=true)","operationId":"list_cluster_targets_api_v1_clusters__cluster_id__targets_get","parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"title":"Cluster Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TargetListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Cluster Targets","tags":["clusters"]}},"/api/v1/clusters/{cluster_id}/targets/{target}/documents":{"get":{"description":"Paginated _id + truncated _source preview for a target (FR-3).\n\nThe endpoint asks the adapter for ``limit + 1`` rows so it can detect\nend-of-data exactly (no extra round-trip). Only the first ``limit`` rows\nare returned; ``next_cursor`` encodes the ES ``hits[i].sort`` of the\nlast visible row when ``has_more`` is True. ``X-Total-Count`` header\ncarries the engine's ``hits.total.value``.","operationId":"list_target_documents_api_v1_clusters__cluster_id__targets__target__documents_get","parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"title":"Cluster Id","type":"string"}},{"in":"path","name":"target","required":true,"schema":{"title":"Target","type":"string"}},{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"maxLength":4096,"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":25,"maximum":100,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"fields","required":false,"schema":{"anyOf":[{"maxLength":2048,"type":"string"},{"type":"null"}],"title":"Fields"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Target Documents","tags":["clusters"]}},"/api/v1/clusters/{cluster_id}/targets/{target}/documents/{doc_id}":{"get":{"description":"Fetch one document by ``_id`` (FR-4).\n\nFastAPI's ``{doc_id:path}`` converter round-trips slashes verbatim, so\noperator IDs containing ``/`` are supported (D-17 / AC-16). Returns the\nadapter ``Document`` shape directly; on ``found: false`` returns 404\n``DOCUMENT_NOT_FOUND`` (distinct from ``TARGET_NOT_FOUND``).","operationId":"get_target_document_api_v1_clusters__cluster_id__targets__target__documents__doc_id__get","parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"title":"Cluster Id","type":"string"}},{"in":"path","name":"target","required":true,"schema":{"title":"Target","type":"string"}},{"in":"path","name":"doc_id","required":true,"schema":{"title":"Doc Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Target Document","tags":["clusters"]}},"/api/v1/clusters/{cluster_id}/ubi-readiness":{"get":{"description":"Classify ``(cluster, query_set, target)`` on the UBI rung ladder.\n\nfeat_ubi_judgments FR-7.\n\nRequired query params: ``query_set_id`` + ``target`` (Spec FR-7 +\ncycle-3 D-10c: the endpoint MUST 422 without them — the classifier\ncan't compute a per-target rung without an application filter).\n\nError envelopes (all per spec §7.5):\n* ``404 CLUSTER_NOT_FOUND`` — cluster row missing or soft-deleted.\n* ``404 QUERY_SET_NOT_FOUND`` — query set row missing.\n* ``422 VALIDATION_ERROR`` — missing required query params (FastAPI's\n built-in handler, surfaces via ``api/errors.py``).\n* ``503 CLUSTER_UNREACHABLE`` — adapter cannot reach the cluster.\n\nThe result is cached for 60 s in Redis per\n``(cluster_id, query_set_id, target)`` so back-to-back dialog-open\nand dialog-submit calls don't re-probe.","operationId":"get_cluster_ubi_readiness_api_v1_clusters__cluster_id__ubi_readiness_get","parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"title":"Cluster Id","type":"string"}},{"in":"query","name":"query_set_id","required":true,"schema":{"maxLength":36,"minLength":1,"title":"Query Set Id","type":"string"}},{"in":"query","name":"target","required":true,"schema":{"maxLength":256,"minLength":1,"title":"Target","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UbiReadinessResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Cluster Ubi Readiness","tags":["clusters"]}},"/api/v1/config-repos":{"get":{"description":"Cursor-paginated config-repo list, newest first.","operationId":"list_config_repos_endpoint_api_v1_config_repos_get","parameters":[{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigReposListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Config Repos Endpoint","tags":["config-repos"]},"post":{"description":"Register a new config repo. ``provider`` is server-derived from ``repo_url``.\n\nPreflight order matches spec FR-3:\n\n1. ``validate_repo_url(repo_url)`` → 400 ``UNSUPPORTED_PROVIDER`` for\n non-GitHub URLs (AC-8). GitLab + Bitbucket arrive at MVP3.\n2. ``./secrets/{auth_ref}`` must exist → else 400 ``AUTH_REF_NOT_FOUND``\n (AC-9). The contents check defers to the worker — operators may\n populate the file between registration and first PR-open.\n3. ``name`` uniqueness check → 409 ``CONFIG_REPO_NAME_TAKEN`` on collision.\n4. Insert with server-derived ``provider=\"github\"``.\n5. **feat_github_webhook Story 4.2** — when ``webhook_secret_ref`` is\n populated, best-effort enqueue ``register_webhook`` against the\n newly created config_repo id. Enqueue failure (Redis down, pool\n absent, transient blip) does NOT break the 201 — it logs WARN\n and the operator drives recovery via the runbook.","operationId":"create_config_repo_endpoint_api_v1_config_repos_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateConfigRepoRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigRepoDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Create Config Repo Endpoint","tags":["config-repos"]}},"/api/v1/config-repos/{config_repo_id}":{"get":{"description":"Detail by id; 404 ``CONFIG_REPO_NOT_FOUND`` if missing.\n\nfeat_config_repo_baseline_tracking FR-4 — when\n``last_merged_proposal_id`` is set, embed the pointed-at proposal as a\n:class:`ProposalSummary` with ``is_currently_live=True``. The embed-side\nderivation uses the pointer context directly (NOT the generic\n``proposals → clusters → config_repos`` JOIN used elsewhere) so the\nbadge renders correctly even when the proposal's cluster was later\nunwired from this config_repo (spec §19 \"Cluster-with-config_repo-\nrotated\" decision-log entry).","operationId":"get_config_repo_endpoint_api_v1_config_repos__config_repo_id__get","parameters":[{"in":"path","name":"config_repo_id","required":true,"schema":{"title":"Config Repo Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigRepoDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Config Repo Endpoint","tags":["config-repos"]}},"/api/v1/conversations":{"get":{"description":"List conversations newest-first with per-row message_count + X-Total-Count header.\n\n``?since=`` (Story 1.5 — closes api-conventions.md drift) filters by\n``created_at >= since``. ``?q=`` (Story 1.2) is a Postgres FTS match\nagainst ``search_vector`` (coalesce(title, '')); 2-200 chars.","operationId":"list_conversations_endpoint_api_v1_conversations_get","parameters":[{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"since","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Since"}},{"in":"query","name":"q","required":false,"schema":{"anyOf":[{"maxLength":200,"minLength":2,"type":"string"},{"type":"null"}],"title":"Q"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationsListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Conversations Endpoint","tags":["conversations"]},"post":{"description":"Create a new conversation. Title is optional (FR-1 auto-generates from first message).","operationId":"create_conversation_endpoint_api_v1_conversations_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateConversationRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationSummary"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Create Conversation Endpoint","tags":["conversations"]}},"/api/v1/conversations/{conversation_id}":{"delete":{"description":"Soft-delete the conversation; subsequent reads return 404.","operationId":"delete_conversation_endpoint_api_v1_conversations__conversation_id__delete","parameters":[{"in":"path","name":"conversation_id","required":true,"schema":{"title":"Conversation Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Delete Conversation Endpoint","tags":["conversations"]},"get":{"description":"Return the conversation's full message history.","operationId":"get_conversation_endpoint_api_v1_conversations__conversation_id__get","parameters":[{"in":"path","name":"conversation_id","required":true,"schema":{"title":"Conversation Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Conversation Endpoint","tags":["conversations"]}},"/api/v1/conversations/{conversation_id}/messages":{"post":{"description":"Send a user message and stream the assistant turn as SSE.\n\nPreflight (in order; returns plain JSON envelope, NOT a partial stream):\n A. Conversation exists → else 404 ``CONVERSATION_NOT_FOUND``.\n B. ``Settings.openai_api_key`` populated → else 503 ``OPENAI_NOT_CONFIGURED``.\n C. Daily budget peek under cap → else 503 ``OPENAI_BUDGET_EXCEEDED``.\n\nSuccessful preflight returns a ``StreamingResponse(text/event-stream)``\ndriven by :func:`agent_chat.send_user_message`.","operationId":"post_message_endpoint_api_v1_conversations__conversation_id__messages_post","parameters":[{"in":"path","name":"conversation_id","required":true,"schema":{"title":"Conversation Id","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessageRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Post Message Endpoint","tags":["conversations"]}},"/api/v1/judgment-lists":{"get":{"description":"List judgment lists, newest-first with cursor pagination.\n\n``?since=`` filters by ``created_at >= since`` (Story 1.5). ``?q=`` FTS\nmatch against ``search_vector`` (name + target). ``?sort=`` is a\n:data:`JudgmentListSortKey` value with sort-aware cursor (Story 1.3).\n``?query_set_id`` / ``?cluster_id`` filter to lists belonging to the\nsupplied parent (``bug_judgment_lists_listing_ignores_query_set_filter``\n— required by the create-study modal's Step-2 dropdown so the user\ncan only pick judgment-lists valid for the chosen query-set + cluster;\nwithout these filters the modal returns all rows and the user can\npick a mismatched pair, which the ``POST /api/v1/studies`` cross-\nentity integrity check then rejects at create time with a confusing\n422 ``VALIDATION_ERROR: \"judgment_list query_set_id does not match\nstudy query_set_id\"``).\n\n``?target=`` filters by exact target index/collection name\n(``feat_study_target_judgment_mismatch_guard`` FR-2 — pairs with the\n``POST /studies`` ``JUDGMENT_TARGET_MISMATCH`` 422 so the create-study\nmodal can pre-filter the dropdown to only lists matching the chosen\nstudy target). Bounded by the ES/OpenSearch index-name ceiling\n(255 bytes).","operationId":"list_judgment_lists_endpoint_api_v1_judgment_lists_get","parameters":[{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"since","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Since"}},{"in":"query","name":"q","required":false,"schema":{"anyOf":[{"maxLength":200,"minLength":2,"type":"string"},{"type":"null"}],"title":"Q"}},{"in":"query","name":"sort","required":false,"schema":{"anyOf":[{"enum":["name:asc","name:desc","created_at:asc","created_at:desc","status:asc","status:desc"],"type":"string"},{"type":"null"}],"title":"Sort"}},{"in":"query","name":"query_set_id","required":false,"schema":{"anyOf":[{"maxLength":36,"minLength":1,"type":"string"},{"type":"null"}],"title":"Query Set Id"}},{"in":"query","name":"cluster_id","required":false,"schema":{"anyOf":[{"maxLength":36,"minLength":1,"type":"string"},{"type":"null"}],"title":"Cluster Id"}},{"in":"query","name":"target","required":false,"schema":{"anyOf":[{"maxLength":255,"minLength":1,"type":"string"},{"type":"null"}],"title":"Target"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JudgmentListListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Judgment Lists Endpoint","tags":["judgments"]}},"/api/v1/judgment-lists/import":{"post":{"description":"Create a judgment_lists row with status='complete' + bulk-insert judgments.\n\nTutorial path; no OpenAI involvement. Every supplied judgment must\nreference a ``query_id`` that exists in ``body.query_set_id`` —\nmismatches → 400 ``QUERY_NOT_IN_SET``.","operationId":"import_judgment_list_api_v1_judgment_lists_import_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportJudgmentListRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JudgmentListDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Import Judgment List","tags":["judgments"]}},"/api/v1/judgment-lists/{judgment_list_id}":{"get":{"operationId":"get_judgment_list_endpoint_api_v1_judgment_lists__judgment_list_id__get","parameters":[{"in":"path","name":"judgment_list_id","required":true,"schema":{"title":"Judgment List Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JudgmentListDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Judgment List Endpoint","tags":["judgments"]}},"/api/v1/judgment-lists/{judgment_list_id}/calibration":{"post":{"description":"Compute Cohen's + weighted kappa from supplied human samples.\n\nPairs are built by joining each sample with the existing\n``source='llm'`` judgment at ``(query_id, doc_id)`` — overridden rows\n(``source='human'``) are excluded (per spec FR-5 + GPT-5.5 cycle 1 F12).","operationId":"calibrate_judgment_list_api_v1_judgment_lists__judgment_list_id__calibration_post","parameters":[{"in":"path","name":"judgment_list_id","required":true,"schema":{"title":"Judgment List Id","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CalibrationSamplesRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CalibrationResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Calibrate Judgment List","tags":["judgments"]}},"/api/v1/judgment-lists/{judgment_list_id}/judgments":{"get":{"description":"List per-list judgments with cursor pagination.\n\n``?sort=`` is :data:`JudgmentRowSortKey` with sort-aware cursor\n(feat_data_table_primitive Story 1.3).","operationId":"list_judgments_endpoint_api_v1_judgment_lists__judgment_list_id__judgments_get","parameters":[{"in":"path","name":"judgment_list_id","required":true,"schema":{"title":"Judgment List Id","type":"string"}},{"in":"query","name":"source","required":false,"schema":{"anyOf":[{"enum":["llm","human","click"],"type":"string"},{"type":"null"}],"title":"Source"}},{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"sort","required":false,"schema":{"anyOf":[{"enum":["created_at:asc","created_at:desc","rating:asc","rating:desc","source:asc","source:desc"],"type":"string"},{"type":"null"}],"title":"Sort"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JudgmentListJudgmentsResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Judgments Endpoint","tags":["judgments"]}},"/api/v1/judgment-lists/{judgment_list_id}/judgments/{judgment_id}":{"patch":{"description":"Replace an LLM rating with a human override (UPSERT-replace).","operationId":"override_judgment_api_v1_judgment_lists__judgment_list_id__judgments__judgment_id__patch","parameters":[{"in":"path","name":"judgment_list_id","required":true,"schema":{"title":"Judgment List Id","type":"string"}},{"in":"path","name":"judgment_id","required":true,"schema":{"title":"Judgment Id","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OverrideJudgmentRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JudgmentRow"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Override Judgment","tags":["judgments"]}},"/api/v1/judgments/generate":{"post":{"description":"Create a judgment_lists row + enqueue the worker.\n\nDelegates the full preflight + INSERT + Arq enqueue to\n:func:`backend.app.services.agent_judgments_dispatch.start_judgment_generation`\nso the chat-agent ``generate_judgments_llm`` tool reuses the exact same\nchecks (no duplicated preflight). Wire behavior is identical — same error\ncodes, same status codes, same response shape.","operationId":"generate_judgments_api_v1_judgments_generate_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJudgmentListGenerateRequest"}}},"required":true},"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateJudgmentsResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Generate Judgments","tags":["judgments"]}},"/api/v1/judgments/generate-from-ubi":{"post":{"description":"Start a UBI-derived judgment generation job.\n\nDelegates to\n:func:`backend.app.services.agent_judgments_dispatch.start_ubi_judgment_generation`\nwhich runs the full FR-4 preflight (U-A..U-H) before INSERT + Arq\nenqueue. The Pydantic ``model_validator`` on\n:class:`CreateJudgmentListFromUbiRequest` already enforces the\nhybrid conditional (``current_template_id`` + ``rubric`` required\niff ``converter == 'hybrid_ubi_llm'``); the dispatcher trusts the\nvalidated request.","operationId":"generate_judgments_from_ubi_api_v1_judgments_generate_from_ubi_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJudgmentListFromUbiRequest"}}},"required":true},"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateJudgmentsResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Generate Judgments From Ubi","tags":["judgments"]}},"/api/v1/proposals":{"get":{"description":"List proposals with cursor pagination + filters.\n\n``?template_id=`` (Story 1.5) filters by ``proposals.template_id`` FK;\n``?study_id=`` filters by ``proposals.study_id`` FK (used by the\nstudy-detail page's pending-proposal lookup). Both reject invalid\nUUIDs with 422 via FastAPI's UUID parsing. ``?sort=`` (Story 1.3) is\na :data:`ProposalSortKey` value with sort-aware cursor.","operationId":"list_proposals_endpoint_api_v1_proposals_get","parameters":[{"in":"query","name":"status","required":false,"schema":{"anyOf":[{"enum":["pending","pr_opened","pr_merged","rejected"],"type":"string"},{"type":"null"}],"title":"Status"}},{"in":"query","name":"cluster_id","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cluster Id"}},{"in":"query","name":"source","required":false,"schema":{"anyOf":[{"enum":["study","manual"],"type":"string"},{"type":"null"}],"title":"Source"}},{"in":"query","name":"template_id","required":false,"schema":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"title":"Template Id"}},{"in":"query","name":"study_id","required":false,"schema":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"title":"Study Id"}},{"in":"query","name":"is_last_merged","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Last Merged"}},{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"sort","required":false,"schema":{"anyOf":[{"enum":["created_at:asc","created_at:desc","status:asc","status:desc","pr_state:asc","pr_state:desc"],"type":"string"},{"type":"null"}],"title":"Sort"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProposalsListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Proposals Endpoint","tags":["proposals"]},"post":{"description":"Manually create a proposal (chat-agent hand-crafted tweaks).\n\n``study_id`` and ``study_trial_id`` are NULL for manual proposals.\nValidates FK targets (cluster + template exist) before insert.","operationId":"create_manual_proposal_api_v1_proposals_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProposalRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProposalDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Create Manual Proposal","tags":["proposals"]}},"/api/v1/proposals/{proposal_id}":{"get":{"operationId":"get_proposal_endpoint_api_v1_proposals__proposal_id__get","parameters":[{"in":"path","name":"proposal_id","required":true,"schema":{"title":"Proposal Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProposalDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Proposal Endpoint","tags":["proposals"]}},"/api/v1/proposals/{proposal_id}/open_pr":{"post":{"description":"Enqueue the ``open_pr`` worker for an operator-approved proposal.\n\nDelegates the full preflight + Arq enqueue to\n:func:`backend.app.services.agent_proposals_dispatch.open_pr` so the\nchat-agent ``open_pr`` tool reuses the same checks. Wire behavior is\nidentical — same error codes, status codes, response shape.","operationId":"open_pr_endpoint_api_v1_proposals__proposal_id__open_pr_post","parameters":[{"in":"path","name":"proposal_id","required":true,"schema":{"title":"Proposal Id","type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenPrResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Open Pr Endpoint","tags":["proposals"]}},"/api/v1/proposals/{proposal_id}/reject":{"post":{"description":"AC-5: ``pending → rejected`` transition; 409 INVALID_STATE_TRANSITION otherwise.","operationId":"reject_proposal_endpoint_api_v1_proposals__proposal_id__reject_post","parameters":[{"in":"path","name":"proposal_id","required":true,"schema":{"title":"Proposal Id","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RejectProposalRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProposalDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Reject Proposal Endpoint","tags":["proposals"]}},"/api/v1/query-sets":{"get":{"description":"List query sets with cursor pagination + X-Total-Count.\n\n``?q=`` is FTS match against ``search_vector`` (name). ``?sort=`` is a\n:data:`QuerySetSortKey` value; cursor is sort-aware.","operationId":"list_query_sets_api_v1_query_sets_get","parameters":[{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"since","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Since"}},{"in":"query","name":"q","required":false,"schema":{"anyOf":[{"maxLength":200,"minLength":2,"type":"string"},{"type":"null"}],"title":"Q"}},{"in":"query","name":"sort","required":false,"schema":{"anyOf":[{"enum":["name:asc","name:desc","created_at:asc","created_at:desc"],"type":"string"},{"type":"null"}],"title":"Sort"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuerySetListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Query Sets","tags":["query-sets"]},"post":{"description":"Register a query set under a cluster (FR-3).","operationId":"create_query_set_api_v1_query_sets_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateQuerySetRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuerySetDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Create Query Set","tags":["query-sets"]}},"/api/v1/query-sets/{query_set_id}":{"get":{"description":"Return a query set by id (includes ``query_count``).","operationId":"get_query_set_detail_api_v1_query_sets__query_set_id__get","parameters":[{"in":"path","name":"query_set_id","required":true,"schema":{"title":"Query Set Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuerySetDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Query Set Detail","tags":["query-sets"]}},"/api/v1/query-sets/{query_set_id}/queries":{"get":{"description":"List per-query rows under a query set, with derived ``judgment_count``.","operationId":"list_queries_in_set_api_v1_query_sets__query_set_id__queries_get","parameters":[{"in":"path","name":"query_set_id","required":true,"schema":{"title":"Query Set Id","type":"string"}},{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"since","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Since"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Queries In Set","tags":["query-sets"]},"post":{"description":"Bulk-add queries to a set (FR-3 + AC-8).\n\nDispatches on Content-Type:\n\n* ``application/json`` → :class:`BulkQueriesJsonRequest` Pydantic-parse.\n* ``text/csv`` → :func:`parse_queries_csv` (AC-8).\n\nOther content types → 415-equivalent surfaced as 400 ``INVALID_CSV``\n(the documented error code for content-type-mismatch in spec §7.5).","operationId":"bulk_add_queries_api_v1_query_sets__query_set_id__queries_post","parameters":[{"in":"path","name":"query_set_id","required":true,"schema":{"title":"Query Set Id","type":"string"}}],"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkQueriesResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Bulk Add Queries","tags":["query-sets"]}},"/api/v1/query-sets/{query_set_id}/queries/{query_id}":{"delete":{"description":"Hard-delete a query. FK-guarded — 409 if any judgment references it.","operationId":"delete_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__delete","parameters":[{"in":"path","name":"query_set_id","required":true,"schema":{"title":"Query Set Id","type":"string"}},{"in":"path","name":"query_id","required":true,"schema":{"title":"Query Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryHasJudgmentsEnvelope"}}},"description":"Conflict"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Delete Query Endpoint","tags":["query-sets"]},"patch":{"description":"Partial-update a query. Whole-object replace on ``query_metadata``.","operationId":"update_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__patch","parameters":[{"in":"path","name":"query_set_id","required":true,"schema":{"title":"Query Set Id","type":"string"}},{"in":"path","name":"query_id","required":true,"schema":{"title":"Query Id","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateQueryRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryRow"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Update Query Endpoint","tags":["query-sets"]}},"/api/v1/query-templates":{"get":{"description":"List query templates with cursor pagination + X-Total-Count header.\n\n``?q=`` FTS match (name). ``?sort=`` sort-aware cursor (Story 1.3).\n``?engine_type=`` filters by engine (Story 1.4).","operationId":"list_query_templates_api_v1_query_templates_get","parameters":[{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"since","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Since"}},{"in":"query","name":"q","required":false,"schema":{"anyOf":[{"maxLength":200,"minLength":2,"type":"string"},{"type":"null"}],"title":"Q"}},{"in":"query","name":"sort","required":false,"schema":{"anyOf":[{"enum":["name:asc","name:desc","created_at:asc","created_at:desc","engine_type:asc","engine_type:desc","version:asc","version:desc"],"type":"string"},{"type":"null"}],"title":"Sort"}},{"in":"query","name":"engine_type","required":false,"schema":{"anyOf":[{"enum":["elasticsearch","opensearch","solr"],"type":"string"},{"type":"null"}],"title":"Engine Type"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryTemplateListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Query Templates","tags":["query-templates"]},"post":{"description":"Register a query template (FR-2 + AC-7).\n\nAC-7: a body containing ``{{ os.system('rm -rf /') }}`` surfaces as\n400 ``INVALID_TEMPLATE_SYNTAX`` (the AST walk catches the ``Call``\nnode before reaching the meta-vars cross-check that would otherwise\nclassify ``os`` as ``UndeclaredParamUsed``).","operationId":"create_query_template_api_v1_query_templates_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateQueryTemplateRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryTemplateDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Create Query Template","tags":["query-templates"]}},"/api/v1/query-templates/{template_id}":{"get":{"description":"Return a query template by id.","operationId":"get_query_template_detail_api_v1_query_templates__template_id__get","parameters":[{"in":"path","name":"template_id","required":true,"schema":{"title":"Template Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryTemplateDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Query Template Detail","tags":["query-templates"]}},"/api/v1/studies":{"get":{"description":"List studies with cursor pagination + X-Total-Count.\n\n``?status=`` is typed as :data:`StudyStatusWire` so FastAPI returns\n422 ``VALIDATION_ERROR`` for unsupported values. ``?q=`` is a Postgres\nFTS match against ``search_vector`` (name + target). ``?sort=`` is a\n:data:`StudySortKey` value (``:``); the cursor is\nsort-aware (feat_data_table_primitive Stories 1.2 + 1.3).\n\n``?target=`` (feat_index_document_browser FR-5) scopes the list to\nstudies targeting a single index/collection. Composes with all other\nfilters via AND.","operationId":"list_studies_api_v1_studies_get","parameters":[{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"since","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Since"}},{"in":"query","name":"status","required":false,"schema":{"anyOf":[{"enum":["queued","running","completed","cancelled","failed"],"type":"string"},{"type":"null"}],"title":"Status"}},{"in":"query","name":"cluster_id","required":false,"schema":{"anyOf":[{"maxLength":36,"minLength":1,"type":"string"},{"type":"null"}],"title":"Cluster Id"}},{"in":"query","name":"target","required":false,"schema":{"anyOf":[{"maxLength":256,"minLength":1,"type":"string"},{"type":"null"}],"title":"Target"}},{"in":"query","name":"q","required":false,"schema":{"anyOf":[{"maxLength":200,"minLength":2,"type":"string"},{"type":"null"}],"title":"Q"}},{"in":"query","name":"sort","required":false,"schema":{"anyOf":[{"enum":["name:asc","name:desc","created_at:asc","created_at:desc","completed_at:asc","completed_at:desc","best_metric:asc","best_metric:desc","status:asc","status:desc"],"type":"string"},{"type":"null"}],"title":"Sort"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudyListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Studies","tags":["studies"]},"post":{"description":"Create a study (FR-1 + AC-1) and enqueue the orchestrator job.","operationId":"create_study_api_v1_studies_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateStudyRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudyDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Create Study","tags":["studies"]}},"/api/v1/studies/{study_id}":{"get":{"description":"Return a study by id (includes ``trials_summary``).","operationId":"get_study_detail_api_v1_studies__study_id__get","parameters":[{"in":"path","name":"study_id","required":true,"schema":{"title":"Study Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudyDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Study Detail","tags":["studies"]}},"/api/v1/studies/{study_id}/cancel":{"post":{"description":"Cancel a study (Story 2.3, FR-8 + AC-8/AC-9).\n\nOptionally cascades to in-flight chain children.\n\n``?cascade=true`` (default): routes through\n:func:`services.study_state.cancel_study_with_chain_cascade` —\ncancels the parent (if in-flight) AND recursively cancels in-flight\ndescendants. Tolerates terminal parents (recurses through completed\nintermediates to reach an in-flight grandchild).\n\n``?cascade=false``: routes through the original\n:func:`services.study_state.cancel_study` — single-study cancel,\npreserves the existing 409 error contract on terminal parents\n(AC-9 wire contract).","operationId":"cancel_study_api_v1_studies__study_id__cancel_post","parameters":[{"in":"path","name":"study_id","required":true,"schema":{"title":"Study Id","type":"string"}},{"in":"query","name":"cascade","required":false,"schema":{"default":"true","title":"Cascade","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudyDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Cancel Study","tags":["studies"]}},"/api/v1/studies/{study_id}/chain":{"get":{"description":"Return the rolled-up chain summary for the study and its lineage (FR-3).\n\nWalks to the chain anchor, aggregates the completed-link subset into a\nbest link + cumulative lift + derived stop reason, and emits per-link\ndeltas. The anchor's ``delta_from_prev`` is always ``None`` (spec §8.3).\nReturns ``404 STUDY_NOT_FOUND`` when the study does not exist.","operationId":"get_study_chain_api_v1_studies__study_id__chain_get","parameters":[{"in":"path","name":"study_id","required":true,"schema":{"title":"Study Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudyChainResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Study Chain","tags":["studies"]}},"/api/v1/studies/{study_id}/children":{"get":{"description":"List direct child studies of a parent (FR-10 + D-13).\n\nReturns ``{\"data\": [], \"next_cursor\": null}`` for a study with no\nchildren — empty data array, NOT 404. 404 only fires when the parent\nstudy itself is missing.\n\nPer D-13 (direct-children-only): does NOT return transitive\ndescendants. The chain panel renders parent ↑ + direct children ↓;\noperators walk lineage one hop per page navigation.","operationId":"list_study_children_api_v1_studies__study_id__children_get","parameters":[{"in":"path","name":"study_id","required":true,"schema":{"title":"Study Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudyListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Study Children","tags":["studies"]}},"/api/v1/studies/{study_id}/digest":{"get":{"description":"Fetch the digest for a completed study.\n\nReturns 404 ``DIGEST_NOT_READY`` (``retryable=true``) when:\n- the study is not in ``status='completed'``, OR\n- the study is completed but the worker hasn't written the digest yet\n (worker lag, or a worker-side terminal failure like\n ``OPENAI_NOT_CONFIGURED`` deferred the run).","operationId":"get_study_digest_api_v1_studies__study_id__digest_get","parameters":[{"in":"path","name":"study_id","required":true,"schema":{"title":"Study Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DigestResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Study Digest","tags":["digests"]}},"/api/v1/studies/{study_id}/trials":{"get":{"description":"List trials in a study (FR-6).\n\nSort variants per spec §7.4: ``primary_metric_desc`` (default),\n``primary_metric_asc``, ``ended_at_desc``, ``ended_at_asc``,\n``optuna_trial_number_asc``.","operationId":"list_study_trials_api_v1_studies__study_id__trials_get","parameters":[{"in":"path","name":"study_id","required":true,"schema":{"title":"Study Id","type":"string"}},{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"since","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Since"}},{"in":"query","name":"sort","required":false,"schema":{"default":"primary_metric_desc","title":"Sort","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrialListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Study Trials","tags":["trials"]}},"/healthz":{"get":{"description":"Probe each subsystem in parallel and return the documented JSON shape.\n\nArgs:\n settings: Application settings (DB URL, ES/OS URLs, OpenAI base URL, etc.)\n redis_client: Redis client for ping probe + capability-cache read\n es_client: shared httpx client for ES + OpenSearch HTTP probes\n db: Async DB session for the registered-clusters aggregate (Story 3.5)\n\nReturns:\n JSONResponse with the HealthResponse body and HTTP 200 (healthy) or 503 (degraded).","operationId":"healthz_healthz_get","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}},"description":"Successful Response"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}},"description":"One or more required subsystems is down"}},"summary":"Healthz","tags":["operator"]}},"/webhooks/github":{"post":{"description":"Receive a single GitHub webhook delivery.\n\nReturns ``{\"status\": \"ok\", \"action\": }`` where\n``wire_action`` is one of the four values in\n:data:`WEBHOOK_ACTION_VALUES`.\n\nRaises:\n HTTPException(403, INVALID_SIGNATURE): bad signature or unknown\n repository. Both share one error code so the receiver does\n not reveal repo enumeration.","operationId":"github_webhook_webhooks_github_post","responses":{"200":{"content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"title":"Response Github Webhook Webhooks Github Post","type":"object"}}},"description":"Successful Response"}},"summary":"Github Webhook","tags":["webhooks"]}}}} +{"components":{"schemas":{"BulkQueriesResponse":{"description":"``POST /api/v1/query-sets/{id}/queries`` response.","properties":{"added":{"title":"Added","type":"integer"}},"required":["added"],"title":"BulkQueriesResponse","type":"object"},"CIShape":{"description":"Bootstrap percentile CI on the winner's per-query metric values.","properties":{"high":{"title":"High","type":"number"},"low":{"title":"Low","type":"number"},"method":{"const":"bootstrap_n1000","title":"Method","type":"string"},"n_samples":{"title":"N Samples","type":"integer"}},"required":["low","high","method","n_samples"],"title":"CIShape","type":"object"},"CalibrationResponse":{"description":"Calibration endpoint response.\n\nMirrors :class:`backend.app.eval.calibration.CalibrationResult` —\npersisted as ``judgment_lists.calibration`` JSONB.","properties":{"cohens_kappa":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cohens Kappa"},"n_samples":{"title":"N Samples","type":"integer"},"per_class":{"additionalProperties":{"type":"number"},"title":"Per Class","type":"object"},"warning":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Warning"},"weighted_kappa":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Weighted Kappa"}},"required":["cohens_kappa","weighted_kappa","per_class","n_samples","warning"],"title":"CalibrationResponse","type":"object"},"CalibrationSample":{"description":"One row in :class:`CalibrationSamplesRequest`.","properties":{"doc_id":{"maxLength":512,"minLength":1,"title":"Doc Id","type":"string"},"query_id":{"maxLength":36,"minLength":1,"title":"Query Id","type":"string"},"rating":{"enum":[0,1,2,3],"title":"Rating","type":"integer"}},"required":["query_id","doc_id","rating"],"title":"CalibrationSample","type":"object"},"CalibrationSamplesRequest":{"description":"Body for ``POST /api/v1/judgment-lists/{id}/calibration`` (Story 3.5).","properties":{"human_samples":{"items":{"$ref":"#/components/schemas/CalibrationSample"},"minItems":1,"title":"Human Samples","type":"array"}},"required":["human_samples"],"title":"CalibrationSamplesRequest","type":"object"},"CategoricalParam":{"additionalProperties":false,"description":"Discrete choice parameter.\n\nOptuna ``suggest_categorical`` handles strings, ints, floats, and bools\nas choices.","properties":{"choices":{"items":{"anyOf":[{"type":"string"},{"type":"integer"},{"type":"number"},{"type":"boolean"}]},"minItems":1,"title":"Choices","type":"array"},"type":{"const":"categorical","title":"Type","type":"string"}},"required":["type","choices"],"title":"CategoricalParam","type":"object"},"ClusterAggregateHealth":{"description":"Aggregate counts for the ``elasticsearch_clusters`` /healthz field (Story 3.5).\n\nPer spec §2: probes only the *registered* user clusters (from the DB),\nNOT the local Compose ES/OpenSearch — those have their own subsystem\nfields. ``status`` is a count derived from the cached ``cluster:health:*``\nentries; missing-cache or red/unreachable clusters are counted as\n``unreachable``.","properties":{"healthy":{"title":"Healthy","type":"integer"},"registered":{"title":"Registered","type":"integer"},"unreachable":{"title":"Unreachable","type":"integer"}},"required":["registered","healthy","unreachable"],"title":"ClusterAggregateHealth","type":"object"},"ClusterDetail":{"description":"``GET /api/v1/clusters/{id}`` response.","properties":{"auth_kind":{"enum":["es_apikey","es_basic","opensearch_basic","opensearch_sigv4","solr_basic","solr_apikey"],"title":"Auth Kind","type":"string"},"base_url":{"title":"Base Url","type":"string"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"engine_config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Engine Config"},"engine_type":{"enum":["elasticsearch","opensearch","solr"],"title":"Engine Type","type":"string"},"environment":{"enum":["prod","staging","dev"],"title":"Environment","type":"string"},"health_check":{"$ref":"#/components/schemas/HealthCheckResult"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"target_filter":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Filter"}},"required":["id","name","engine_type","environment","base_url","auth_kind","created_at","health_check"],"title":"ClusterDetail","type":"object"},"ClusterListResponse":{"description":"Paginated list response.","properties":{"data":{"items":{"$ref":"#/components/schemas/ClusterSummary"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"ClusterListResponse","type":"object"},"ClusterSummary":{"description":"List-view; drops engine_config + notes for brevity.","properties":{"auth_kind":{"enum":["es_apikey","es_basic","opensearch_basic","opensearch_sigv4","solr_basic","solr_apikey"],"title":"Auth Kind","type":"string"},"base_url":{"title":"Base Url","type":"string"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"engine_type":{"enum":["elasticsearch","opensearch","solr"],"title":"Engine Type","type":"string"},"environment":{"enum":["prod","staging","dev"],"title":"Environment","type":"string"},"health_check":{"$ref":"#/components/schemas/HealthCheckResult"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"target_filter":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Filter"}},"required":["id","name","engine_type","environment","base_url","auth_kind","created_at","health_check"],"title":"ClusterSummary","type":"object"},"ConfidenceShape":{"description":"The top-level shape exposed via ``StudyDetail.confidence``.\n\nEvery sub-field is independently nullable per FR-7 — degraded paths\nsuppress only the sub-fields they affect, never the whole shape (the\norchestrator returns whole-object ``None`` only when the winner trial\nrow itself is missing).","properties":{"ci_95":{"anyOf":[{"$ref":"#/components/schemas/CIShape"},{"type":"null"}]},"convergence":{"anyOf":[{"$ref":"#/components/schemas/ConvergenceShape"},{"type":"null"}]},"headline":{"$ref":"#/components/schemas/HeadlineShape"},"late_trial_stddev":{"anyOf":[{"$ref":"#/components/schemas/LateTrialStddevShape"},{"type":"null"}]},"per_query_outcomes":{"anyOf":[{"$ref":"#/components/schemas/PerQueryOutcomesShape"},{"type":"null"}]},"runner_up_gap":{"anyOf":[{"$ref":"#/components/schemas/RunnerUpGapShape"},{"type":"null"}]}},"required":["headline","ci_95","runner_up_gap","late_trial_stddev","convergence","per_query_outcomes"],"title":"ConfidenceShape","type":"object"},"ConfigRepoDetail":{"description":"``GET /api/v1/config-repos/{id}`` response + ``POST`` 201 body.","properties":{"auth_ref":{"title":"Auth Ref","type":"string"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"default_branch":{"title":"Default Branch","type":"string"},"id":{"title":"Id","type":"string"},"last_merged_proposal":{"anyOf":[{"$ref":"#/components/schemas/ProposalSummary"},{"type":"null"}]},"name":{"title":"Name","type":"string"},"pr_base_branch":{"title":"Pr Base Branch","type":"string"},"provider":{"const":"github","title":"Provider","type":"string"},"repo_url":{"title":"Repo Url","type":"string"},"webhook_registration_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Webhook Registration Error"},"webhook_secret_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Webhook Secret Ref"}},"required":["id","name","provider","repo_url","default_branch","pr_base_branch","auth_ref","webhook_secret_ref","webhook_registration_error","created_at"],"title":"ConfigRepoDetail","type":"object"},"ConfigReposListResponse":{"description":"``GET /api/v1/config-repos`` response.","properties":{"data":{"items":{"$ref":"#/components/schemas/ConfigRepoDetail"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"ConfigReposListResponse","type":"object"},"ConnectionTestRequest":{"description":"Body for ``POST /api/v1/clusters/test-connection`` (infra_adapter_solr Story A9).\n\nSame shape as ``CreateClusterRequest`` minus the persisted-only fields\n(``name``, ``environment``, ``notes``, ``target_filter``). ``engine_type``\n+ ``auth_kind`` are typed as ``str`` (not Literal) so a bad value yields\nthe project-standard 400 envelope rather than a raw 422 — same convention\nas ``CreateClusterRequest``.","properties":{"auth_kind":{"maxLength":64,"minLength":1,"title":"Auth Kind","type":"string"},"base_url":{"maxLength":512,"minLength":1,"title":"Base Url","type":"string"},"credentials_ref":{"maxLength":128,"minLength":1,"title":"Credentials Ref","type":"string"},"engine_config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Engine Config"},"engine_type":{"maxLength":64,"minLength":1,"title":"Engine Type","type":"string"}},"required":["engine_type","base_url","auth_kind","credentials_ref"],"title":"ConnectionTestRequest","type":"object"},"ConnectionTestResult":{"description":"Response for ``POST /api/v1/clusters/test-connection``.\n\nAlways 200 — reachable vs unreachable surfaces via ``reachable`` +\n``status`` fields. The endpoint is a diagnostic, never a mutation,\nso it never returns 503; invalid engine×auth pairings 400 BEFORE the\nnetwork call. (Cycle-delta F1.)","properties":{"engine_capabilities":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Engine Capabilities"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"reachable":{"title":"Reachable","type":"boolean"},"status":{"enum":["green","yellow","red","unreachable"],"title":"Status","type":"string"},"version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Version"}},"required":["reachable","status"],"title":"ConnectionTestResult","type":"object"},"ConvergenceShape":{"description":"Where the winner sits in the Optuna trial sequence + the classified regime.","properties":{"best_at_trial":{"title":"Best At Trial","type":"integer"},"regime":{"enum":["early_held","late_rising","noisy"],"title":"Regime","type":"string"},"total_trials":{"title":"Total Trials","type":"integer"}},"required":["best_at_trial","total_trials","regime"],"title":"ConvergenceShape","type":"object"},"ConversationDetail":{"description":"``GET /api/v1/conversations/{id}`` response.","properties":{"created_at":{"format":"date-time","title":"Created At","type":"string"},"id":{"title":"Id","type":"string"},"messages":{"items":{"$ref":"#/components/schemas/MessageWire"},"title":"Messages","type":"array"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"}},"required":["id","title","created_at","messages"],"title":"ConversationDetail","type":"object"},"ConversationSummary":{"description":"``GET /api/v1/conversations`` row + ``POST`` 201 body.\n\n``last_message_preview`` is the most recent user / assistant message's\n``content.text``, truncated at the repo layer to 120 chars (with ``…``\nsuffix when cut). Tool-role rows and assistant rows whose ``content.kind``\nis ``system_notice`` are skipped. ``None`` for brand-new conversations\nwith no qualifying messages — see ``chore_chat_last_message_preview``.\n\n``last_message_at`` is the ``created_at`` of that same row, or ``None``\nfor empty conversations. The list page uses it to render \"when did\nanyone last touch this thread\" instead of the conversation's\n``created_at``.","properties":{"created_at":{"format":"date-time","title":"Created At","type":"string"},"id":{"title":"Id","type":"string"},"last_message_at":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Last Message At"},"last_message_preview":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Message Preview"},"message_count":{"title":"Message Count","type":"integer"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"}},"required":["id","title","created_at","message_count"],"title":"ConversationSummary","type":"object"},"ConversationsListResponse":{"description":"``GET /api/v1/conversations`` response.","properties":{"data":{"items":{"$ref":"#/components/schemas/ConversationSummary"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"ConversationsListResponse","type":"object"},"CreateClusterRequest":{"description":"Request body for ``POST /api/v1/clusters``.\n\nSee module docstring for the deliberate ``str`` vs ``Literal`` split.","properties":{"auth_kind":{"maxLength":64,"minLength":1,"title":"Auth Kind","type":"string"},"base_url":{"maxLength":512,"minLength":1,"title":"Base Url","type":"string"},"credentials_ref":{"maxLength":128,"minLength":1,"title":"Credentials Ref","type":"string"},"engine_config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Engine Config"},"engine_type":{"maxLength":64,"minLength":1,"title":"Engine Type","type":"string"},"environment":{"enum":["prod","staging","dev"],"title":"Environment","type":"string"},"name":{"maxLength":128,"minLength":1,"pattern":"^[a-z0-9][a-z0-9-]*$","title":"Name","type":"string"},"notes":{"anyOf":[{"maxLength":2000,"type":"string"},{"type":"null"}],"title":"Notes"},"target_filter":{"anyOf":[{"maxLength":256,"minLength":1,"type":"string"},{"type":"null"}],"description":"Optional glob pattern (fnmatch.fnmatchcase: *, ?, [seq], [!seq]; no brace expansion). Scopes GET /clusters/{id}/targets to matching index names. Null = no filter.","title":"Target Filter"}},"required":["name","engine_type","environment","base_url","auth_kind","credentials_ref"],"title":"CreateClusterRequest","type":"object"},"CreateConfigRepoRequest":{"description":"Body of ``POST /api/v1/config-repos`` (FR-3).\n\n``provider`` is server-derived from ``repo_url`` (cycle-2 F4 from\nspec review) — NOT in the payload. The validator enforces a strict\nGitHub URL pattern; non-GitHub URLs surface as 400\n``UNSUPPORTED_PROVIDER`` at the router layer.","properties":{"auth_ref":{"maxLength":128,"minLength":1,"pattern":"^[a-zA-Z0-9_-]+$","title":"Auth Ref","type":"string"},"default_branch":{"default":"main","maxLength":128,"minLength":1,"title":"Default Branch","type":"string"},"name":{"maxLength":128,"minLength":1,"pattern":"^[a-z0-9][a-z0-9-]*$","title":"Name","type":"string"},"pr_base_branch":{"default":"main","maxLength":128,"minLength":1,"title":"Pr Base Branch","type":"string"},"repo_url":{"maxLength":512,"minLength":1,"title":"Repo Url","type":"string"},"webhook_secret_ref":{"anyOf":[{"maxLength":128,"pattern":"^[a-zA-Z0-9_-]+$","type":"string"},{"type":"null"}],"title":"Webhook Secret Ref"}},"required":["name","repo_url","auth_ref"],"title":"CreateConfigRepoRequest","type":"object"},"CreateConversationRequest":{"description":"``POST /api/v1/conversations`` body.","properties":{"title":{"anyOf":[{"maxLength":200,"type":"string"},{"type":"null"}],"title":"Title"}},"title":"CreateConversationRequest","type":"object"},"CreateJudgmentListFromUbiRequest":{"description":"Body for ``POST /api/v1/judgments/generate-from-ubi`` (Story 3.2 / FR-3).\n\nMirrors :class:`backend.app.services.agent_judgments_dispatch.UbiJudgmentGenerationRequest`.\nThe ``@model_validator(mode=\"after\")`` enforces the conditional\nrequiredness of ``current_template_id`` + ``rubric`` per the hybrid\nconverter: REQUIRED when ``converter == 'hybrid_ubi_llm'`` (the LLM-\nfill path needs both); FORBIDDEN otherwise (pure UBI never calls\nthe LLM so accepting them silently would mask operator error).","properties":{"cluster_id":{"maxLength":36,"minLength":1,"title":"Cluster Id","type":"string"},"converter":{"enum":["ctr_threshold","dwell_time","hybrid_ubi_llm"],"title":"Converter","type":"string"},"converter_config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Converter Config"},"current_template_id":{"anyOf":[{"maxLength":36,"minLength":36,"type":"string"},{"type":"null"}],"title":"Current Template Id"},"description":{"anyOf":[{"maxLength":2000,"type":"string"},{"type":"null"}],"title":"Description"},"llm_fill_threshold":{"anyOf":[{"minimum":1.0,"type":"integer"},{"type":"null"}],"default":20,"title":"Llm Fill Threshold"},"mapping_strategy":{"default":"reject","enum":["reject","first_match","most_recent"],"title":"Mapping Strategy","type":"string"},"min_impressions_threshold":{"anyOf":[{"minimum":1.0,"type":"integer"},{"type":"null"}],"default":100,"title":"Min Impressions Threshold"},"name":{"maxLength":256,"minLength":1,"title":"Name","type":"string"},"query_set_id":{"maxLength":36,"minLength":1,"title":"Query Set Id","type":"string"},"rubric":{"anyOf":[{"minLength":1,"type":"string"},{"type":"null"}],"title":"Rubric"},"since":{"format":"date-time","title":"Since","type":"string"},"target":{"maxLength":256,"minLength":1,"title":"Target","type":"string"},"until":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Until"}},"required":["name","query_set_id","cluster_id","target","since","converter"],"title":"CreateJudgmentListFromUbiRequest","type":"object"},"CreateJudgmentListGenerateRequest":{"description":"Body for ``POST /api/v1/judgments/generate`` (Story 3.1).","properties":{"cluster_id":{"maxLength":36,"minLength":1,"title":"Cluster Id","type":"string"},"current_template_id":{"maxLength":36,"minLength":1,"title":"Current Template Id","type":"string"},"description":{"anyOf":[{"maxLength":2000,"type":"string"},{"type":"null"}],"title":"Description"},"name":{"maxLength":256,"minLength":1,"title":"Name","type":"string"},"query_set_id":{"maxLength":36,"minLength":1,"title":"Query Set Id","type":"string"},"rubric":{"minLength":1,"title":"Rubric","type":"string"},"target":{"maxLength":256,"minLength":1,"title":"Target","type":"string"}},"required":["name","query_set_id","cluster_id","target","current_template_id","rubric"],"title":"CreateJudgmentListGenerateRequest","type":"object"},"CreateProposalRequest":{"description":"Body of ``POST /api/v1/proposals`` (manual proposal creation, FR-4 / AC-6).","properties":{"cluster_id":{"maxLength":36,"minLength":1,"title":"Cluster Id","type":"string"},"config_diff":{"additionalProperties":true,"title":"Config Diff","type":"object"},"metric_delta":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Metric Delta"},"template_id":{"maxLength":36,"minLength":1,"title":"Template Id","type":"string"}},"required":["cluster_id","template_id","config_diff"],"title":"CreateProposalRequest","type":"object"},"CreateQuerySetRequest":{"description":"``POST /api/v1/query-sets`` body.\n\n``cluster_id`` is required because Phase 1's shipped schema has\n``query_sets.cluster_id NOT NULL``. Spec FR-3 wording (``cluster_id?``)\nis documented drift tracked at\n``docs/00_overview/planned_features/chore_spec_query_set_cluster_id_drift/idea.md``.","properties":{"cluster_id":{"maxLength":36,"minLength":1,"title":"Cluster Id","type":"string"},"description":{"anyOf":[{"maxLength":2000,"type":"string"},{"type":"null"}],"title":"Description"},"name":{"maxLength":256,"minLength":1,"title":"Name","type":"string"}},"required":["name","cluster_id"],"title":"CreateQuerySetRequest","type":"object"},"CreateQueryTemplateRequest":{"description":"Request body for ``POST /api/v1/query-templates``.","properties":{"body":{"minLength":1,"title":"Body","type":"string"},"declared_params":{"additionalProperties":{"type":"string"},"title":"Declared Params","type":"object"},"engine_type":{"enum":["elasticsearch","opensearch","solr"],"title":"Engine Type","type":"string"},"name":{"maxLength":256,"minLength":1,"title":"Name","type":"string"},"parent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Id"}},"required":["name","engine_type","body"],"title":"CreateQueryTemplateRequest","type":"object"},"CreateStudyRequest":{"description":"``POST /api/v1/studies`` body.\n\n``search_space`` is validated post-Pydantic-parse via\n:class:`backend.app.domain.study.search_space.SearchSpace` so\n:exc:`pydantic.ValidationError` produces the spec's 400\n``INVALID_SEARCH_SPACE`` (per Story 3.3 task 2).\n\nfeat_digest_executable_followups Story 4.2 — optional ``parent`` field\nrecords the parent proposal + followup-index lineage when the study\nwas spawned from a digest \"Run this followup\" action (FR-11).","properties":{"cluster_id":{"maxLength":36,"minLength":1,"title":"Cluster Id","type":"string"},"config":{"$ref":"#/components/schemas/StudyConfigSpec"},"judgment_list_id":{"maxLength":36,"minLength":1,"title":"Judgment List Id","type":"string"},"name":{"maxLength":256,"minLength":1,"title":"Name","type":"string"},"objective":{"$ref":"#/components/schemas/ObjectiveSpec"},"parent":{"anyOf":[{"$ref":"#/components/schemas/ParentFollowupRef"},{"type":"null"}]},"parent_study_id":{"anyOf":[{"maxLength":36,"minLength":36,"type":"string"},{"type":"null"}],"description":"feat_study_clone_from_previous FR-7 — when the operator clones an existing study via the study-detail Clone button, this carries the source study's id. Server validates existence (404 PARENT_STUDY_NOT_FOUND) and same-cluster (422 PARENT_STUDY_WRONG_CLUSTER) before persisting to studies.parent_study_id. Independent of the proposal-lineage 'parent' field (D-5); both may be set.","title":"Parent Study Id"},"query_set_id":{"maxLength":36,"minLength":1,"title":"Query Set Id","type":"string"},"search_space":{"additionalProperties":true,"title":"Search Space","type":"object"},"target":{"maxLength":256,"minLength":1,"title":"Target","type":"string"},"template_id":{"maxLength":36,"minLength":1,"title":"Template Id","type":"string"}},"required":["name","cluster_id","target","template_id","query_set_id","judgment_list_id","search_space","objective","config"],"title":"CreateStudyRequest","type":"object"},"CurvePoint":{"description":"One point on the best-so-far curve.\n\n``trial_number`` is the trial's ``optuna_trial_number`` (the canonical\n\"trial order within the study\" field — see ``auto_followup.py`` module\ndocstring for why we sort by this rather than ``started_at``).\n``best_so_far`` is the running extremum of ``primary_metric`` over all\nearlier trials, sign-corrected to the study's optimization direction.","properties":{"best_so_far":{"title":"Best So Far","type":"number"},"trial_number":{"title":"Trial Number","type":"integer"}},"required":["trial_number","best_so_far"],"title":"CurvePoint","type":"object"},"DigestResponse":{"description":"Body of ``GET /api/v1/studies/{id}/digest`` (FR-3 / AC-3).\n\nfeat_digest_executable_followups Story 4.1 — ``suggested_followups`` is\nnow a discriminated-union list (NarrowFollowup | WidenFollowup |\nTextFollowup), populated by the digest handler via\n``parse_followup_list(digest.suggested_followups, ...)`` so legacy or\nmalformed JSONB payloads never crash the response.","properties":{"generated_at":{"format":"date-time","title":"Generated At","type":"string"},"generated_by":{"title":"Generated By","type":"string"},"id":{"title":"Id","type":"string"},"narrative":{"title":"Narrative","type":"string"},"parameter_importance":{"additionalProperties":{"type":"number"},"title":"Parameter Importance","type":"object"},"recommended_config":{"additionalProperties":true,"title":"Recommended Config","type":"object"},"study_id":{"title":"Study Id","type":"string"},"suggested_followups":{"items":{"$ref":"#/components/schemas/FollowupItem"},"title":"Suggested Followups","type":"array"}},"required":["id","study_id","narrative","parameter_importance","recommended_config","suggested_followups","generated_by","generated_at"],"title":"DigestResponse","type":"object"},"Document":{"description":"A single document by ID — return shape of ``SearchAdapter.get_document``.\n\nMirrors :class:`ScoredHit` minus ``score`` (browsing doesn't need scoring).\n``source`` is ``None`` when the engine's index has ``_source: false`` mapping.","properties":{"doc_id":{"minLength":1,"title":"Doc Id","type":"string"},"source":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Source"}},"required":["doc_id"],"title":"Document","type":"object"},"DocumentListResponse":{"description":"``GET /api/v1/clusters/{cluster_id}/targets/{target}/documents`` response.\n\n``next_cursor`` opaque-encodes the ES ``hits[-1].sort`` array of the\nlast visible row when ``has_more`` is True (see\n``backend.app.api.v1._documents_cursor``). The ``X-Total-Count`` header\non the response carries the engine's ``hits.total.value``.","properties":{"data":{"items":{"$ref":"#/components/schemas/DocumentSummary"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"DocumentListResponse","type":"object"},"DocumentSummary":{"description":"One row in the documents list (per FR-3 / FR-8).\n\n``source`` is the *truncated* preview emitted by\n``backend.app.services.documents.truncate_source_for_list``. The detail\nendpoint returns the untruncated ``Document.source``.","properties":{"doc_id":{"minLength":1,"title":"Doc Id","type":"string"},"source":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Source"}},"required":["doc_id","source"],"title":"DocumentSummary","type":"object"},"FieldSpec":{"description":"One field returned by ``get_schema``.","properties":{"analyzer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Analyzer"},"doc_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Doc Count"},"name":{"title":"Name","type":"string"},"type":{"title":"Type","type":"string"}},"required":["name","type"],"title":"FieldSpec","type":"object"},"FloatParam":{"additionalProperties":false,"description":"Continuous float parameter.\n\n``log=True`` enables log-uniform sampling\n(Optuna's ``suggest_float(..., log=True)``); requires ``low > 0``.","properties":{"high":{"title":"High","type":"number"},"log":{"default":false,"title":"Log","type":"boolean"},"low":{"title":"Low","type":"number"},"type":{"const":"float","title":"Type","type":"string"}},"required":["type","low","high"],"title":"FloatParam","type":"object"},"FollowupItem":{"discriminator":{"mapping":{"narrow":"#/components/schemas/NarrowFollowup","swap_template":"#/components/schemas/SwapTemplateFollowup","text":"#/components/schemas/TextFollowup","widen":"#/components/schemas/WidenFollowup"},"propertyName":"kind"},"oneOf":[{"$ref":"#/components/schemas/NarrowFollowup"},{"$ref":"#/components/schemas/WidenFollowup"},{"$ref":"#/components/schemas/TextFollowup"},{"$ref":"#/components/schemas/SwapTemplateFollowup"}]},"GenerateJudgmentsResponse":{"description":"Response of ``POST /api/v1/judgments/generate``.\n\nPer GPT-5.5 cycle 1 F5 — the endpoint registers a typed\n``response_model`` so OpenAPI introspection + contract tests can verify\nthe wire shape.","properties":{"judgment_list_id":{"title":"Judgment List Id","type":"string"},"status":{"const":"generating","title":"Status","type":"string"}},"required":["judgment_list_id","status"],"title":"GenerateJudgmentsResponse","type":"object"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"HeadlineShape":{"description":"Top-line metric value + N(queries) used in the CI.\n\n``metric`` uses ``str`` (not ``ObjectiveMetric``) to avoid a circular\nimport: ``schemas.py`` imports ``ConfidenceShape`` from here, so this\nmodule cannot import back from ``schemas.py``. The upstream value is\nalready validated by the existing ``ObjectiveMetric`` Literal at the\ncreate-study endpoint (``schemas.py:214``).","properties":{"k":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"K"},"metric":{"title":"Metric","type":"string"},"n_queries":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"N Queries"},"value":{"title":"Value","type":"number"}},"required":["metric","value","k","n_queries"],"title":"HeadlineShape","type":"object"},"HealthCheckResult":{"description":"Wire shape of the per-cluster health probe (mirrors ``HealthStatus``).","properties":{"checked_at":{"title":"Checked At","type":"string"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"status":{"enum":["green","yellow","red","unreachable"],"title":"Status","type":"string"},"version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Version"}},"required":["status","checked_at"],"title":"HealthCheckResult","type":"object"},"HealthResponse":{"description":"The /healthz response body. Same shape for HTTP 200 and 503.","properties":{"openai_capabilities":{"$ref":"#/components/schemas/OpenAICapabilities"},"openai_endpoint":{"description":"Configured OPENAI_BASE_URL","title":"Openai Endpoint","type":"string"},"status":{"enum":["ok","degraded"],"title":"Status","type":"string"},"subsystems":{"$ref":"#/components/schemas/Subsystems"},"uptime_seconds":{"description":"Seconds since the API process started","title":"Uptime Seconds","type":"integer"},"version":{"description":"Application version (relyloop_git_sha)","title":"Version","type":"string"}},"required":["status","subsystems","openai_endpoint","openai_capabilities","version","uptime_seconds"],"title":"HealthResponse","type":"object"},"ImportJudgmentItem":{"description":"One row in :class:`ImportJudgmentListRequest`.","properties":{"doc_id":{"maxLength":512,"minLength":1,"title":"Doc Id","type":"string"},"notes":{"anyOf":[{"maxLength":2000,"type":"string"},{"type":"null"}],"title":"Notes"},"query_id":{"maxLength":36,"minLength":1,"title":"Query Id","type":"string"},"rating":{"enum":[0,1,2,3],"title":"Rating","type":"integer"}},"required":["query_id","doc_id","rating"],"title":"ImportJudgmentItem","type":"object"},"ImportJudgmentListRequest":{"description":"Body for ``POST /api/v1/judgment-lists/import`` (Story 3.2).","properties":{"cluster_id":{"maxLength":36,"minLength":1,"title":"Cluster Id","type":"string"},"description":{"anyOf":[{"maxLength":2000,"type":"string"},{"type":"null"}],"title":"Description"},"judgments":{"items":{"$ref":"#/components/schemas/ImportJudgmentItem"},"maxItems":100000,"minItems":1,"title":"Judgments","type":"array"},"name":{"maxLength":256,"minLength":1,"title":"Name","type":"string"},"query_set_id":{"maxLength":36,"minLength":1,"title":"Query Set Id","type":"string"},"rubric":{"minLength":1,"title":"Rubric","type":"string"},"target":{"maxLength":256,"minLength":1,"title":"Target","type":"string"}},"required":["name","query_set_id","cluster_id","target","rubric","judgments"],"title":"ImportJudgmentListRequest","type":"object"},"IntParam":{"additionalProperties":false,"description":"Integer parameter inclusive of both bounds.","properties":{"high":{"title":"High","type":"integer"},"low":{"title":"Low","type":"integer"},"type":{"const":"int","title":"Type","type":"string"}},"required":["type","low","high"],"title":"IntParam","type":"object"},"JudgmentListDetail":{"description":"``GET /api/v1/judgment-lists/{id}`` response.\n\nNote: ``generation_params`` is populated for UBI lists (feat_ubi_judgments\nStory 1.1's JSONB column) and NULL for LLM lists. The Story 4.3 UI\n(```` + ````) reads the\npayload to discriminate UBI/hybrid lists and to reconstruct the\noriginal request for the ambiguous-skip \"Re-run with most_recent\"\naffordance.","properties":{"calibration":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Calibration"},"cluster_id":{"title":"Cluster Id","type":"string"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"current_template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Template Id"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"failed_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failed Reason"},"generation_params":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Generation Params"},"id":{"title":"Id","type":"string"},"judgment_count":{"title":"Judgment Count","type":"integer"},"name":{"title":"Name","type":"string"},"query_set_id":{"title":"Query Set Id","type":"string"},"rubric":{"title":"Rubric","type":"string"},"source_breakdown":{"$ref":"#/components/schemas/_SourceBreakdown"},"status":{"enum":["generating","complete","failed"],"title":"Status","type":"string"},"target":{"title":"Target","type":"string"}},"required":["id","name","description","query_set_id","cluster_id","target","current_template_id","rubric","status","failed_reason","judgment_count","source_breakdown","calibration","generation_params","created_at"],"title":"JudgmentListDetail","type":"object"},"JudgmentListJudgmentsResponse":{"description":"``GET /api/v1/judgment-lists/{id}/judgments`` response.","properties":{"data":{"items":{"$ref":"#/components/schemas/JudgmentRow"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"JudgmentListJudgmentsResponse","type":"object"},"JudgmentListListResponse":{"description":"``GET /api/v1/judgment-lists`` response.","properties":{"data":{"items":{"$ref":"#/components/schemas/JudgmentListSummary"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"JudgmentListListResponse","type":"object"},"JudgmentListRef":{"description":"One entry in the ``QUERY_HAS_JUDGMENTS`` 409 envelope.\n\nLives in ``detail.judgment_lists``. Maps from the repo-layer\n:class:`backend.app.db.repo.judgment.JudgmentListRefRow` at the\nrouter boundary.","properties":{"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"}},"required":["id","name"],"title":"JudgmentListRef","type":"object"},"JudgmentListSummary":{"description":"List-view row on ``GET /api/v1/judgment-lists``.","properties":{"cluster_id":{"title":"Cluster Id","type":"string"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"query_set_id":{"title":"Query Set Id","type":"string"},"status":{"enum":["generating","complete","failed"],"title":"Status","type":"string"},"target":{"title":"Target","type":"string"}},"required":["id","name","description","query_set_id","cluster_id","target","status","created_at"],"title":"JudgmentListSummary","type":"object"},"JudgmentRow":{"description":"``GET /api/v1/judgment-lists/{id}/judgments`` row + PATCH response.","properties":{"confidence":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Confidence"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"doc_id":{"title":"Doc Id","type":"string"},"id":{"title":"Id","type":"string"},"judgment_list_id":{"title":"Judgment List Id","type":"string"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"query_id":{"title":"Query Id","type":"string"},"rater_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rater Ref"},"rating":{"enum":[0,1,2,3],"title":"Rating","type":"integer"},"source":{"enum":["llm","human","click"],"title":"Source","type":"string"}},"required":["id","judgment_list_id","query_id","doc_id","rating","source","rater_ref","confidence","notes","created_at"],"title":"JudgmentRow","type":"object"},"LateTrialStddevShape":{"description":"Sample stddev of ``primary_metric`` over the late-trial window.","properties":{"min_window_required":{"title":"Min Window Required","type":"integer"},"value":{"title":"Value","type":"number"},"window_size":{"title":"Window Size","type":"integer"}},"required":["value","window_size","min_window_required"],"title":"LateTrialStddevShape","type":"object"},"MessageWire":{"description":"One row of ``GET /api/v1/conversations/{id}.messages``.","properties":{"content":{"additionalProperties":true,"title":"Content","type":"object"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"id":{"title":"Id","type":"string"},"role":{"enum":["user","assistant","tool"],"title":"Role","type":"string"},"tool_calls":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Tool Calls"}},"required":["id","role","content","created_at"],"title":"MessageWire","type":"object"},"NarrowFollowup":{"additionalProperties":false,"description":"A 'narrow' followup — re-run with a tighter range than the parent.","properties":{"kind":{"const":"narrow","title":"Kind","type":"string"},"rationale":{"title":"Rationale","type":"string"},"search_space":{"$ref":"#/components/schemas/SearchSpace"}},"required":["kind","rationale","search_space"],"title":"NarrowFollowup","type":"object"},"ObjectiveSpec":{"description":"Wire shape of ``studies.objective`` (write-side validated at create).\n\n``k`` is required for ``ndcg`` / ``precision`` / ``recall`` (per\nstandard IR-evaluation conventions: those metrics are computed at a\ncutoff rank). ``map`` accepts ``k`` optionally; ``mrr`` / ``err`` ignore\nit. The model_validator enforces this so a malformed objective\nsurfaces as 400 ``INVALID_SEARCH_SPACE`` / 422 ``VALIDATION_ERROR``\nat study-create time rather than failing later inside ``run_trial``\nwhen the worker computes the metric.","properties":{"direction":{"default":"maximize","enum":["maximize","minimize"],"title":"Direction","type":"string"},"k":{"anyOf":[{"enum":[1,3,5,10,20,50,100],"type":"integer"},{"type":"null"}],"title":"K"},"metric":{"enum":["ndcg","map","precision","recall","mrr"],"title":"Metric","type":"string"}},"required":["metric"],"title":"ObjectiveSpec","type":"object"},"OpenAICapabilities":{"description":"Cached results of the OpenAI capability check (Story 3.3 populates Redis).\n\nStep 1 (``models_endpoint``) is reported first because it gates the rest:\nwhen it fails, the other three are reported as ``\"untested\"``. The\n``models_endpoint_status_code`` field is required-but-nullable\n(per ``bug_openai_capability_check_incapable_on_valid_key`` spec §19 D-3/D-8)\n— always present in the JSON, ``null`` when not applicable. This lets\noperators distinguish ``401 -> bad key``, ``429 -> quota``,\n``5xx -> upstream outage``, ``null -> network unreachable / cache miss``.","properties":{"chat":{"description":"Chat completion probe result","enum":["ok","fail","untested"],"title":"Chat","type":"string"},"function_calling":{"description":"Function-calling probe result (tool_choice=required)","enum":["ok","fail","untested"],"title":"Function Calling","type":"string"},"models_endpoint":{"description":"GET /models probe outcome. 'ok' / 'fail' are projected from CapabilityResult.models_endpoint; 'untested' is the cache-miss default, matching the existing chat / function_calling / structured_output cache-miss handling.","enum":["ok","fail","untested"],"title":"Models Endpoint","type":"string"},"models_endpoint_status_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"HTTP status code from the GET /models probe when it HTTP-failed (>= 400). null for the success path, network-class failure (timeout / DNS / connection-refused), or cache miss. Required-but-nullable: the JSON key is always present with explicit null when no value, never omitted.","title":"Models Endpoint Status Code"},"structured_output":{"description":"JSON-schema response_format probe result","enum":["ok","fail","untested"],"title":"Structured Output","type":"string"}},"required":["models_endpoint","models_endpoint_status_code","chat","function_calling","structured_output"],"title":"OpenAICapabilities","type":"object"},"OpenPrResponse":{"description":"Body of ``POST /api/v1/proposals/{id}/open_pr`` (FR-1).\n\nReturned with HTTP 202 on successful enqueue. Status is always\n``'pending'`` at enqueue time; the worker flips it to ``'pr_opened'``\nafter the PR is open.","properties":{"message":{"title":"Message","type":"string"},"proposal_id":{"title":"Proposal Id","type":"string"},"status":{"const":"pending","title":"Status","type":"string"}},"required":["proposal_id","status","message"],"title":"OpenPrResponse","type":"object"},"OverrideJudgmentRequest":{"description":"Body for ``PATCH /api/v1/judgment-lists/{id}/judgments/{judgment_id}``.\n\n``rating`` is INTENTIONALLY unbounded at the Pydantic layer — spec §8.5\nrequires out-of-range failures to surface as 400 ``INVALID_RATING`` (not\nPydantic's default 422 ``VALIDATION_ERROR``). The handler validates the\nvalue manually and raises the domain code (per GPT-5.5 cycle 1 F4).","properties":{"notes":{"anyOf":[{"maxLength":2000,"type":"string"},{"type":"null"}],"title":"Notes"},"rating":{"title":"Rating","type":"integer"}},"required":["rating"],"title":"OverrideJudgmentRequest","type":"object"},"ParentFollowupRef":{"description":"Optional lineage payload on ``POST /api/v1/studies``.\n\nfeat_digest_executable_followups FR-11 — when the operator clicks\n\"Run this followup\" on a proposal's digest card, the create-study\npayload carries the parent proposal's id + the 0-based index into\nthe digest's ``suggested_followups`` array so the spawned study\nremembers where it came from.\n\n``proposal_id`` is a UUIDv7 (36-char hex). The exact-length bound\nforces malformed strings to surface as 422 ``VALIDATION_ERROR``\nrather than reach the DB FK check and emerge as a 404\n``PROPOSAL_NOT_FOUND``.","properties":{"followup_index":{"minimum":0.0,"title":"Followup Index","type":"integer"},"proposal_id":{"maxLength":36,"minLength":36,"title":"Proposal Id","type":"string"}},"required":["proposal_id","followup_index"],"title":"ParentFollowupRef","type":"object"},"PerQueryOutcomesShape":{"description":"Per-query outcome counts + the top-5 named regressors and improvers.","properties":{"comparison_against":{"enum":["runner_up","baseline"],"title":"Comparison Against","type":"string"},"improved":{"title":"Improved","type":"integer"},"regressed":{"title":"Regressed","type":"integer"},"top_improvers":{"default":[],"items":{"$ref":"#/components/schemas/RegressorRowShape"},"title":"Top Improvers","type":"array"},"top_regressors":{"items":{"$ref":"#/components/schemas/RegressorRowShape"},"title":"Top Regressors","type":"array"},"unchanged":{"title":"Unchanged","type":"integer"}},"required":["improved","unchanged","regressed","comparison_against","top_regressors"],"title":"PerQueryOutcomesShape","type":"object"},"ProposalDetail":{"description":"Body of the proposal detail endpoints.\n\nUsed by ``GET /api/v1/proposals/{id}``, ``POST /api/v1/proposals``,\nand ``POST /api/v1/proposals/{id}/reject``.","properties":{"cluster":{"$ref":"#/components/schemas/_ClusterEmbed"},"config_diff":{"additionalProperties":true,"title":"Config Diff","type":"object"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"digest":{"anyOf":[{"$ref":"#/components/schemas/_DigestEmbed"},{"type":"null"}]},"id":{"title":"Id","type":"string"},"is_currently_live":{"default":false,"title":"Is Currently Live","type":"boolean"},"metric_delta":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Metric Delta"},"pr_merged_at":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Pr Merged At"},"pr_open_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pr Open Error"},"pr_state":{"anyOf":[{"enum":["open","closed","merged"],"type":"string"},{"type":"null"}],"title":"Pr State"},"pr_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pr Url"},"rejected_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rejected Reason"},"status":{"enum":["pending","pr_opened","pr_merged","rejected"],"title":"Status","type":"string"},"study_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Study Id"},"study_summary":{"anyOf":[{"$ref":"#/components/schemas/_StudySummary"},{"type":"null"}]},"study_trial_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Study Trial Id"},"template":{"$ref":"#/components/schemas/_TemplateEmbed"}},"required":["id","study_id","study_summary","study_trial_id","cluster","template","config_diff","metric_delta","status","pr_url","pr_state","pr_merged_at","pr_open_error","rejected_reason","digest","created_at"],"title":"ProposalDetail","type":"object"},"ProposalSummary":{"description":"Row in the ``GET /api/v1/proposals`` list response.","properties":{"cluster":{"$ref":"#/components/schemas/_ClusterEmbed"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"id":{"title":"Id","type":"string"},"is_currently_live":{"default":false,"title":"Is Currently Live","type":"boolean"},"metric_delta":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Metric Delta"},"pr_state":{"anyOf":[{"enum":["open","closed","merged"],"type":"string"},{"type":"null"}],"title":"Pr State"},"pr_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pr Url"},"status":{"enum":["pending","pr_opened","pr_merged","rejected"],"title":"Status","type":"string"},"study_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Study Id"},"template":{"$ref":"#/components/schemas/_TemplateEmbed"}},"required":["id","study_id","cluster","template","status","pr_state","pr_url","metric_delta","created_at"],"title":"ProposalSummary","type":"object"},"ProposalsListResponse":{"description":"Body of ``GET /api/v1/proposals``.","properties":{"data":{"items":{"$ref":"#/components/schemas/ProposalSummary"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"ProposalsListResponse","type":"object"},"QueryHasJudgmentsDetail":{"description":"The ``detail`` object of a 409 ``QUERY_HAS_JUDGMENTS`` response.\n\nExtends the canonical ``{error_code, message, retryable}`` envelope\nwith two structured fields the frontend consumes directly\n(``judgment_lists`` + ``overflow_count``). Wired into the FastAPI\nroute's ``responses={409: {\"model\": QueryHasJudgmentsEnvelope}}`` so\nthe OpenAPI schema documents the contract.","properties":{"error_code":{"const":"QUERY_HAS_JUDGMENTS","title":"Error Code","type":"string"},"judgment_lists":{"items":{"$ref":"#/components/schemas/JudgmentListRef"},"title":"Judgment Lists","type":"array"},"message":{"title":"Message","type":"string"},"overflow_count":{"title":"Overflow Count","type":"integer"},"retryable":{"const":false,"title":"Retryable","type":"boolean"}},"required":["error_code","message","retryable","judgment_lists","overflow_count"],"title":"QueryHasJudgmentsDetail","type":"object"},"QueryHasJudgmentsEnvelope":{"description":"Top-level 409 wrapper (FastAPI nests under ``detail`` for HTTPException).","properties":{"detail":{"$ref":"#/components/schemas/QueryHasJudgmentsDetail"}},"required":["detail"],"title":"QueryHasJudgmentsEnvelope","type":"object"},"QueryListResponse":{"description":"``GET /api/v1/query-sets/{set_id}/queries`` response.","properties":{"data":{"items":{"$ref":"#/components/schemas/QueryRow"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"QueryListResponse","type":"object"},"QueryRow":{"description":"Wire row returned by the per-query GET + PATCH endpoints.\n\nUsed by both ``GET /api/v1/query-sets/{set_id}/queries`` and\n``PATCH /api/v1/query-sets/{set_id}/queries/{query_id}``.\n``judgment_count`` is a derived field — single batched GROUP BY in the\nrouter via :func:`backend.app.db.repo.judgment.count_judgments_per_query`.","properties":{"id":{"title":"Id","type":"string"},"judgment_count":{"title":"Judgment Count","type":"integer"},"query_metadata":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Query Metadata"},"query_text":{"title":"Query Text","type":"string"},"reference_answer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reference Answer"}},"required":["id","query_text","reference_answer","query_metadata","judgment_count"],"title":"QueryRow","type":"object"},"QuerySetDetail":{"description":"``GET /api/v1/query-sets/{id}`` response.","properties":{"cluster_id":{"title":"Cluster Id","type":"string"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"query_count":{"title":"Query Count","type":"integer"}},"required":["id","name","description","cluster_id","query_count","created_at"],"title":"QuerySetDetail","type":"object"},"QuerySetListResponse":{"description":"``GET /api/v1/query-sets`` response.","properties":{"data":{"items":{"$ref":"#/components/schemas/QuerySetSummary"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"QuerySetListResponse","type":"object"},"QuerySetSummary":{"description":"List-view shape.\n\n``query_count`` is the number of queries in the set. It is resolved\nvia a single batched ``GROUP BY query_set_id`` aggregate per page\n(``repo.count_queries_for_sets``), NOT a per-row count — so the\nlist endpoint stays at a fixed 2 queries (the page + the count\naggregate) regardless of page size. This is the same no-N+1 pattern\n``feat_studies_convergence_visibility`` (PR #421) used for the\nstudies-list ``trial_count`` field.","properties":{"cluster_id":{"title":"Cluster Id","type":"string"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"query_count":{"title":"Query Count","type":"integer"}},"required":["id","name","cluster_id","query_count","created_at"],"title":"QuerySetSummary","type":"object"},"QueryTemplateDetail":{"description":"``GET /api/v1/query-templates/{id}`` response.","properties":{"body":{"title":"Body","type":"string"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"declared_params":{"additionalProperties":{"type":"string"},"title":"Declared Params","type":"object"},"engine_type":{"enum":["elasticsearch","opensearch","solr"],"title":"Engine Type","type":"string"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"parent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Id"},"version":{"title":"Version","type":"integer"}},"required":["id","name","engine_type","body","declared_params","version","parent_id","created_at"],"title":"QueryTemplateDetail","type":"object"},"QueryTemplateListResponse":{"description":"``GET /api/v1/query-templates`` response.","properties":{"data":{"items":{"$ref":"#/components/schemas/QueryTemplateSummary"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"QueryTemplateListResponse","type":"object"},"QueryTemplateSummary":{"description":"List-view shape; drops ``body`` + the full ``declared_params`` dict.\n\nSurfaces ``param_count`` (= ``len(declared_params)``) so the\ntemplates list can show each template's tuning surface at a glance.\n``param_count`` is free to compute — ``declared_params`` is a JSONB\ncolumn already loaded on the row (not a child relationship), so the\ncount is ``len(row.declared_params)`` with no extra query and no\nN+1 risk. The full dict remains on ``QueryTemplateDetail``.","properties":{"created_at":{"format":"date-time","title":"Created At","type":"string"},"engine_type":{"enum":["elasticsearch","opensearch","solr"],"title":"Engine Type","type":"string"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"param_count":{"title":"Param Count","type":"integer"},"version":{"title":"Version","type":"integer"}},"required":["id","name","engine_type","version","param_count","created_at"],"title":"QueryTemplateSummary","type":"object"},"RegressorRowShape":{"description":"One row in the named-regressors or named-improvers table.\n\nUsed for BOTH the ``top_regressors`` and ``top_improvers`` lists.\nThe wire shape is identical — ``delta = winner_score - comparison_score``\nis negative on the regressor list, positive on the improver list. The\nclass name is historical (regressors shipped first); reusing the same\ntype keeps the schema and the per-row renderer compact.","properties":{"comparison_score":{"title":"Comparison Score","type":"number"},"delta":{"title":"Delta","type":"number"},"query_id":{"title":"Query Id","type":"string"},"query_text":{"title":"Query Text","type":"string"},"winner_score":{"title":"Winner Score","type":"number"}},"required":["query_id","query_text","winner_score","comparison_score","delta"],"title":"RegressorRowShape","type":"object"},"RejectProposalRequest":{"description":"Body of ``POST /api/v1/proposals/{id}/reject`` (FR-4 / AC-5).","properties":{"reason":{"anyOf":[{"maxLength":500,"type":"string"},{"type":"null"}],"title":"Reason"}},"title":"RejectProposalRequest","type":"object"},"ReseedStatusResponse":{"additionalProperties":false,"description":"Polling-endpoint response for ``GET /api/v1/_test/demo/reseed/status``.\n\nPer ``bug_demo_reseed_fake_metric_regression`` D-2. Lives in Redis as a\nsingle JSON blob keyed by :data:`DEMO_RESEED_STATUS_KEY` so the\nhandler reads it in one round-trip.","properties":{"current_step":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Step"},"failed_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failed Reason"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Finished At"},"scenarios_completed":{"default":0,"title":"Scenarios Completed","type":"integer"},"scenarios_skipped":{"items":{"type":"string"},"title":"Scenarios Skipped","type":"array"},"scenarios_total":{"default":0,"title":"Scenarios Total","type":"integer"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"status":{"enum":["idle","running","complete","failed"],"title":"Status","type":"string"},"steps":{"items":{"type":"string"},"title":"Steps","type":"array"},"summary":{"anyOf":[{"$ref":"#/components/schemas/ReseedSummary"},{"type":"null"}]}},"required":["status"],"title":"ReseedStatusResponse","type":"object"},"ReseedSummary":{"additionalProperties":false,"description":"Returned by :func:`reseed_demo_state` on success.\n\nPer spec §9 Required invariants, every counter is exactly 4 on the\nhappy path; ``duration_ms`` is wall-clock from orchestration start\nto the rename commit.","properties":{"clusters_created":{"title":"Clusters Created","type":"integer"},"duration_ms":{"title":"Duration Ms","type":"integer"},"proposals_created":{"title":"Proposals Created","type":"integer"},"query_sets_created":{"title":"Query Sets Created","type":"integer"},"studies_completed":{"title":"Studies Completed","type":"integer"}},"required":["clusters_created","query_sets_created","studies_completed","proposals_created","duration_ms"],"title":"ReseedSummary","type":"object"},"RunQueryHit":{"description":"One hit in the ``run_query`` response.","properties":{"doc_id":{"title":"Doc Id","type":"string"},"score":{"title":"Score","type":"number"},"source":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Source"}},"required":["doc_id","score"],"title":"RunQueryHit","type":"object"},"RunQueryRequest":{"description":"``POST /api/v1/clusters/{id}/run_query`` body.","properties":{"query_dsl":{"additionalProperties":true,"title":"Query Dsl","type":"object"},"target":{"maxLength":256,"minLength":1,"title":"Target","type":"string"},"top_k":{"default":10,"maximum":1000.0,"minimum":1.0,"title":"Top K","type":"integer"}},"required":["target","query_dsl"],"title":"RunQueryRequest","type":"object"},"RunQueryResponse":{"description":"``POST /api/v1/clusters/{id}/run_query`` response.","properties":{"hits":{"items":{"$ref":"#/components/schemas/RunQueryHit"},"title":"Hits","type":"array"}},"required":["hits"],"title":"RunQueryResponse","type":"object"},"RunnerUpGapShape":{"description":"Runner-up trial's metric vs the winner.\n\nThe whole shape is suppressed to ``None`` when there are <2 complete\ntrials (FR-2 + FR-7); ``classification`` is non-null whenever this shape\nis present.","properties":{"classification":{"enum":["robust_plateau","sharp_peak"],"title":"Classification","type":"string"},"runner_up_metric":{"title":"Runner Up Metric","type":"number"},"top10_within":{"title":"Top10 Within","type":"number"},"value":{"title":"Value","type":"number"}},"required":["value","classification","top10_within","runner_up_metric"],"title":"RunnerUpGapShape","type":"object"},"Schema":{"description":"An index / collection's field schema.","properties":{"fields":{"items":{"$ref":"#/components/schemas/FieldSpec"},"title":"Fields","type":"array"},"name":{"title":"Name","type":"string"}},"required":["name","fields"],"title":"Schema","type":"object"},"SearchSpace":{"additionalProperties":false,"description":"Pydantic model for the ``studies.search_space`` JSONB column.\n\nWire format::\n\n {\n \"params\": {\n \"boost_title\": {\"type\": \"float\", \"low\": 0.1, \"high\": 10.0, \"log\": true},\n \"min_should_match\": {\"type\": \"int\", \"low\": 1, \"high\": 5},\n \"operator\": {\"type\": \"categorical\", \"choices\": [\"and\", \"or\"]},\n }\n }","properties":{"params":{"additionalProperties":{"discriminator":{"mapping":{"categorical":"#/components/schemas/CategoricalParam","float":"#/components/schemas/FloatParam","int":"#/components/schemas/IntParam"},"propertyName":"type"},"oneOf":[{"$ref":"#/components/schemas/FloatParam"},{"$ref":"#/components/schemas/IntParam"},{"$ref":"#/components/schemas/CategoricalParam"}]},"minProperties":1,"title":"Params","type":"object"}},"required":["params"],"title":"SearchSpace","type":"object"},"SeedAutoFollowupChainRequest":{"additionalProperties":false,"description":"Payload for ``POST /api/v1/_test/auto-followup/seed-chain``.\n\nSeeds ``depth + 1`` linked studies (root → … → leaf) so E2E tests can\ncover the chain-panel parent-link / children-table / cascade-radio paths\nthat the public ``POST /api/v1/studies`` endpoint can't drive\n(``parent_study_id`` is set only by the auto-followup worker).\n\nCloses ``chore_auto_followup_e2e_chain_seed_helper`` (idea #2).","properties":{"cluster_id":{"minLength":1,"title":"Cluster Id","type":"string"},"depth":{"description":"Number of chain hops to seed. depth=1 → root + leaf (2 nodes). depth=2 → root + 1 middle + leaf (3 nodes).","maximum":5.0,"minimum":1.0,"title":"Depth","type":"integer"},"in_flight_leaf":{"default":true,"description":"When True (default), the deepest node is left at status='queued'. When False, it's driven to 'completed' too. Default True matches the primary E2E use case: cascade-radio coverage where the middle node needs an in-flight child.","title":"In Flight Leaf","type":"boolean"},"in_flight_middle":{"default":true,"description":"When True (default), the immediate parent of the leaf is left at status='queued' so the Cancel button is enabled (canCancel = running || queued per study-action-bar.tsx:46). Required for the cancel-modal cascade-radio test. When False, all intermediates are completed (more realistic chain state but cancel modal won't open on the middle).","title":"In Flight Middle","type":"boolean"},"judgment_list_id":{"minLength":1,"title":"Judgment List Id","type":"string"},"query_set_id":{"minLength":1,"title":"Query Set Id","type":"string"},"template_id":{"minLength":1,"title":"Template Id","type":"string"}},"required":["cluster_id","query_set_id","template_id","judgment_list_id","depth"],"title":"SeedAutoFollowupChainRequest","type":"object"},"SeedAutoFollowupChainResponse":{"description":"IDs of every node in the seeded chain, in parent→child order.","properties":{"leaf_id":{"title":"Leaf Id","type":"string"},"middle_ids":{"items":{"type":"string"},"title":"Middle Ids","type":"array"},"root_id":{"title":"Root Id","type":"string"}},"required":["root_id","middle_ids","leaf_id"],"title":"SeedAutoFollowupChainResponse","type":"object"},"SeedCompletedStudyRequest":{"additionalProperties":false,"description":"Payload for ``POST /api/v1/_test/studies/seed-completed``.\n\nAll four FK fields are required; the caller is responsible for\nseeding the parent rows first (typically via the public\n``seedFullChain`` E2E helper).","properties":{"cluster_id":{"minLength":1,"title":"Cluster Id","type":"string"},"extra_trial_metrics":{"anyOf":[{"items":{"type":"number"},"type":"array"},{"type":"null"}],"description":"Optional list of additional complete-trial `primary_metric` values (numbered from 2 upward) seeded on top of the default winner (0.487) + runner-up (0.412). Used to push the study past the convergence classifier's usable-trial floor (5) so the `` renders a real verdict + curve instead of the too_few_trials null state (feat_study_convergence_indicator). Every value MUST be < 0.487 so the winner / best_metric / proposal / digest stay anchored to the unchanged 0.412 -> 0.487 story. Omit for the default 2-trial shape.","title":"Extra Trial Metrics"},"judgment_list_id":{"minLength":1,"title":"Judgment List Id","type":"string"},"query_set_id":{"minLength":1,"title":"Query Set Id","type":"string"},"runner_up_per_query":{"anyOf":[{"additionalProperties":{"additionalProperties":true,"type":"object"},"type":"object"},{"type":"null"}],"description":"Optional per-query metrics for the runner-up trial; pairs with `winner_per_query`.","title":"Runner Up Per Query"},"suggested_followups":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"description":"feat_digest_executable_followups Story 6.1 — optional structured FollowupItem list (`[{kind, rationale, search_space}]`) to seed on the digest. When omitted, the seeder writes two default text-kind items. The E2E Run-followup spec passes a `narrow` item so it can drive the per-card Run button + modal prefill flow.","title":"Suggested Followups"},"template_id":{"minLength":1,"title":"Template Id","type":"string"},"winner_per_query":{"anyOf":[{"additionalProperties":{"additionalProperties":true,"type":"object"},"type":"object"},{"type":"null"}],"description":"Optional per-query metrics dict to populate on the winner trial. Shape: `{query_id: {metric_token: float}}` where metric_token matches what `scoring.score()` emits (e.g. `ndcg@10`). Set alongside `runner_up_per_query` to drive the ConfidencePanel happy path on `/studies/[id]`. When omitted, the seeded trials have `per_query_metrics IS NULL` (the pre-feat_pr_metric_confidence shape).","title":"Winner Per Query"},"with_pending_proposal":{"default":true,"description":"When true (default), also insert a `status='pending'` proposal linked to the study so the digest panel's Open PR button renders enabled. Set false to test the AC-11 aria-disabled-button + tooltip path.","title":"With Pending Proposal","type":"boolean"}},"required":["cluster_id","query_set_id","template_id","judgment_list_id"],"title":"SeedCompletedStudyRequest","type":"object"},"SeedCompletedStudyResponse":{"description":"IDs of the inserted rows; mirrors :class:`SeededStudyTriple`.","properties":{"digest_id":{"title":"Digest Id","type":"string"},"proposal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proposal Id"},"study_id":{"title":"Study Id","type":"string"}},"required":["study_id","digest_id","proposal_id"],"title":"SeedCompletedStudyResponse","type":"object"},"SendMessageRequest":{"description":"``POST /api/v1/conversations/{id}/messages`` body (Story 3.2).","properties":{"content":{"$ref":"#/components/schemas/SendMessageRequestContent"},"role":{"const":"user","default":"user","title":"Role","type":"string"}},"required":["content"],"title":"SendMessageRequest","type":"object"},"SendMessageRequestContent":{"description":"Sub-shape inside :class:`SendMessageRequest`.","properties":{"text":{"maxLength":20000,"minLength":1,"title":"Text","type":"string"}},"required":["text"],"title":"SendMessageRequestContent","type":"object"},"StudyChainLink":{"description":"One link in the rolled-up overnight-chain summary (feat_overnight_autopilot §8.3).","properties":{"auto_followup_depth_remaining":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Auto Followup Depth Remaining"},"baseline_metric":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Baseline Metric"},"best_metric":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Best Metric"},"completed_at":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Completed At"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"delta_from_prev":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Delta From Prev"},"direction":{"enum":["maximize","minimize"],"title":"Direction","type":"string"},"failed_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failed Reason"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"proposal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proposal Id"},"status":{"enum":["queued","running","completed","cancelled","failed"],"title":"Status","type":"string"}},"required":["id","name","status","best_metric","baseline_metric","direction","delta_from_prev","proposal_id","auto_followup_depth_remaining","failed_reason","created_at","completed_at"],"title":"StudyChainLink","type":"object"},"StudyChainResponse":{"description":"``GET /api/v1/studies/{id}/chain`` response (feat_overnight_autopilot §8.3).","properties":{"anchor_study_id":{"title":"Anchor Study Id","type":"string"},"best_link_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Best Link Id"},"best_metric":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Best Metric"},"cumulative_lift":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cumulative Lift"},"direction":{"enum":["maximize","minimize"],"title":"Direction","type":"string"},"links":{"items":{"$ref":"#/components/schemas/StudyChainLink"},"title":"Links","type":"array"},"proposal_id_for_best_link":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Proposal Id For Best Link"},"stop_reason":{"enum":["depth_exhausted","no_lift","budget","parent_failed","cancelled","in_flight"],"title":"Stop Reason","type":"string"}},"required":["anchor_study_id","best_link_id","best_metric","cumulative_lift","direction","stop_reason","proposal_id_for_best_link","links"],"title":"StudyChainResponse","type":"object"},"StudyConfigSpec":{"description":"Wire shape of ``studies.config`` (write-side).\n\nThe model_validator below enforces that at least one stop condition is\nset — otherwise the study has no terminating condition (FR-4).\n``parallelism`` / ``trial_timeout_s`` are optional; when absent the\nworker reads ``Settings.studies_default_parallelism`` /\n``studies_default_timeout_s`` at job time. The API layer does NOT\nmaterialize these fields into the stored row — see Story 1.5 +\nStory 3.3's ``config.model_dump(exclude_none=True, exclude_unset=True)``\ncontract.","properties":{"auto_followup_depth":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Auto Followup Depth"},"baseline_params":{"anyOf":[{"additionalProperties":{"anyOf":[{"type":"string"},{"type":"integer"},{"type":"number"},{"type":"boolean"},{"type":"null"}]},"type":"object"},{"type":"null"}],"title":"Baseline Params"},"max_trials":{"anyOf":[{"maximum":100000.0,"minimum":1.0,"type":"integer"},{"type":"null"}],"title":"Max Trials"},"parallelism":{"anyOf":[{"maximum":64.0,"minimum":1.0,"type":"integer"},{"type":"null"}],"title":"Parallelism"},"pruner":{"anyOf":[{"enum":["median","none"],"type":"string"},{"type":"null"}],"title":"Pruner"},"sampler":{"anyOf":[{"enum":["tpe","random"],"type":"string"},{"type":"null"}],"title":"Sampler"},"secondary_metrics":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Secondary Metrics"},"seed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Seed"},"time_budget_min":{"anyOf":[{"exclusiveMinimum":0.0,"type":"number"},{"type":"null"}],"title":"Time Budget Min"},"trial_timeout_s":{"anyOf":[{"maximum":3600.0,"minimum":5.0,"type":"integer"},{"type":"null"}],"title":"Trial Timeout S"}},"title":"StudyConfigSpec","type":"object"},"StudyConvergenceShape":{"description":"Verdict + supporting numerics for the UI panel and the digest narrative.\n\nMirrors the ``ConfidenceShape`` pattern from ``confidence.py``: the\ndomain module owns the Pydantic model, and ``backend.app.api.v1.schemas``\nre-exports it for the ``StudyDetail.convergence`` field. The\n``best_so_far_curve`` is the chart's data series; ``verdict`` is the\nbadge label.\n\n**Name discipline (plan §0).** The bare class name ``ConvergenceShape``\nis already taken by :class:`backend.app.domain.study.confidence.ConvergenceShape`\n(a different concept — winner-trial *timing*, not metric plateau).\n``StudyConvergenceShape`` is the study-level analogue; the confidence\nsub-shape stays on its inner module. The two coexist on ``StudyDetail``\n(``confidence.convergence`` is the inner one; ``convergence`` is this\none), and FastAPI emits both under their bare class names in the\nOpenAPI schema — no fully-qualified disambiguation noise leaks to the\nfrontend.","properties":{"best_so_far_curve":{"items":{"$ref":"#/components/schemas/CurvePoint"},"title":"Best So Far Curve","type":"array"},"direction":{"enum":["maximize","minimize"],"title":"Direction","type":"string"},"epsilon":{"title":"Epsilon","type":"number"},"improvement_in_window":{"title":"Improvement In Window","type":"number"},"total_complete_trials":{"title":"Total Complete Trials","type":"integer"},"verdict":{"enum":["converged","still_improving","too_few_trials"],"title":"Verdict","type":"string"},"warmup_floor":{"title":"Warmup Floor","type":"integer"},"window_size":{"title":"Window Size","type":"integer"}},"required":["verdict","direction","window_size","epsilon","warmup_floor","total_complete_trials","improvement_in_window","best_so_far_curve"],"title":"StudyConvergenceShape","type":"object"},"StudyDetail":{"description":"``GET /api/v1/studies/{id}`` response + ``POST/cancel`` response.","properties":{"baseline_metric":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Baseline Metric"},"baseline_trial_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Baseline Trial Id"},"best_metric":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Best Metric"},"best_trial_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Best Trial Id"},"cluster_id":{"title":"Cluster Id","type":"string"},"completed_at":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Completed At"},"confidence":{"anyOf":[{"$ref":"#/components/schemas/ConfidenceShape"},{"type":"null"}]},"config":{"additionalProperties":true,"title":"Config","type":"object"},"convergence":{"anyOf":[{"$ref":"#/components/schemas/StudyConvergenceShape"},{"type":"null"}]},"created_at":{"format":"date-time","title":"Created At","type":"string"},"failed_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failed Reason"},"id":{"title":"Id","type":"string"},"judgment_list_id":{"title":"Judgment List Id","type":"string"},"name":{"title":"Name","type":"string"},"objective":{"additionalProperties":true,"title":"Objective","type":"object"},"optuna_study_name":{"title":"Optuna Study Name","type":"string"},"parent_study_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Study Id"},"query_set_id":{"title":"Query Set Id","type":"string"},"search_space":{"additionalProperties":true,"title":"Search Space","type":"object"},"started_at":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Started At"},"status":{"enum":["queued","running","completed","cancelled","failed"],"title":"Status","type":"string"},"target":{"title":"Target","type":"string"},"template_id":{"title":"Template Id","type":"string"},"trials_summary":{"$ref":"#/components/schemas/TrialsSummaryShape"}},"required":["id","name","cluster_id","target","template_id","query_set_id","judgment_list_id","search_space","objective","config","status","failed_reason","optuna_study_name","parent_study_id","baseline_metric","baseline_trial_id","best_metric","best_trial_id","created_at","started_at","completed_at","trials_summary"],"title":"StudyDetail","type":"object"},"StudyListResponse":{"description":"``GET /api/v1/studies`` response.","properties":{"data":{"items":{"$ref":"#/components/schemas/StudySummary"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"StudyListResponse","type":"object"},"StudySummary":{"description":"List-view shape.","properties":{"best_metric":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Best Metric"},"cluster_id":{"title":"Cluster Id","type":"string"},"completed_at":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Completed At"},"convergence_verdict":{"anyOf":[{"enum":["converged","still_improving","too_few_trials"],"type":"string"},{"type":"null"}],"title":"Convergence Verdict"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"direction":{"default":"maximize","enum":["maximize","minimize"],"title":"Direction","type":"string"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"status":{"enum":["queued","running","completed","cancelled","failed"],"title":"Status","type":"string"},"trial_count":{"default":0,"title":"Trial Count","type":"integer"}},"required":["id","name","cluster_id","status","best_metric","created_at","completed_at"],"title":"StudySummary","type":"object"},"Subsystems":{"description":"Per-subsystem reachability/configuration state. Wire values per spec §7.4.","properties":{"db":{"description":"Postgres reachability","enum":["ok","down"],"title":"Db","type":"string"},"elasticsearch":{"description":"Local Elasticsearch container reachability","enum":["reachable","unreachable"],"title":"Elasticsearch","type":"string"},"elasticsearch_clusters":{"$ref":"#/components/schemas/ClusterAggregateHealth","description":"Aggregate health of user-registered clusters (infra_adapter_elastic Story 3.5 / spec §2). registered=0 → all-zero counts; informational only — does NOT trigger overall `degraded`."},"openai":{"description":"OpenAI key + capability state. 'incapable' added per FR-2 vs. spec §7.4 enum table — see implementation_plan.md §13 Review log.","enum":["configured","missing_key","incapable"],"title":"Openai","type":"string"},"opensearch":{"description":"Local OpenSearch container reachability","enum":["reachable","unreachable"],"title":"Opensearch","type":"string"},"redis":{"description":"Redis reachability","enum":["ok","down"],"title":"Redis","type":"string"},"solr":{"default":"not_configured","description":"Local Apache Solr container reachability. 'not_configured' when SOLR_HOST is unset (operator opted out of running the Solr service). Added by infra_adapter_solr Story A10 / spec FR-12a.","enum":["reachable","unreachable","not_configured"],"title":"Solr","type":"string"}},"required":["db","redis","openai","elasticsearch","opensearch","elasticsearch_clusters"],"title":"Subsystems","type":"object"},"SwapTemplateFollowup":{"additionalProperties":false,"description":"A 'swap_template' followup — re-run against a different query template.\n\nCarries the LLM-proposed bounds for params shared with the parent template\nin ``search_space``. The digest worker calls\n:func:`backend.app.domain.study.template_swap.remap_search_space_for_swap_target`\nafter parsing to merge these bounds with heuristic defaults for any\nswap-target params not shared with the parent.\n\nOwner: ``feat_digest_executable_followups_swap_template`` (Tier B).","properties":{"kind":{"const":"swap_template","title":"Kind","type":"string"},"rationale":{"title":"Rationale","type":"string"},"search_space":{"$ref":"#/components/schemas/SearchSpace"},"template_id":{"maxLength":36,"minLength":36,"title":"Template Id","type":"string"}},"required":["kind","rationale","template_id","search_space"],"title":"SwapTemplateFollowup","type":"object"},"TargetInfo":{"description":"One target (index / collection) on a cluster.","properties":{"doc_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Doc Count"},"name":{"title":"Name","type":"string"}},"required":["name"],"title":"TargetInfo","type":"object"},"TargetListResponse":{"description":"Response for ``GET /api/v1/clusters/{cluster_id}/targets`` (FR-1).\n\nUnpaginated by design — see feature_spec.md §7.1 \"pagination shape\nrationale\". The single-resource lookup pattern matches\n``/clusters/{id}/schema`` rather than the queryable ``/clusters`` list.\n``EntitySelectListPage``'s ``next_cursor`` and ``has_more`` fields\nare optional, so this bare ``data``-only shape consumes correctly on\nthe frontend without pretending to be a cursor endpoint.","properties":{"data":{"items":{"$ref":"#/components/schemas/TargetInfo"},"title":"Data","type":"array"}},"required":["data"],"title":"TargetListResponse","type":"object"},"TextFollowup":{"additionalProperties":false,"description":"A free-form textual suggestion — no auto-prefill, operator interprets.","properties":{"kind":{"const":"text","title":"Kind","type":"string"},"rationale":{"title":"Rationale","type":"string"},"search_space":{"title":"Search Space","type":"null"}},"required":["kind","rationale"],"title":"TextFollowup","type":"object"},"TrialDetail":{"description":"``GET /api/v1/studies/{id}/trials`` response row.","properties":{"duration_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration Ms"},"ended_at":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Ended At"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"id":{"title":"Id","type":"string"},"is_baseline":{"default":false,"title":"Is Baseline","type":"boolean"},"metrics":{"additionalProperties":true,"title":"Metrics","type":"object"},"optuna_trial_number":{"title":"Optuna Trial Number","type":"integer"},"params":{"additionalProperties":true,"title":"Params","type":"object"},"primary_metric":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Primary Metric"},"started_at":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Started At"},"status":{"enum":["complete","failed","pruned"],"title":"Status","type":"string"},"study_id":{"title":"Study Id","type":"string"}},"required":["id","study_id","optuna_trial_number","params","primary_metric","metrics","duration_ms","status","error","started_at","ended_at"],"title":"TrialDetail","type":"object"},"TrialListResponse":{"description":"``GET /api/v1/studies/{id}/trials`` response.","properties":{"data":{"items":{"$ref":"#/components/schemas/TrialDetail"},"title":"Data","type":"array"},"has_more":{"title":"Has More","type":"boolean"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"required":["data","next_cursor","has_more"],"title":"TrialListResponse","type":"object"},"TrialsSummaryShape":{"description":"The ``trials_summary`` field embedded in :class:`StudyDetail`.","properties":{"best_primary_metric":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Best Primary Metric"},"complete":{"title":"Complete","type":"integer"},"failed":{"title":"Failed","type":"integer"},"pruned":{"title":"Pruned","type":"integer"},"total":{"title":"Total","type":"integer"}},"required":["total","complete","failed","pruned","best_primary_metric"],"title":"TrialsSummaryShape","type":"object"},"UbiReadinessResponse":{"description":"``GET /api/v1/clusters/{cluster_id}/ubi-readiness`` response (FR-7).\n\n``covered_pairs_pct`` and ``head_covered`` are nullable — MVP2's\nrung classifier uses event-count thresholds (the SearchAdapter\nProtocol doesn't expose an exact ``_count`` endpoint). The fields\nare reserved on the wire so a future ``infra_adapter_count_method``\ncan fill them without breaking the contract. See\n:mod:`backend.app.services.ubi_readiness` for the rationale.","properties":{"checked_at":{"format":"date-time","title":"Checked At","type":"string"},"covered_pairs_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Covered Pairs Pct"},"head_covered":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Head Covered"},"rung":{"enum":["rung_0","rung_1","rung_2","rung_3"],"title":"Rung","type":"string"}},"required":["rung","covered_pairs_pct","head_covered","checked_at"],"title":"UbiReadinessResponse","type":"object"},"UpdateQueryRequest":{"additionalProperties":false,"description":"``PATCH /api/v1/query-sets/{set_id}/queries/{query_id}`` body.\n\nWhole-object replace on ``query_metadata`` (NOT deep-merge); explicit\n``null`` removes a nullable field; omitted key = no change. Empty\nbody ``{}`` validates as a no-op (AC-28).\n\n``query_text`` is NOT NULL on the underlying table, so explicit-null\nis rejected by the ``@model_validator`` below (a 422 surfaces sooner\nthan the SQL ``NotNullViolation``).","properties":{"query_metadata":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Query Metadata"},"query_text":{"anyOf":[{"maxLength":4000,"minLength":1,"type":"string"},{"type":"null"}],"title":"Query Text"},"reference_answer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reference Answer"}},"title":"UpdateQueryRequest","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"},"WidenFollowup":{"additionalProperties":false,"description":"A 'widen' followup — re-run with a broader range than the parent.","properties":{"kind":{"const":"widen","title":"Kind","type":"string"},"rationale":{"title":"Rationale","type":"string"},"search_space":{"$ref":"#/components/schemas/SearchSpace"}},"required":["kind","rationale","search_space"],"title":"WidenFollowup","type":"object"},"_ClusterEmbed":{"description":"Inline cluster summary on proposal responses.","properties":{"engine_type":{"title":"Engine Type","type":"string"},"environment":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Environment"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"}},"required":["id","name","engine_type"],"title":"_ClusterEmbed","type":"object"},"_DigestEmbed":{"description":"Inline digest summary on the proposal-detail response.\n\nfeat_digest_executable_followups Story 4.1 — ``suggested_followups`` is\nnow a discriminated-union list (see ``DigestResponse``).","properties":{"generated_at":{"format":"date-time","title":"Generated At","type":"string"},"id":{"title":"Id","type":"string"},"narrative":{"title":"Narrative","type":"string"},"parameter_importance":{"additionalProperties":{"type":"number"},"title":"Parameter Importance","type":"object"},"recommended_config":{"additionalProperties":true,"title":"Recommended Config","type":"object"},"suggested_followups":{"items":{"$ref":"#/components/schemas/FollowupItem"},"title":"Suggested Followups","type":"array"}},"required":["id","narrative","parameter_importance","recommended_config","suggested_followups","generated_at"],"title":"_DigestEmbed","type":"object"},"_SourceBreakdown":{"description":"Source-breakdown sub-shape on :class:`JudgmentListDetail`.\n\nEvolved 2026-05-29 by ``feat_ubi_judgments`` FR-10 — now three terms\n(``llm + human + click == judgment_count``). The cycle-2 F6\n\"click folds into human\" contract is superseded the moment UBI ships\nclick rows; the UI's source-breakdown card now renders all three\nbuckets separately so operators see the mix at a glance.","properties":{"click":{"title":"Click","type":"integer"},"human":{"title":"Human","type":"integer"},"llm":{"title":"Llm","type":"integer"}},"required":["llm","human","click"],"title":"_SourceBreakdown","type":"object"},"_StudySummary":{"description":"Inline study summary on the proposal-detail response.","properties":{"best_metric":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Best Metric"},"best_trial_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Best Trial Id"},"id":{"title":"Id","type":"string"},"judgment_list":{"additionalProperties":true,"title":"Judgment List","type":"object"},"name":{"title":"Name","type":"string"},"query_set":{"additionalProperties":true,"title":"Query Set","type":"object"},"status":{"title":"Status","type":"string"}},"required":["id","name","status","best_metric","best_trial_id","query_set","judgment_list"],"title":"_StudySummary","type":"object"},"_TemplateEmbed":{"description":"Inline template summary on proposal responses.","properties":{"engine_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Engine Type"},"id":{"title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"version":{"title":"Version","type":"integer"}},"required":["id","name","version"],"title":"_TemplateEmbed","type":"object"}}},"info":{"description":"Open-source automated relevance tuning for enterprise search platforms","title":"RelyLoop","version":"0.1.0"},"openapi":"3.1.0","paths":{"/api/v1/_test/auto-followup/seed-chain":{"post":{"description":"Test-only endpoint. Returns 404 unless `ENVIRONMENT=development`. Inserts a chain of `depth + 1` studies where each child carries the prior node's id as `parent_study_id`. The public POST /studies endpoint does NOT accept `parent_study_id` (it's set only by the auto-followup worker via `repo.create_study(parent_study_id=...)`), so this endpoint is the only way to drive deterministic E2E coverage of chain-panel parent-link / children-table / cascade-radio paths. Closes chore_auto_followup_e2e_chain_seed_helper.","operationId":"seed_auto_followup_chain_endpoint_api_v1__test_auto_followup_seed_chain_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeedAutoFollowupChainRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeedAutoFollowupChainResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Seed an auto-followup chain of N+1 linked studies","tags":["test-only"]}},"/api/v1/_test/demo/reseed":{"post":{"description":"Enqueues an Arq job that wipes the demo Postgres tables + ES/OS indices, then re-seeds the 4 demo scenarios from ``scripts/seed_meaningful_demos.py`` using REAL studies (real Optuna trials, real metrics per scenario). Returns 202 + an initial ``ReseedStatusResponse`` immediately; the frontend polls ``GET /api/v1/_test/demo/reseed/status`` for progress.\n\nPer ``bug_demo_reseed_fake_metric_regression``. Replaces the previous synchronous path that called ``/_test/studies/seed-completed`` and produced identical ``best_metric=0.487`` rows for every scenario.","operationId":"reseed_demo_api_v1__test_demo_reseed_post","responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReseedStatusResponse"}}},"description":"Successful Response"}},"summary":"Enqueue a demo-state reseed (dev-only, async)","tags":["test-only"]}},"/api/v1/_test/demo/reseed/status":{"get":{"description":"Returns the current reseed status from Redis. When no reseed has ever run (or the result TTL'd out), returns ``{status: 'idle'}`` rather than 404 so the frontend's polling loop is trivially safe.","operationId":"reseed_demo_status_api_v1__test_demo_reseed_status_get","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReseedStatusResponse"}}},"description":"Successful Response"}},"summary":"Poll the current demo-reseed progress (dev-only)","tags":["test-only"]}},"/api/v1/_test/digests/{digest_id}":{"delete":{"description":"FR-2: Hard-delete the digest row. No FK children — no preflight needed.","operationId":"delete_test_digest_api_v1__test_digests__digest_id__delete","parameters":[{"in":"path","name":"digest_id","required":true,"schema":{"title":"Digest Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Hard-delete a digest (test-only)","tags":["test-only"]}},"/api/v1/_test/judgment-lists/{judgment_list_id}":{"delete":{"description":"FR-4 — hard-delete the judgment_list row.\n\nJudgments cascade-delete via existing FK. Preflight-checks ``studies``\n(non-cascade); 409 if any study references the judgment_list.","operationId":"delete_test_judgment_list_api_v1__test_judgment_lists__judgment_list_id__delete","parameters":[{"in":"path","name":"judgment_list_id","required":true,"schema":{"title":"Judgment List Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Hard-delete a judgment_list (test-only)","tags":["test-only"]}},"/api/v1/_test/proposals/{proposal_id}":{"delete":{"description":"FR-1: Hard-delete the proposal row. No FK children — no preflight needed.","operationId":"delete_test_proposal_api_v1__test_proposals__proposal_id__delete","parameters":[{"in":"path","name":"proposal_id","required":true,"schema":{"title":"Proposal Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Hard-delete a proposal (test-only)","tags":["test-only"]}},"/api/v1/_test/query-sets/{query_set_id}":{"delete":{"description":"FR-5 — hard-delete the query_set row.\n\nQueries cascade-delete via existing FK. Preflight-checks ``studies``\n+ ``judgment_lists`` (both non-cascade); 409 with resource-specific\ncode if either references.","operationId":"delete_test_query_set_api_v1__test_query_sets__query_set_id__delete","parameters":[{"in":"path","name":"query_set_id","required":true,"schema":{"title":"Query Set Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Hard-delete a query_set (test-only)","tags":["test-only"]}},"/api/v1/_test/query-templates/{template_id}":{"delete":{"description":"FR-6 — hard-delete the query_template row.\n\nNo FK children cascade with template. Preflight-checks ``studies``,\n``proposals``, and ``judgment_lists.current_template_id`` in\n**fixed priority order: STUDY > PROPOSAL > JUDGMENT_LIST** (per\nspec §FR-6) — first match wins.","operationId":"delete_test_query_template_api_v1__test_query_templates__template_id__delete","parameters":[{"in":"path","name":"template_id","required":true,"schema":{"title":"Template Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Hard-delete a query_template (test-only)","tags":["test-only"]}},"/api/v1/_test/studies/seed-completed":{"post":{"description":"Test-only endpoint. Returns 404 unless `ENVIRONMENT=development`. Inserts a study (driven through queued → running → completed via the legal state-machine transitions), 2 trials (one winner, one comparison), a digest, and optionally a pending proposal in a single transaction. Used by the Playwright E2E suite to cover the digest-panel surfaces (7 tooltip placements + AC-7 body content + AC-11 Open PR enabled/disabled branches) without waiting on the orchestrator + Optuna workers.","operationId":"seed_completed_study_api_v1__test_studies_seed_completed_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeedCompletedStudyRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeedCompletedStudyResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Seed a completed study + digest + (optional) pending proposal","tags":["test-only"]}},"/api/v1/_test/studies/{study_id}":{"delete":{"description":"FR-3 — hard-delete the study row.\n\nTrials cascade-delete via existing FK. Preflight-checks ``proposals``\n+ ``digests`` (both non-cascade); 409 if any dependent rows reference\nthe study.","operationId":"delete_test_study_api_v1__test_studies__study_id__delete","parameters":[{"in":"path","name":"study_id","required":true,"schema":{"title":"Study Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Hard-delete a study (test-only)","tags":["test-only"]}},"/api/v1/clusters":{"get":{"description":"List clusters with cursor pagination + ``X-Total-Count`` header.\n\n``?q=`` is a Postgres FTS match against the cluster's ``search_vector``\n(name + base_url); 2–200 chars. Filter-only — ordering unchanged per\nspec FR-1. ``?sort=`` is one of the values in\n:data:`~backend.app.api.v1.schemas.ClusterSortKey`; the cursor is\nsort-aware so the keyset predicate matches the active ORDER BY\n(feat_data_table_primitive Stories 1.2 + 1.3).","operationId":"list_clusters_api_v1_clusters_get","parameters":[{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"since","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Since"}},{"in":"query","name":"q","required":false,"schema":{"anyOf":[{"maxLength":200,"minLength":2,"type":"string"},{"type":"null"}],"title":"Q"}},{"in":"query","name":"sort","required":false,"schema":{"anyOf":[{"enum":["name:asc","name:desc","created_at:asc","created_at:desc","environment:asc","environment:desc"],"type":"string"},{"type":"null"}],"title":"Sort"}},{"in":"query","name":"engine_type","required":false,"schema":{"anyOf":[{"enum":["elasticsearch","opensearch","solr"],"type":"string"},{"type":"null"}],"title":"Engine Type"}},{"in":"query","name":"environment","required":false,"schema":{"anyOf":[{"enum":["prod","staging","dev"],"type":"string"},{"type":"null"}],"title":"Environment"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClusterListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Clusters","tags":["clusters"]},"post":{"description":"Register a cluster (FR-5 / AC-1).","operationId":"create_cluster_api_v1_clusters_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateClusterRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClusterDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Create Cluster","tags":["clusters"]}},"/api/v1/clusters/test-connection":{"post":{"description":"Probe a cluster config WITHOUT persisting (infra_adapter_solr Story A9).\n\nPowers the registration modal's \"Test connection\" button. Always 200 —\ntransport failures surface as ``reachable=false`` with ``error`` set.\nInvalid engine×auth pairings 400 BEFORE the network call.","operationId":"test_connection_api_v1_clusters_test_connection_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionTestRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionTestResult"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Test Connection","tags":["clusters"]}},"/api/v1/clusters/{cluster_id}":{"delete":{"description":"Soft-delete a cluster (AC-8). Returns 204 with no body.","operationId":"delete_cluster_api_v1_clusters__cluster_id__delete","parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"title":"Cluster Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Delete Cluster","tags":["clusters"]},"get":{"description":"Return cluster row + cached/fresh health probe.","operationId":"get_cluster_detail_api_v1_clusters__cluster_id__get","parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"title":"Cluster Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClusterDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Cluster Detail","tags":["clusters"]}},"/api/v1/clusters/{cluster_id}/reprobe":{"post":{"description":"Re-run cluster capability probe (Story A9 / spec FR-2 + AC-14).\n\nConcurrent calls serialize on ``SELECT … FOR UPDATE``. On probe failure\nthe row's engine_config is NOT updated (the transaction rolls back).","operationId":"reprobe_cluster_api_v1_clusters__cluster_id__reprobe_post","parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"title":"Cluster Id","type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClusterDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Reprobe Cluster","tags":["clusters"]}},"/api/v1/clusters/{cluster_id}/run_query":{"post":{"description":"Execute one query DSL fragment against the cluster (FR-6 / AC-3).","operationId":"run_query_api_v1_clusters__cluster_id__run_query_post","parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"title":"Cluster Id","type":"string"}},{"in":"query","name":"timeout_s","required":false,"schema":{"default":5.0,"maximum":30.0,"minimum":1.0,"title":"Timeout S","type":"number"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunQueryRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunQueryResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Run Query","tags":["clusters"]}},"/api/v1/clusters/{cluster_id}/schema":{"get":{"description":"Return the field schema for ``target`` (FR-4 / AC-2).","operationId":"get_cluster_schema_api_v1_clusters__cluster_id__schema_get","parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"title":"Cluster Id","type":"string"}},{"in":"query","name":"target","required":true,"schema":{"maxLength":256,"minLength":1,"title":"Target","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Schema"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Cluster Schema","tags":["clusters"]}},"/api/v1/clusters/{cluster_id}/targets":{"get":{"description":"List targets (indices/collections) on the cluster (FR-1 / AC-1).\n\nThin passthrough to ``ElasticAdapter.list_targets()`` (which filters out\nsystem indices whose names start with ``.``). Mirrors the ``get_cluster_schema``\npattern: ``get_cluster`` → ``acquire_adapter`` async context → adapter call\n→ translate exceptions via the ``_err()`` helper to the spec §7.5 envelope.\n\nError mapping:\n* cluster missing or soft-deleted → 404 ``CLUSTER_NOT_FOUND`` (retryable=false)\n* adapter raises ``TargetsForbiddenError`` (ACL 401/403) → 403\n ``TARGETS_FORBIDDEN`` (retryable=false) — frontend auto-engages manual mode\n* adapter raises ``ClusterUnreachableError`` (5xx / connection failure) → 503\n ``CLUSTER_UNREACHABLE`` (retryable=true)","operationId":"list_cluster_targets_api_v1_clusters__cluster_id__targets_get","parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"title":"Cluster Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TargetListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Cluster Targets","tags":["clusters"]}},"/api/v1/clusters/{cluster_id}/targets/{target}/documents":{"get":{"description":"Paginated _id + truncated _source preview for a target (FR-3).\n\nThe endpoint asks the adapter for ``limit + 1`` rows so it can detect\nend-of-data exactly (no extra round-trip). Only the first ``limit`` rows\nare returned; ``next_cursor`` encodes the ES ``hits[i].sort`` of the\nlast visible row when ``has_more`` is True. ``X-Total-Count`` header\ncarries the engine's ``hits.total.value``.","operationId":"list_target_documents_api_v1_clusters__cluster_id__targets__target__documents_get","parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"title":"Cluster Id","type":"string"}},{"in":"path","name":"target","required":true,"schema":{"title":"Target","type":"string"}},{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"maxLength":4096,"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":25,"maximum":100,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"fields","required":false,"schema":{"anyOf":[{"maxLength":2048,"type":"string"},{"type":"null"}],"title":"Fields"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Target Documents","tags":["clusters"]}},"/api/v1/clusters/{cluster_id}/targets/{target}/documents/{doc_id}":{"get":{"description":"Fetch one document by ``_id`` (FR-4).\n\nFastAPI's ``{doc_id:path}`` converter round-trips slashes verbatim, so\noperator IDs containing ``/`` are supported (D-17 / AC-16). Returns the\nadapter ``Document`` shape directly; on ``found: false`` returns 404\n``DOCUMENT_NOT_FOUND`` (distinct from ``TARGET_NOT_FOUND``).","operationId":"get_target_document_api_v1_clusters__cluster_id__targets__target__documents__doc_id__get","parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"title":"Cluster Id","type":"string"}},{"in":"path","name":"target","required":true,"schema":{"title":"Target","type":"string"}},{"in":"path","name":"doc_id","required":true,"schema":{"title":"Doc Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Document"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Target Document","tags":["clusters"]}},"/api/v1/clusters/{cluster_id}/ubi-readiness":{"get":{"description":"Classify ``(cluster, query_set, target)`` on the UBI rung ladder.\n\nfeat_ubi_judgments FR-7.\n\nRequired query params: ``query_set_id`` + ``target`` (Spec FR-7 +\ncycle-3 D-10c: the endpoint MUST 422 without them — the classifier\ncan't compute a per-target rung without an application filter).\n\nError envelopes (all per spec §7.5):\n* ``404 CLUSTER_NOT_FOUND`` — cluster row missing or soft-deleted.\n* ``404 QUERY_SET_NOT_FOUND`` — query set row missing.\n* ``422 VALIDATION_ERROR`` — missing required query params (FastAPI's\n built-in handler, surfaces via ``api/errors.py``).\n* ``503 CLUSTER_UNREACHABLE`` — adapter cannot reach the cluster.\n\nThe result is cached for 60 s in Redis per\n``(cluster_id, query_set_id, target)`` so back-to-back dialog-open\nand dialog-submit calls don't re-probe.","operationId":"get_cluster_ubi_readiness_api_v1_clusters__cluster_id__ubi_readiness_get","parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"title":"Cluster Id","type":"string"}},{"in":"query","name":"query_set_id","required":true,"schema":{"maxLength":36,"minLength":1,"title":"Query Set Id","type":"string"}},{"in":"query","name":"target","required":true,"schema":{"maxLength":256,"minLength":1,"title":"Target","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UbiReadinessResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Cluster Ubi Readiness","tags":["clusters"]}},"/api/v1/config-repos":{"get":{"description":"Cursor-paginated config-repo list, newest first.","operationId":"list_config_repos_endpoint_api_v1_config_repos_get","parameters":[{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigReposListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Config Repos Endpoint","tags":["config-repos"]},"post":{"description":"Register a new config repo. ``provider`` is server-derived from ``repo_url``.\n\nPreflight order matches spec FR-3:\n\n1. ``validate_repo_url(repo_url)`` → 400 ``UNSUPPORTED_PROVIDER`` for\n non-GitHub URLs (AC-8). GitLab + Bitbucket arrive at MVP3.\n2. ``./secrets/{auth_ref}`` must exist → else 400 ``AUTH_REF_NOT_FOUND``\n (AC-9). The contents check defers to the worker — operators may\n populate the file between registration and first PR-open.\n3. ``name`` uniqueness check → 409 ``CONFIG_REPO_NAME_TAKEN`` on collision.\n4. Insert with server-derived ``provider=\"github\"``.\n5. **feat_github_webhook Story 4.2** — when ``webhook_secret_ref`` is\n populated, best-effort enqueue ``register_webhook`` against the\n newly created config_repo id. Enqueue failure (Redis down, pool\n absent, transient blip) does NOT break the 201 — it logs WARN\n and the operator drives recovery via the runbook.","operationId":"create_config_repo_endpoint_api_v1_config_repos_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateConfigRepoRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigRepoDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Create Config Repo Endpoint","tags":["config-repos"]}},"/api/v1/config-repos/{config_repo_id}":{"get":{"description":"Detail by id; 404 ``CONFIG_REPO_NOT_FOUND`` if missing.\n\nfeat_config_repo_baseline_tracking FR-4 — when\n``last_merged_proposal_id`` is set, embed the pointed-at proposal as a\n:class:`ProposalSummary` with ``is_currently_live=True``. The embed-side\nderivation uses the pointer context directly (NOT the generic\n``proposals → clusters → config_repos`` JOIN used elsewhere) so the\nbadge renders correctly even when the proposal's cluster was later\nunwired from this config_repo (spec §19 \"Cluster-with-config_repo-\nrotated\" decision-log entry).","operationId":"get_config_repo_endpoint_api_v1_config_repos__config_repo_id__get","parameters":[{"in":"path","name":"config_repo_id","required":true,"schema":{"title":"Config Repo Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigRepoDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Config Repo Endpoint","tags":["config-repos"]}},"/api/v1/conversations":{"get":{"description":"List conversations newest-first with per-row message_count + X-Total-Count header.\n\n``?since=`` (Story 1.5 — closes api-conventions.md drift) filters by\n``created_at >= since``. ``?q=`` (Story 1.2) is a Postgres FTS match\nagainst ``search_vector`` (coalesce(title, '')); 2-200 chars.","operationId":"list_conversations_endpoint_api_v1_conversations_get","parameters":[{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"since","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Since"}},{"in":"query","name":"q","required":false,"schema":{"anyOf":[{"maxLength":200,"minLength":2,"type":"string"},{"type":"null"}],"title":"Q"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationsListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Conversations Endpoint","tags":["conversations"]},"post":{"description":"Create a new conversation. Title is optional (FR-1 auto-generates from first message).","operationId":"create_conversation_endpoint_api_v1_conversations_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateConversationRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationSummary"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Create Conversation Endpoint","tags":["conversations"]}},"/api/v1/conversations/{conversation_id}":{"delete":{"description":"Soft-delete the conversation; subsequent reads return 404.","operationId":"delete_conversation_endpoint_api_v1_conversations__conversation_id__delete","parameters":[{"in":"path","name":"conversation_id","required":true,"schema":{"title":"Conversation Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Delete Conversation Endpoint","tags":["conversations"]},"get":{"description":"Return the conversation's full message history.","operationId":"get_conversation_endpoint_api_v1_conversations__conversation_id__get","parameters":[{"in":"path","name":"conversation_id","required":true,"schema":{"title":"Conversation Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Conversation Endpoint","tags":["conversations"]}},"/api/v1/conversations/{conversation_id}/messages":{"post":{"description":"Send a user message and stream the assistant turn as SSE.\n\nPreflight (in order; returns plain JSON envelope, NOT a partial stream):\n A. Conversation exists → else 404 ``CONVERSATION_NOT_FOUND``.\n B. ``Settings.openai_api_key`` populated → else 503 ``OPENAI_NOT_CONFIGURED``.\n C. Daily budget peek under cap → else 503 ``OPENAI_BUDGET_EXCEEDED``.\n\nSuccessful preflight returns a ``StreamingResponse(text/event-stream)``\ndriven by :func:`agent_chat.send_user_message`.","operationId":"post_message_endpoint_api_v1_conversations__conversation_id__messages_post","parameters":[{"in":"path","name":"conversation_id","required":true,"schema":{"title":"Conversation Id","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessageRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Post Message Endpoint","tags":["conversations"]}},"/api/v1/judgment-lists":{"get":{"description":"List judgment lists, newest-first with cursor pagination.\n\n``?since=`` filters by ``created_at >= since`` (Story 1.5). ``?q=`` FTS\nmatch against ``search_vector`` (name + target). ``?sort=`` is a\n:data:`JudgmentListSortKey` value with sort-aware cursor (Story 1.3).\n``?query_set_id`` / ``?cluster_id`` filter to lists belonging to the\nsupplied parent (``bug_judgment_lists_listing_ignores_query_set_filter``\n— required by the create-study modal's Step-2 dropdown so the user\ncan only pick judgment-lists valid for the chosen query-set + cluster;\nwithout these filters the modal returns all rows and the user can\npick a mismatched pair, which the ``POST /api/v1/studies`` cross-\nentity integrity check then rejects at create time with a confusing\n422 ``VALIDATION_ERROR: \"judgment_list query_set_id does not match\nstudy query_set_id\"``).\n\n``?target=`` filters by exact target index/collection name\n(``feat_study_target_judgment_mismatch_guard`` FR-2 — pairs with the\n``POST /studies`` ``JUDGMENT_TARGET_MISMATCH`` 422 so the create-study\nmodal can pre-filter the dropdown to only lists matching the chosen\nstudy target). Bounded by the ES/OpenSearch index-name ceiling\n(255 bytes).","operationId":"list_judgment_lists_endpoint_api_v1_judgment_lists_get","parameters":[{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"since","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Since"}},{"in":"query","name":"q","required":false,"schema":{"anyOf":[{"maxLength":200,"minLength":2,"type":"string"},{"type":"null"}],"title":"Q"}},{"in":"query","name":"sort","required":false,"schema":{"anyOf":[{"enum":["name:asc","name:desc","created_at:asc","created_at:desc","status:asc","status:desc"],"type":"string"},{"type":"null"}],"title":"Sort"}},{"in":"query","name":"query_set_id","required":false,"schema":{"anyOf":[{"maxLength":36,"minLength":1,"type":"string"},{"type":"null"}],"title":"Query Set Id"}},{"in":"query","name":"cluster_id","required":false,"schema":{"anyOf":[{"maxLength":36,"minLength":1,"type":"string"},{"type":"null"}],"title":"Cluster Id"}},{"in":"query","name":"target","required":false,"schema":{"anyOf":[{"maxLength":255,"minLength":1,"type":"string"},{"type":"null"}],"title":"Target"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JudgmentListListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Judgment Lists Endpoint","tags":["judgments"]}},"/api/v1/judgment-lists/import":{"post":{"description":"Create a judgment_lists row with status='complete' + bulk-insert judgments.\n\nTutorial path; no OpenAI involvement. Every supplied judgment must\nreference a ``query_id`` that exists in ``body.query_set_id`` —\nmismatches → 400 ``QUERY_NOT_IN_SET``.","operationId":"import_judgment_list_api_v1_judgment_lists_import_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportJudgmentListRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JudgmentListDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Import Judgment List","tags":["judgments"]}},"/api/v1/judgment-lists/{judgment_list_id}":{"get":{"operationId":"get_judgment_list_endpoint_api_v1_judgment_lists__judgment_list_id__get","parameters":[{"in":"path","name":"judgment_list_id","required":true,"schema":{"title":"Judgment List Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JudgmentListDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Judgment List Endpoint","tags":["judgments"]}},"/api/v1/judgment-lists/{judgment_list_id}/calibration":{"post":{"description":"Compute Cohen's + weighted kappa from supplied human samples.\n\nPairs are built by joining each sample with the existing\n``source='llm'`` judgment at ``(query_id, doc_id)`` — overridden rows\n(``source='human'``) are excluded (per spec FR-5 + GPT-5.5 cycle 1 F12).","operationId":"calibrate_judgment_list_api_v1_judgment_lists__judgment_list_id__calibration_post","parameters":[{"in":"path","name":"judgment_list_id","required":true,"schema":{"title":"Judgment List Id","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CalibrationSamplesRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CalibrationResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Calibrate Judgment List","tags":["judgments"]}},"/api/v1/judgment-lists/{judgment_list_id}/judgments":{"get":{"description":"List per-list judgments with cursor pagination.\n\n``?sort=`` is :data:`JudgmentRowSortKey` with sort-aware cursor\n(feat_data_table_primitive Story 1.3).","operationId":"list_judgments_endpoint_api_v1_judgment_lists__judgment_list_id__judgments_get","parameters":[{"in":"path","name":"judgment_list_id","required":true,"schema":{"title":"Judgment List Id","type":"string"}},{"in":"query","name":"source","required":false,"schema":{"anyOf":[{"enum":["llm","human","click"],"type":"string"},{"type":"null"}],"title":"Source"}},{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"sort","required":false,"schema":{"anyOf":[{"enum":["created_at:asc","created_at:desc","rating:asc","rating:desc","source:asc","source:desc"],"type":"string"},{"type":"null"}],"title":"Sort"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JudgmentListJudgmentsResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Judgments Endpoint","tags":["judgments"]}},"/api/v1/judgment-lists/{judgment_list_id}/judgments/{judgment_id}":{"patch":{"description":"Replace an LLM rating with a human override (UPSERT-replace).","operationId":"override_judgment_api_v1_judgment_lists__judgment_list_id__judgments__judgment_id__patch","parameters":[{"in":"path","name":"judgment_list_id","required":true,"schema":{"title":"Judgment List Id","type":"string"}},{"in":"path","name":"judgment_id","required":true,"schema":{"title":"Judgment Id","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OverrideJudgmentRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JudgmentRow"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Override Judgment","tags":["judgments"]}},"/api/v1/judgments/generate":{"post":{"description":"Create a judgment_lists row + enqueue the worker.\n\nDelegates the full preflight + INSERT + Arq enqueue to\n:func:`backend.app.services.agent_judgments_dispatch.start_judgment_generation`\nso the chat-agent ``generate_judgments_llm`` tool reuses the exact same\nchecks (no duplicated preflight). Wire behavior is identical — same error\ncodes, same status codes, same response shape.","operationId":"generate_judgments_api_v1_judgments_generate_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJudgmentListGenerateRequest"}}},"required":true},"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateJudgmentsResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Generate Judgments","tags":["judgments"]}},"/api/v1/judgments/generate-from-ubi":{"post":{"description":"Start a UBI-derived judgment generation job.\n\nDelegates to\n:func:`backend.app.services.agent_judgments_dispatch.start_ubi_judgment_generation`\nwhich runs the full FR-4 preflight (U-A..U-H) before INSERT + Arq\nenqueue. The Pydantic ``model_validator`` on\n:class:`CreateJudgmentListFromUbiRequest` already enforces the\nhybrid conditional (``current_template_id`` + ``rubric`` required\niff ``converter == 'hybrid_ubi_llm'``); the dispatcher trusts the\nvalidated request.","operationId":"generate_judgments_from_ubi_api_v1_judgments_generate_from_ubi_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJudgmentListFromUbiRequest"}}},"required":true},"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateJudgmentsResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Generate Judgments From Ubi","tags":["judgments"]}},"/api/v1/proposals":{"get":{"description":"List proposals with cursor pagination + filters.\n\n``?template_id=`` (Story 1.5) filters by ``proposals.template_id`` FK;\n``?study_id=`` filters by ``proposals.study_id`` FK (used by the\nstudy-detail page's pending-proposal lookup). Both reject invalid\nUUIDs with 422 via FastAPI's UUID parsing. ``?sort=`` (Story 1.3) is\na :data:`ProposalSortKey` value with sort-aware cursor.","operationId":"list_proposals_endpoint_api_v1_proposals_get","parameters":[{"in":"query","name":"status","required":false,"schema":{"anyOf":[{"enum":["pending","pr_opened","pr_merged","rejected"],"type":"string"},{"type":"null"}],"title":"Status"}},{"in":"query","name":"cluster_id","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cluster Id"}},{"in":"query","name":"source","required":false,"schema":{"anyOf":[{"enum":["study","manual"],"type":"string"},{"type":"null"}],"title":"Source"}},{"in":"query","name":"template_id","required":false,"schema":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"title":"Template Id"}},{"in":"query","name":"study_id","required":false,"schema":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"title":"Study Id"}},{"in":"query","name":"is_last_merged","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Last Merged"}},{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"sort","required":false,"schema":{"anyOf":[{"enum":["created_at:asc","created_at:desc","status:asc","status:desc","pr_state:asc","pr_state:desc"],"type":"string"},{"type":"null"}],"title":"Sort"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProposalsListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Proposals Endpoint","tags":["proposals"]},"post":{"description":"Manually create a proposal (chat-agent hand-crafted tweaks).\n\n``study_id`` and ``study_trial_id`` are NULL for manual proposals.\nValidates FK targets (cluster + template exist) before insert.","operationId":"create_manual_proposal_api_v1_proposals_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProposalRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProposalDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Create Manual Proposal","tags":["proposals"]}},"/api/v1/proposals/{proposal_id}":{"get":{"operationId":"get_proposal_endpoint_api_v1_proposals__proposal_id__get","parameters":[{"in":"path","name":"proposal_id","required":true,"schema":{"title":"Proposal Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProposalDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Proposal Endpoint","tags":["proposals"]}},"/api/v1/proposals/{proposal_id}/open_pr":{"post":{"description":"Enqueue the ``open_pr`` worker for an operator-approved proposal.\n\nDelegates the full preflight + Arq enqueue to\n:func:`backend.app.services.agent_proposals_dispatch.open_pr` so the\nchat-agent ``open_pr`` tool reuses the same checks. Wire behavior is\nidentical — same error codes, status codes, response shape.","operationId":"open_pr_endpoint_api_v1_proposals__proposal_id__open_pr_post","parameters":[{"in":"path","name":"proposal_id","required":true,"schema":{"title":"Proposal Id","type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenPrResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Open Pr Endpoint","tags":["proposals"]}},"/api/v1/proposals/{proposal_id}/reject":{"post":{"description":"AC-5: ``pending → rejected`` transition; 409 INVALID_STATE_TRANSITION otherwise.","operationId":"reject_proposal_endpoint_api_v1_proposals__proposal_id__reject_post","parameters":[{"in":"path","name":"proposal_id","required":true,"schema":{"title":"Proposal Id","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RejectProposalRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProposalDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Reject Proposal Endpoint","tags":["proposals"]}},"/api/v1/query-sets":{"get":{"description":"List query sets with cursor pagination + X-Total-Count.\n\n``?q=`` is FTS match against ``search_vector`` (name). ``?sort=`` is a\n:data:`QuerySetSortKey` value; cursor is sort-aware.","operationId":"list_query_sets_api_v1_query_sets_get","parameters":[{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"since","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Since"}},{"in":"query","name":"q","required":false,"schema":{"anyOf":[{"maxLength":200,"minLength":2,"type":"string"},{"type":"null"}],"title":"Q"}},{"in":"query","name":"sort","required":false,"schema":{"anyOf":[{"enum":["name:asc","name:desc","created_at:asc","created_at:desc"],"type":"string"},{"type":"null"}],"title":"Sort"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuerySetListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Query Sets","tags":["query-sets"]},"post":{"description":"Register a query set under a cluster (FR-3).","operationId":"create_query_set_api_v1_query_sets_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateQuerySetRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuerySetDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Create Query Set","tags":["query-sets"]}},"/api/v1/query-sets/{query_set_id}":{"get":{"description":"Return a query set by id (includes ``query_count``).","operationId":"get_query_set_detail_api_v1_query_sets__query_set_id__get","parameters":[{"in":"path","name":"query_set_id","required":true,"schema":{"title":"Query Set Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuerySetDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Query Set Detail","tags":["query-sets"]}},"/api/v1/query-sets/{query_set_id}/queries":{"get":{"description":"List per-query rows under a query set, with derived ``judgment_count``.","operationId":"list_queries_in_set_api_v1_query_sets__query_set_id__queries_get","parameters":[{"in":"path","name":"query_set_id","required":true,"schema":{"title":"Query Set Id","type":"string"}},{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"since","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Since"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Queries In Set","tags":["query-sets"]},"post":{"description":"Bulk-add queries to a set (FR-3 + AC-8).\n\nDispatches on Content-Type:\n\n* ``application/json`` → :class:`BulkQueriesJsonRequest` Pydantic-parse.\n* ``text/csv`` → :func:`parse_queries_csv` (AC-8).\n\nOther content types → 415-equivalent surfaced as 400 ``INVALID_CSV``\n(the documented error code for content-type-mismatch in spec §7.5).","operationId":"bulk_add_queries_api_v1_query_sets__query_set_id__queries_post","parameters":[{"in":"path","name":"query_set_id","required":true,"schema":{"title":"Query Set Id","type":"string"}}],"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkQueriesResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Bulk Add Queries","tags":["query-sets"]}},"/api/v1/query-sets/{query_set_id}/queries/{query_id}":{"delete":{"description":"Hard-delete a query. FK-guarded — 409 if any judgment references it.","operationId":"delete_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__delete","parameters":[{"in":"path","name":"query_set_id","required":true,"schema":{"title":"Query Set Id","type":"string"}},{"in":"path","name":"query_id","required":true,"schema":{"title":"Query Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryHasJudgmentsEnvelope"}}},"description":"Conflict"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Delete Query Endpoint","tags":["query-sets"]},"patch":{"description":"Partial-update a query. Whole-object replace on ``query_metadata``.","operationId":"update_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__patch","parameters":[{"in":"path","name":"query_set_id","required":true,"schema":{"title":"Query Set Id","type":"string"}},{"in":"path","name":"query_id","required":true,"schema":{"title":"Query Id","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateQueryRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryRow"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Update Query Endpoint","tags":["query-sets"]}},"/api/v1/query-templates":{"get":{"description":"List query templates with cursor pagination + X-Total-Count header.\n\n``?q=`` FTS match (name). ``?sort=`` sort-aware cursor (Story 1.3).\n``?engine_type=`` filters by engine (Story 1.4).","operationId":"list_query_templates_api_v1_query_templates_get","parameters":[{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"since","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Since"}},{"in":"query","name":"q","required":false,"schema":{"anyOf":[{"maxLength":200,"minLength":2,"type":"string"},{"type":"null"}],"title":"Q"}},{"in":"query","name":"sort","required":false,"schema":{"anyOf":[{"enum":["name:asc","name:desc","created_at:asc","created_at:desc","engine_type:asc","engine_type:desc","version:asc","version:desc"],"type":"string"},{"type":"null"}],"title":"Sort"}},{"in":"query","name":"engine_type","required":false,"schema":{"anyOf":[{"enum":["elasticsearch","opensearch","solr"],"type":"string"},{"type":"null"}],"title":"Engine Type"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryTemplateListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Query Templates","tags":["query-templates"]},"post":{"description":"Register a query template (FR-2 + AC-7).\n\nAC-7: a body containing ``{{ os.system('rm -rf /') }}`` surfaces as\n400 ``INVALID_TEMPLATE_SYNTAX`` (the AST walk catches the ``Call``\nnode before reaching the meta-vars cross-check that would otherwise\nclassify ``os`` as ``UndeclaredParamUsed``).","operationId":"create_query_template_api_v1_query_templates_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateQueryTemplateRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryTemplateDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Create Query Template","tags":["query-templates"]}},"/api/v1/query-templates/{template_id}":{"get":{"description":"Return a query template by id.","operationId":"get_query_template_detail_api_v1_query_templates__template_id__get","parameters":[{"in":"path","name":"template_id","required":true,"schema":{"title":"Template Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryTemplateDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Query Template Detail","tags":["query-templates"]}},"/api/v1/studies":{"get":{"description":"List studies with cursor pagination + X-Total-Count.\n\n``?status=`` is typed as :data:`StudyStatusWire` so FastAPI returns\n422 ``VALIDATION_ERROR`` for unsupported values. ``?q=`` is a Postgres\nFTS match against ``search_vector`` (name + target). ``?sort=`` is a\n:data:`StudySortKey` value (``:``); the cursor is\nsort-aware (feat_data_table_primitive Stories 1.2 + 1.3).\n\n``?target=`` (feat_index_document_browser FR-5) scopes the list to\nstudies targeting a single index/collection. Composes with all other\nfilters via AND.","operationId":"list_studies_api_v1_studies_get","parameters":[{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"since","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Since"}},{"in":"query","name":"status","required":false,"schema":{"anyOf":[{"enum":["queued","running","completed","cancelled","failed"],"type":"string"},{"type":"null"}],"title":"Status"}},{"in":"query","name":"cluster_id","required":false,"schema":{"anyOf":[{"maxLength":36,"minLength":1,"type":"string"},{"type":"null"}],"title":"Cluster Id"}},{"in":"query","name":"target","required":false,"schema":{"anyOf":[{"maxLength":256,"minLength":1,"type":"string"},{"type":"null"}],"title":"Target"}},{"in":"query","name":"q","required":false,"schema":{"anyOf":[{"maxLength":200,"minLength":2,"type":"string"},{"type":"null"}],"title":"Q"}},{"in":"query","name":"sort","required":false,"schema":{"anyOf":[{"enum":["name:asc","name:desc","created_at:asc","created_at:desc","completed_at:asc","completed_at:desc","best_metric:asc","best_metric:desc","status:asc","status:desc"],"type":"string"},{"type":"null"}],"title":"Sort"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudyListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Studies","tags":["studies"]},"post":{"description":"Create a study (FR-1 + AC-1) and enqueue the orchestrator job.","operationId":"create_study_api_v1_studies_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateStudyRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudyDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Create Study","tags":["studies"]}},"/api/v1/studies/{study_id}":{"get":{"description":"Return a study by id (includes ``trials_summary``).","operationId":"get_study_detail_api_v1_studies__study_id__get","parameters":[{"in":"path","name":"study_id","required":true,"schema":{"title":"Study Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudyDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Study Detail","tags":["studies"]}},"/api/v1/studies/{study_id}/cancel":{"post":{"description":"Cancel a study (Story 2.3, FR-8 + AC-8/AC-9).\n\nOptionally cascades to in-flight chain children.\n\n``?cascade=true`` (default): routes through\n:func:`services.study_state.cancel_study_with_chain_cascade` —\ncancels the parent (if in-flight) AND recursively cancels in-flight\ndescendants. Tolerates terminal parents (recurses through completed\nintermediates to reach an in-flight grandchild).\n\n``?cascade=false``: routes through the original\n:func:`services.study_state.cancel_study` — single-study cancel,\npreserves the existing 409 error contract on terminal parents\n(AC-9 wire contract).","operationId":"cancel_study_api_v1_studies__study_id__cancel_post","parameters":[{"in":"path","name":"study_id","required":true,"schema":{"title":"Study Id","type":"string"}},{"in":"query","name":"cascade","required":false,"schema":{"default":"true","title":"Cascade","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudyDetail"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Cancel Study","tags":["studies"]}},"/api/v1/studies/{study_id}/chain":{"get":{"description":"Return the rolled-up chain summary for the study and its lineage (FR-3).\n\nWalks to the chain anchor, aggregates the completed-link subset into a\nbest link + cumulative lift + derived stop reason, and emits per-link\ndeltas. The anchor's ``delta_from_prev`` is always ``None`` (spec §8.3).\nReturns ``404 STUDY_NOT_FOUND`` when the study does not exist.","operationId":"get_study_chain_api_v1_studies__study_id__chain_get","parameters":[{"in":"path","name":"study_id","required":true,"schema":{"title":"Study Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudyChainResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Study Chain","tags":["studies"]}},"/api/v1/studies/{study_id}/children":{"get":{"description":"List direct child studies of a parent (FR-10 + D-13).\n\nReturns ``{\"data\": [], \"next_cursor\": null}`` for a study with no\nchildren — empty data array, NOT 404. 404 only fires when the parent\nstudy itself is missing.\n\nPer D-13 (direct-children-only): does NOT return transitive\ndescendants. The chain panel renders parent ↑ + direct children ↓;\noperators walk lineage one hop per page navigation.","operationId":"list_study_children_api_v1_studies__study_id__children_get","parameters":[{"in":"path","name":"study_id","required":true,"schema":{"title":"Study Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudyListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Study Children","tags":["studies"]}},"/api/v1/studies/{study_id}/digest":{"get":{"description":"Fetch the digest for a completed study.\n\nReturns 404 ``DIGEST_NOT_READY`` (``retryable=true``) when:\n- the study is not in ``status='completed'``, OR\n- the study is completed but the worker hasn't written the digest yet\n (worker lag, or a worker-side terminal failure like\n ``OPENAI_NOT_CONFIGURED`` deferred the run).","operationId":"get_study_digest_api_v1_studies__study_id__digest_get","parameters":[{"in":"path","name":"study_id","required":true,"schema":{"title":"Study Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DigestResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Study Digest","tags":["digests"]}},"/api/v1/studies/{study_id}/trials":{"get":{"description":"List trials in a study (FR-6).\n\nSort variants per spec §7.4: ``primary_metric_desc`` (default),\n``primary_metric_asc``, ``ended_at_desc``, ``ended_at_asc``,\n``optuna_trial_number_asc``.","operationId":"list_study_trials_api_v1_studies__study_id__trials_get","parameters":[{"in":"path","name":"study_id","required":true,"schema":{"title":"Study Id","type":"string"}},{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"since","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Since"}},{"in":"query","name":"sort","required":false,"schema":{"default":"primary_metric_desc","title":"Sort","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrialListResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Study Trials","tags":["trials"]}},"/healthz":{"get":{"description":"Probe each subsystem in parallel and return the documented JSON shape.\n\nArgs:\n settings: Application settings (DB URL, ES/OS URLs, OpenAI base URL, etc.)\n redis_client: Redis client for ping probe + capability-cache read\n es_client: shared httpx client for ES + OpenSearch HTTP probes\n db: Async DB session for the registered-clusters aggregate (Story 3.5)\n\nReturns:\n JSONResponse with the HealthResponse body and HTTP 200 (healthy) or 503 (degraded).","operationId":"healthz_healthz_get","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}},"description":"Successful Response"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}},"description":"One or more required subsystems is down"}},"summary":"Healthz","tags":["operator"]}},"/webhooks/github":{"post":{"description":"Receive a single GitHub webhook delivery.\n\nReturns ``{\"status\": \"ok\", \"action\": }`` where\n``wire_action`` is one of the four values in\n:data:`WEBHOOK_ACTION_VALUES`.\n\nRaises:\n HTTPException(403, INVALID_SIGNATURE): bad signature or unknown\n repository. Both share one error code so the receiver does\n not reveal repo enumeration.","operationId":"github_webhook_webhooks_github_post","responses":{"200":{"content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"title":"Response Github Webhook Webhooks Github Post","type":"object"}}},"description":"Successful Response"}},"summary":"Github Webhook","tags":["webhooks"]}}}} diff --git a/ui/public/guides/03_create_query_template/01-templates-list.png b/ui/public/guides/03_create_query_template/01-templates-list.png index 83c49930..6a08755e 100644 Binary files a/ui/public/guides/03_create_query_template/01-templates-list.png and b/ui/public/guides/03_create_query_template/01-templates-list.png differ diff --git a/ui/public/guides/03_create_query_template/02-create-modal-empty.png b/ui/public/guides/03_create_query_template/02-create-modal-empty.png index a951b706..78864241 100644 Binary files a/ui/public/guides/03_create_query_template/02-create-modal-empty.png and b/ui/public/guides/03_create_query_template/02-create-modal-empty.png differ diff --git a/ui/public/guides/03_create_query_template/03-create-modal-filled.png b/ui/public/guides/03_create_query_template/03-create-modal-filled.png index 0643b75f..e468054e 100644 Binary files a/ui/public/guides/03_create_query_template/03-create-modal-filled.png and b/ui/public/guides/03_create_query_template/03-create-modal-filled.png differ diff --git a/ui/public/guides/03_create_query_template/04-template-created.png b/ui/public/guides/03_create_query_template/04-template-created.png index 28bfe2e0..04c3e3d3 100644 Binary files a/ui/public/guides/03_create_query_template/04-template-created.png and b/ui/public/guides/03_create_query_template/04-template-created.png differ diff --git a/ui/public/guides/03_create_query_template/05-template-detail-fork-button.png b/ui/public/guides/03_create_query_template/05-template-detail-fork-button.png index adf2cccb..2efc8872 100644 Binary files a/ui/public/guides/03_create_query_template/05-template-detail-fork-button.png and b/ui/public/guides/03_create_query_template/05-template-detail-fork-button.png differ diff --git a/ui/public/guides/03_create_query_template/walkthrough.webm b/ui/public/guides/03_create_query_template/walkthrough.webm index 3d36b4dd..e566e00b 100644 Binary files a/ui/public/guides/03_create_query_template/walkthrough.webm and b/ui/public/guides/03_create_query_template/walkthrough.webm differ diff --git a/ui/public/guides/04_create_query_set/01-query-sets-list.png b/ui/public/guides/04_create_query_set/01-query-sets-list.png index dfb348e7..1880b6c8 100644 Binary files a/ui/public/guides/04_create_query_set/01-query-sets-list.png and b/ui/public/guides/04_create_query_set/01-query-sets-list.png differ diff --git a/ui/public/guides/04_create_query_set/02-create-modal-empty.png b/ui/public/guides/04_create_query_set/02-create-modal-empty.png index 7df42256..5c3e34a9 100644 Binary files a/ui/public/guides/04_create_query_set/02-create-modal-empty.png and b/ui/public/guides/04_create_query_set/02-create-modal-empty.png differ diff --git a/ui/public/guides/04_create_query_set/03-create-modal-filled.png b/ui/public/guides/04_create_query_set/03-create-modal-filled.png index 58e8f5ce..5fd983e4 100644 Binary files a/ui/public/guides/04_create_query_set/03-create-modal-filled.png and b/ui/public/guides/04_create_query_set/03-create-modal-filled.png differ diff --git a/ui/public/guides/04_create_query_set/04-query-set-detail-empty.png b/ui/public/guides/04_create_query_set/04-query-set-detail-empty.png index 8604056f..5b1e5127 100644 Binary files a/ui/public/guides/04_create_query_set/04-query-set-detail-empty.png and b/ui/public/guides/04_create_query_set/04-query-set-detail-empty.png differ diff --git a/ui/public/guides/04_create_query_set/05-add-queries-dialog.png b/ui/public/guides/04_create_query_set/05-add-queries-dialog.png index 4a52462a..66b46704 100644 Binary files a/ui/public/guides/04_create_query_set/05-add-queries-dialog.png and b/ui/public/guides/04_create_query_set/05-add-queries-dialog.png differ diff --git a/ui/public/guides/04_create_query_set/walkthrough.webm b/ui/public/guides/04_create_query_set/walkthrough.webm index 597ae849..f76ae5ad 100644 Binary files a/ui/public/guides/04_create_query_set/walkthrough.webm and b/ui/public/guides/04_create_query_set/walkthrough.webm differ diff --git a/ui/src/__tests__/components/common/list-count-columns.test.tsx b/ui/src/__tests__/components/common/list-count-columns.test.tsx new file mode 100644 index 00000000..e9a3c1c3 --- /dev/null +++ b/ui/src/__tests__/components/common/list-count-columns.test.tsx @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2026 soundminds.ai +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Tests for the list-summary count columns (feat_list_count_columns). + * + * - query-sets table gains a "Queries" column reading `query_count`. + * - templates table gains a "Parameters" column reading `param_count`. + * + * Both render the integer via `toLocaleString()` (thousands separators for + * large sets). The cell renderers only read `row.original`, so a minimal + * stub object covers the contract without standing up a TanStack table. + */ + +import { render, screen } from '@testing-library/react'; +import type { ReactNode } from 'react'; +import { describe, expect, it } from 'vitest'; + +import { querySetsColumns } from '@/components/query-sets/query-sets-table.column-config'; +import { templatesColumns } from '@/components/templates/templates-table.column-config'; +import type { QuerySetSummary } from '@/lib/api/query-sets'; +import type { QueryTemplateSummary } from '@/lib/api/query-templates'; + +function renderCell( + columns: { id?: string; cell?: unknown }[], + columnId: string, + original: T, +): void { + const column = columns.find((c) => c.id === columnId); + if (!column?.cell || typeof column.cell !== 'function') { + throw new Error(`column ${columnId} or its cell renderer not found`); + } + const cell = column.cell as (ctx: { row: { original: T } }) => ReactNode; + render(<>{cell({ row: { original } })}); +} + +const baseQuerySet: QuerySetSummary = { + id: 'qs-1', + name: 'demo set', + cluster_id: 'c1', + query_count: 0, + created_at: '2026-06-03T00:00:00Z', +}; + +const baseTemplate: QueryTemplateSummary = { + id: 'tmpl-1', + name: 'demo template', + engine_type: 'elasticsearch', + version: 1, + param_count: 0, + created_at: '2026-06-03T00:00:00Z', +}; + +describe('query-sets table — Queries column (query_count)', () => { + it('the column exists with header "Queries"', () => { + const col = querySetsColumns.find((c) => c.id === 'query_count'); + expect(col).toBeDefined(); + expect(col?.header).toBe('Queries'); + }); + + it('renders the query_count value', () => { + renderCell(querySetsColumns, 'query_count', { ...baseQuerySet, query_count: 42 }); + expect(screen.getByText('42')).toBeInTheDocument(); + }); + + it('renders 0 for an empty set (not blank/undefined)', () => { + renderCell(querySetsColumns, 'query_count', { ...baseQuerySet, query_count: 0 }); + expect(screen.getByText('0')).toBeInTheDocument(); + }); + + it('formats large counts with thousands separators', () => { + renderCell(querySetsColumns, 'query_count', { ...baseQuerySet, query_count: 12500 }); + expect(screen.getByText('12,500')).toBeInTheDocument(); + }); +}); + +describe('templates table — Parameters column (param_count)', () => { + it('the column exists with header "Parameters"', () => { + const col = templatesColumns.find((c) => c.id === 'param_count'); + expect(col).toBeDefined(); + expect(col?.header).toBe('Parameters'); + }); + + it('renders the param_count value', () => { + renderCell(templatesColumns, 'param_count', { ...baseTemplate, param_count: 6 }); + expect(screen.getByText('6')).toBeInTheDocument(); + }); + + it('renders 0 for a non-tunable template', () => { + renderCell(templatesColumns, 'param_count', { ...baseTemplate, param_count: 0 }); + expect(screen.getByText('0')).toBeInTheDocument(); + }); +}); diff --git a/ui/src/components/query-sets/query-sets-table.column-config.tsx b/ui/src/components/query-sets/query-sets-table.column-config.tsx index 70dd41a4..61710ac3 100644 --- a/ui/src/components/query-sets/query-sets-table.column-config.tsx +++ b/ui/src/components/query-sets/query-sets-table.column-config.tsx @@ -5,8 +5,10 @@ /** * Column configuration for `` (feat_data_table_primitive Story 3.5). * - * 3 columns: Name (link, sortable+sticky), Cluster, Created (sortable). - * No filters. FTS on `name` (Story 1.2). + * 4 columns: Name (link, sortable+sticky), Cluster, Queries (count), Created + * (sortable). No filters. FTS on `name` (Story 1.2). The Queries column reads + * the `query_count` field added to `QuerySetSummary` by feat_list_count_columns + * (resolved server-side via one batched aggregate per page — no N+1). */ import Link from 'next/link'; @@ -35,6 +37,14 @@ export const querySetsColumns: DataTableColumnDef[] = [ accessorKey: 'cluster_id', cell: ({ row }) => {row.original.cluster_id}, }, + { + id: 'query_count', + header: 'Queries', + accessorKey: 'query_count', + cell: ({ row }) => ( + {row.original.query_count.toLocaleString()} + ), + }, { id: 'created_at', header: 'Created', diff --git a/ui/src/components/templates/templates-table.column-config.tsx b/ui/src/components/templates/templates-table.column-config.tsx index f217815f..401b36d1 100644 --- a/ui/src/components/templates/templates-table.column-config.tsx +++ b/ui/src/components/templates/templates-table.column-config.tsx @@ -5,8 +5,12 @@ /** * Column configuration for `` (feat_data_table_primitive Story 3.4). * - * 4 columns: Name (link, sortable+sticky), Engine (sortable, filter enum), - * Version (sortable), Created (sortable, hideable). FTS on `name` (Story 1.2). + * 5 columns: Name (link, sortable+sticky), Engine (sortable, filter enum), + * Version (sortable), Parameters (declared-param count), Created (sortable, + * hideable). FTS on `name` (Story 1.2). The Parameters column reads the + * `param_count` field (= len of declared_params) added to + * `QueryTemplateSummary` by feat_list_count_columns — it surfaces each + * template's tuning surface at a glance (0 = non-tunable). */ import Link from 'next/link'; @@ -49,6 +53,14 @@ export const templatesColumns: DataTableColumnDef[] = [ firstClickDirection: 'desc', cell: ({ row }) => `v${row.original.version}`, }, + { + id: 'param_count', + header: 'Parameters', + accessorKey: 'param_count', + cell: ({ row }) => ( + {row.original.param_count.toLocaleString()} + ), + }, { id: 'created_at', header: 'Created', diff --git a/ui/src/lib/types.ts b/ui/src/lib/types.ts index 4fe9649a..adc262a7 100644 --- a/ui/src/lib/types.ts +++ b/ui/src/lib/types.ts @@ -2809,7 +2809,15 @@ export interface components { }; /** * QuerySetSummary - * @description List-view shape; omits ``query_count`` to avoid N+1 counts at list time. + * @description List-view shape. + * + * ``query_count`` is the number of queries in the set. It is resolved + * via a single batched ``GROUP BY query_set_id`` aggregate per page + * (``repo.count_queries_for_sets``), NOT a per-row count — so the + * list endpoint stays at a fixed 2 queries (the page + the count + * aggregate) regardless of page size. This is the same no-N+1 pattern + * ``feat_studies_convergence_visibility`` (PR #421) used for the + * studies-list ``trial_count`` field. */ QuerySetSummary: { /** Cluster Id */ @@ -2823,6 +2831,8 @@ export interface components { id: string; /** Name */ name: string; + /** Query Count */ + query_count: number; }; /** * QueryTemplateDetail @@ -2868,7 +2878,14 @@ export interface components { }; /** * QueryTemplateSummary - * @description List-view shape; drops ``body`` + ``declared_params`` for brevity. + * @description List-view shape; drops ``body`` + the full ``declared_params`` dict. + * + * Surfaces ``param_count`` (= ``len(declared_params)``) so the + * templates list can show each template's tuning surface at a glance. + * ``param_count`` is free to compute — ``declared_params`` is a JSONB + * column already loaded on the row (not a child relationship), so the + * count is ``len(row.declared_params)`` with no extra query and no + * N+1 risk. The full dict remains on ``QueryTemplateDetail``. */ QueryTemplateSummary: { /** @@ -2885,6 +2902,8 @@ export interface components { id: string; /** Name */ name: string; + /** Param Count */ + param_count: number; /** Version */ version: number; };