From af5d140a0b893529189dd990ae537bed0c107f69 Mon Sep 17 00:00:00 2001 From: SoundMindsAI Date: Tue, 2 Jun 2026 22:56:35 -0400 Subject: [PATCH 01/10] infra(copy-docs): prune ui/public/docs/ to exact generated set (Story 1.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Story 1.1 of infra_generated_artifact_freshness_gate (FR-9 / AC-11): make copy-docs.mjs delete any *.md not in {README.md} ∪ {DOCS[].dest} so a renamed or removed DOCS entry no longer leaves a stale public copy. - Refactor copy-docs.mjs to export DOCS, getDestDir, pruneStale, runCopyDocs + add an ESM entrypoint guard so importing the module no longer triggers generation (mirrors gen-types.mjs pattern). - Add ui/src/__tests__/scripts/copy-docs.prune.test.ts (11 cases): exported-shape sanity, pruneStale direct behavior (delete .md, preserve non-.md, no-op on clean), runCopyDocs end-to-end against tmp dirs (clean run, prune-on-removed-entry, idempotency, rename-mid-flight, cwd-equivalence, entry-point-guard). - Verified operator path: node ui/scripts/copy-docs.mjs on a clean tree leaves git status --porcelain -- ui/public/docs/ empty. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: SoundMindsAI --- ui/scripts/copy-docs.mjs | 140 +++++++++-- .../__tests__/scripts/copy-docs.prune.test.ts | 234 ++++++++++++++++++ 2 files changed, 348 insertions(+), 26 deletions(-) create mode 100644 ui/src/__tests__/scripts/copy-docs.prune.test.ts diff --git a/ui/scripts/copy-docs.mjs b/ui/scripts/copy-docs.mjs index 34ba1975..404fcdb9 100644 --- a/ui/scripts/copy-docs.mjs +++ b/ui/scripts/copy-docs.mjs @@ -17,46 +17,134 @@ * Single-direction sync — the source of truth is `docs/08_guides/`. Editors * should NOT modify `ui/public/docs/` directly; their changes will be * overwritten on the next build. + * + * The script also PRUNES the destination dir to exactly + * `{README.md} ∪ {DOCS[].dest}` so that a removed or renamed `DOCS` entry + * does not leave a stale public copy behind (FR-9 of + * `infra_generated_artifact_freshness_gate`). Anything outside the expected + * set with a `.md` suffix is deleted. + * + * Module shape: the file exposes `DOCS`, `pruneStale`, `getDestDir`, and + * `runCopyDocs` as named exports so the vitest at + * `ui/src/__tests__/scripts/copy-docs.prune.test.ts` can exercise the prune + * logic hermetically against a tmp directory. Importing the module does NOT + * trigger generation — the bottom-of-file ESM entrypoint check + * (`import.meta.url === pathToFileURL(process.argv[1]).href`) gates the + * actual run, mirroring the `ui/scripts/gen-types.mjs` pattern. */ -import { copyFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import { + copyFileSync, + existsSync, + mkdirSync, + readdirSync, + unlinkSync, + writeFileSync, +} from 'node:fs'; import { dirname, join, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(__dirname, '..', '..'); const srcDir = join(repoRoot, 'docs', '08_guides'); -const destDir = join(__dirname, '..', 'public', 'docs'); -const DOCS = [ +/** + * The canonical destination directory (`ui/public/docs/`). Exported as a + * function so callers always get the absolute path regardless of `cwd` — + * see the `import.meta.url` resolution above. The vitest uses this to + * assert cwd-equivalence (FR-1 cwd-robustness). + * @returns {string} + */ +export function getDestDir() { + return join(__dirname, '..', 'public', 'docs'); +} + +/** + * @typedef {Object} DocEntry + * @property {string} src - basename under `docs/08_guides/` + * @property {string} dest - basename under `ui/public/docs/` + */ + +/** @type {readonly DocEntry[]} */ +export const DOCS = Object.freeze([ { src: 'tutorial-first-study.md', dest: 'tutorial-first-study.md' }, { src: 'quick-tour.md', dest: 'quick-tour.md' }, { src: 'workflows-overview.md', dest: 'workflows-overview.md' }, -]; - -mkdirSync(destDir, { recursive: true }); - -for (const { src, dest } of DOCS) { - const fromPath = join(srcDir, src); - const toPath = join(destDir, dest); - if (!existsSync(fromPath)) { - console.warn(`[copy-docs] WARNING: source file missing: ${fromPath}`); - continue; - } - copyFileSync(fromPath, toPath); - console.log(`[copy-docs] ${src} -> public/docs/${dest}`); -} +]); -// Drop a tiny README into the dest dir so contributors who find these files -// in `ui/public/docs/` know they're generated. -writeFileSync( - join(destDir, 'README.md'), - `# ui/public/docs/ — GENERATED +const README_CONTENT = `# ui/public/docs/ — GENERATED These markdown files are copied from \`docs/08_guides/\` at build time by \`ui/scripts/copy-docs.mjs\` (wired into the package.json \`prebuild\` hook). DO NOT edit them here — your changes will be overwritten. Edit the source files in \`docs/08_guides/\` instead. -`, -); -console.log(`[copy-docs] done`); +`; + +/** + * Delete any `*.md` file in `destDir` whose basename is not in + * `expectedNames`. README.md is preserved because callers include it in + * the expected set. Returns the basenames that were pruned (for logging + * and test assertions). Non-`.md` files are left alone. + * + * @param {string} destDir + * @param {Set} expectedNames + * @returns {string[]} + */ +export function pruneStale(destDir, expectedNames) { + const pruned = []; + for (const f of readdirSync(destDir)) { + if (f.endsWith('.md') && !expectedNames.has(f)) { + unlinkSync(join(destDir, f)); + pruned.push(f); + } + } + return pruned; +} + +/** + * Full sync: ensure dest dir exists, copy every entry in DOCS, write the + * README, then prune any obsolete `*.md`. Idempotent — a second call on + * an up-to-date tree is a no-op as far as `git status` is concerned. + * + * @param {Object} [opts] + * @param {string} [opts.destDir] - override (defaults to `getDestDir()`) + * @param {string} [opts.sourceDir] - override (defaults to `docs/08_guides/`) + * @param {readonly DocEntry[]} [opts.docs] - override (defaults to `DOCS`) + * @returns {{copied: string[], pruned: string[]}} + */ +export function runCopyDocs(opts = {}) { + const destDirResolved = opts.destDir ?? getDestDir(); + const sourceDirResolved = opts.sourceDir ?? srcDir; + const docs = opts.docs ?? DOCS; + const copied = []; + + mkdirSync(destDirResolved, { recursive: true }); + + for (const { src, dest } of docs) { + const fromPath = join(sourceDirResolved, src); + const toPath = join(destDirResolved, dest); + if (!existsSync(fromPath)) { + console.warn(`[copy-docs] WARNING: source file missing: ${fromPath}`); + continue; + } + copyFileSync(fromPath, toPath); + copied.push(dest); + console.log(`[copy-docs] ${src} -> public/docs/${dest}`); + } + + writeFileSync(join(destDirResolved, 'README.md'), README_CONTENT); + + const expected = new Set(['README.md', ...docs.map((d) => d.dest)]); + const pruned = pruneStale(destDirResolved, expected); + for (const f of pruned) { + console.log(`[copy-docs] pruned obsolete public/docs/${f}`); + } + console.log(`[copy-docs] done`); + return { copied, pruned }; +} + +// Entry-point guard: run only when invoked as the main script (not when +// imported by tests). Mirrors the `ui/scripts/gen-types.mjs` pattern. +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + runCopyDocs(); +} diff --git a/ui/src/__tests__/scripts/copy-docs.prune.test.ts b/ui/src/__tests__/scripts/copy-docs.prune.test.ts new file mode 100644 index 00000000..d8fa337a --- /dev/null +++ b/ui/src/__tests__/scripts/copy-docs.prune.test.ts @@ -0,0 +1,234 @@ +// SPDX-FileCopyrightText: 2026 soundminds.ai +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Vitest for `ui/scripts/copy-docs.mjs` prune behavior (Story 1.1 of + * `infra_generated_artifact_freshness_gate`, FR-9 / AC-11). + * + * The script-under-test prunes any `*.md` file in its destination that + * isn't in `{README.md} ∪ {DOCS[].dest}`, so that a renamed or removed + * `DOCS` entry doesn't leave a stale public copy behind. These tests + * exercise that behavior hermetically against a tmp directory using the + * exported `pruneStale` / `runCopyDocs` / `getDestDir` symbols (the script + * exposes them after the Story 1.1 refactor + adds an ESM entrypoint + * guard so importing the module does not generate anything). + * + * The tests also assert cwd-equivalence (FR-1) by calling `getDestDir` + * with two different `process.cwd()` values and asserting the resolved + * absolute path is identical — the script resolves paths via + * `import.meta.url`, so the result is cwd-invariant by construction. + */ + +import { mkdtempSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore — the .mjs ships its own JSDoc types; vitest resolves it natively. +import { DOCS, getDestDir, pruneStale, runCopyDocs } from '../../../scripts/copy-docs.mjs'; + +const EXPECTED_DOC_BASENAMES = [ + 'tutorial-first-study.md', + 'quick-tour.md', + 'workflows-overview.md', +]; + +interface DocEntry { + src: string; + dest: string; +} + +describe('copy-docs.mjs — exported shape', () => { + it('exports the expected DOCS entries (canonical source for prune)', () => { + // The prune set is `{README.md} ∪ {DOCS[].dest}`. If DOCS drifts, the + // CI freshness gate and this test both update in lockstep. + expect((DOCS as readonly DocEntry[]).map((d) => d.dest).sort()).toEqual( + [...EXPECTED_DOC_BASENAMES].sort(), + ); + }); + + it('getDestDir() returns an absolute path invariant under cwd (FR-1 cwd-equivalence)', () => { + const original = process.cwd(); + try { + // Resolved once with cwd=repoRoot. + process.chdir(resolve(original)); + const fromRoot = getDestDir() as string; + // Resolved again with cwd=os.tmpdir — totally unrelated to the repo. + process.chdir(tmpdir()); + const fromTmp = getDestDir() as string; + expect(fromRoot).toBe(fromTmp); + expect(fromRoot.endsWith(`${join('ui', 'public', 'docs')}`)).toBe(true); + } finally { + process.chdir(original); + } + }); +}); + +describe('pruneStale — direct behavior', () => { + let tmp: string; + + beforeEach(() => { + tmp = mkdtempSync(join(tmpdir(), 'rl-prune-')); + }); + afterEach(() => { + rmSync(tmp, { recursive: true, force: true }); + }); + + it('deletes *.md files not in the expected set', () => { + writeFileSync(join(tmp, 'keep.md'), 'keep'); + writeFileSync(join(tmp, 'obsolete.md'), 'obsolete'); + writeFileSync(join(tmp, 'README.md'), 'readme'); + + const pruned = pruneStale(tmp, new Set(['README.md', 'keep.md'])) as string[]; + + expect(pruned).toEqual(['obsolete.md']); + expect(readdirSync(tmp).sort()).toEqual(['README.md', 'keep.md']); + }); + + it('preserves non-*.md files (only the .md surface is gated)', () => { + writeFileSync(join(tmp, 'unrelated.txt'), 'leave-me-alone'); + writeFileSync(join(tmp, 'obsolete.md'), 'obsolete'); + + const pruned = pruneStale(tmp, new Set(['README.md'])) as string[]; + + expect(pruned).toEqual(['obsolete.md']); + expect(readdirSync(tmp).sort()).toEqual(['unrelated.txt']); + }); + + it('is a no-op when the directory already matches the expected set', () => { + writeFileSync(join(tmp, 'README.md'), 'readme'); + writeFileSync(join(tmp, 'quick-tour.md'), 'tour'); + + const pruned = pruneStale(tmp, new Set(['README.md', 'quick-tour.md'])) as string[]; + + expect(pruned).toEqual([]); + expect(readdirSync(tmp).sort()).toEqual(['README.md', 'quick-tour.md']); + }); +}); + +describe('runCopyDocs — end-to-end against tmp dirs', () => { + let srcTmp: string; + let destTmp: string; + + beforeEach(() => { + srcTmp = mkdtempSync(join(tmpdir(), 'rl-copydocs-src-')); + destTmp = mkdtempSync(join(tmpdir(), 'rl-copydocs-dest-')); + // Seed the source dir with the three real guide basenames. + for (const f of EXPECTED_DOC_BASENAMES) { + writeFileSync(join(srcTmp, f), `# ${f} — fixture body`); + } + }); + afterEach(() => { + rmSync(srcTmp, { recursive: true, force: true }); + rmSync(destTmp, { recursive: true, force: true }); + }); + + it('produces the exact expected set on a clean run (AC-11 happy path)', () => { + const { copied, pruned } = runCopyDocs({ + destDir: destTmp, + sourceDir: srcTmp, + docs: DOCS, + }) as { copied: string[]; pruned: string[] }; + + expect(copied.sort()).toEqual([...EXPECTED_DOC_BASENAMES].sort()); + expect(pruned).toEqual([]); + expect(readdirSync(destTmp).sort()).toEqual(['README.md', ...EXPECTED_DOC_BASENAMES].sort()); + }); + + it('prunes a stale public copy left by a removed DOCS entry (AC-11)', () => { + // Pre-seed the dest with a guide that USED TO BE in DOCS but no longer is. + writeFileSync(join(destTmp, 'old-renamed.md'), '# obsolete public copy'); + // Run with the current DOCS (which does NOT include `old-renamed.md`). + const { pruned } = runCopyDocs({ + destDir: destTmp, + sourceDir: srcTmp, + docs: DOCS, + }) as { copied: string[]; pruned: string[] }; + + expect(pruned).toContain('old-renamed.md'); + expect(readdirSync(destTmp).sort()).toEqual(['README.md', ...EXPECTED_DOC_BASENAMES].sort()); + }); + + it('is idempotent — a second run on a clean tree changes nothing', () => { + runCopyDocs({ destDir: destTmp, sourceDir: srcTmp, docs: DOCS }); + const firstRunFiles = readdirSync(destTmp).sort(); + const { copied, pruned } = runCopyDocs({ + destDir: destTmp, + sourceDir: srcTmp, + docs: DOCS, + }) as { copied: string[]; pruned: string[] }; + + expect(pruned).toEqual([]); + expect(copied.sort()).toEqual([...EXPECTED_DOC_BASENAMES].sort()); + expect(readdirSync(destTmp).sort()).toEqual(firstRunFiles); + }); + + it('prunes when a DOCS entry is removed mid-rename (the FR-9 motivating scenario)', () => { + // Simulate the failure mode FR-9 catches: someone renames a DOCS entry + // from `quick-tour.md` → `quick-tour-v2.md`, runs the script, and + // expects the old public copy to be deleted. + writeFileSync(join(srcTmp, 'quick-tour-v2.md'), '# v2'); + const renamedDocs: readonly DocEntry[] = [ + { src: 'tutorial-first-study.md', dest: 'tutorial-first-study.md' }, + { src: 'quick-tour-v2.md', dest: 'quick-tour-v2.md' }, + { src: 'workflows-overview.md', dest: 'workflows-overview.md' }, + ]; + // First run with old DOCS to seed `quick-tour.md` in dest. + runCopyDocs({ destDir: destTmp, sourceDir: srcTmp, docs: DOCS }); + expect(readdirSync(destTmp)).toContain('quick-tour.md'); + // Now run with the renamed DOCS — `quick-tour.md` should be pruned. + const { pruned } = runCopyDocs({ + destDir: destTmp, + sourceDir: srcTmp, + docs: renamedDocs, + }) as { copied: string[]; pruned: string[] }; + + expect(pruned).toContain('quick-tour.md'); + expect(readdirSync(destTmp)).not.toContain('quick-tour.md'); + expect(readdirSync(destTmp)).toContain('quick-tour-v2.md'); + }); + + it('cwd-equivalence: behavior is identical whether cwd is repo-root or ui/', () => { + // Structural assertion (the script uses `import.meta.url` for path + // resolution, so cwd is irrelevant). We assert behavioral parity by + // running twice from different cwds against fresh tmp dirs. + const original = process.cwd(); + const dest1 = mkdtempSync(join(tmpdir(), 'rl-cwd1-')); + const dest2 = mkdtempSync(join(tmpdir(), 'rl-cwd2-')); + try { + process.chdir(original); + runCopyDocs({ destDir: dest1, sourceDir: srcTmp, docs: DOCS }); + + process.chdir(tmpdir()); + runCopyDocs({ destDir: dest2, sourceDir: srcTmp, docs: DOCS }); + + expect(readdirSync(dest1).sort()).toEqual(readdirSync(dest2).sort()); + } finally { + process.chdir(original); + rmSync(dest1, { recursive: true, force: true }); + rmSync(dest2, { recursive: true, force: true }); + } + }); +}); + +describe('copy-docs.mjs — entry-point guard', () => { + it('importing the module does not modify ui/public/docs (no auto-run on import)', () => { + // If the entry-point guard regressed, the very act of importing the + // module (at the top of this file) would call `runCopyDocs()` with + // the real `destDir = ui/public/docs`. We assert the import did NOT + // mutate that real directory by checking the current state matches + // the expected canonical set. The check is intentionally weak — + // we don't want this test to flake on unrelated public/docs changes; + // we only want to catch a "module-import-runs-the-script" regression. + const realDest = getDestDir() as string; + mkdirSync(realDest, { recursive: true }); + const files = readdirSync(realDest).filter((f) => f.endsWith('.md')); + // Every committed `.md` in ui/public/docs must be README.md or one of DOCS. + const expected = new Set(['README.md', ...(DOCS as readonly DocEntry[]).map((d) => d.dest)]); + for (const f of files) { + expect(expected.has(f)).toBe(true); + } + }); +}); From a6a58fa7de287669580922d7289acb631952872b Mon Sep 17 00:00:00 2001 From: SoundMindsAI Date: Tue, 2 Jun 2026 23:01:11 -0400 Subject: [PATCH 02/10] infra(copy-docs): add freshness gate + own workflow + self-test (Story 1.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Story 1.2 of infra_generated_artifact_freshness_gate (FR-1 + FR-3 + FR-8 Phase-1 + FR-6 docs half). Catches the failure mode where a contributor edits a source guide under docs/08_guides/ without re-running copy-docs.mjs, leaving ui/public/docs/ stale. - scripts/ci/verify_copy_docs_fresh.sh — regen via copy-docs.mjs, fail on git status --porcelain drift (--porcelain catches modified, untracked, AND deleted; bare git diff misses untracked, which is the FR-9 / AC-9 case). Prints the canonical fix command on failure. Honors COPY_DOCS_FRESH_REPO_ROOT override for the self-test's disposable git fixture. - scripts/ci/test_verify_copy_docs_fresh.sh — three cases against fresh mktemp git fixtures: clean (exit 0), source-drift (exit 1 with the canonical fix-command text), untracked AC-9 via `git rm --cached` (exit 1 with ?? marker). - .github/workflows/copy-docs-freshness.yml — runs on every PR to main with NO paths/paths-ignore filter (FR-3 escape from pr.yml's docs/** filter so docs-only PRs still get the check). Mirrors secrets-defense.yml's own-workflow precedent. Action SHAs pinned per chore_scorecard_pin_deps_postcss (PR #430). - docs/05_quality/testing.md — new "Generated-artifact freshness gates" subsection documenting the gate, why --porcelain (not --exit-code), and the canonical fix command. Verification: 7/7 self-test cases green; guard against the live repo emits "OK: ui/public/docs/ is fresh."; workflow YAML parses. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: SoundMindsAI --- .github/workflows/copy-docs-freshness.yml | 72 ++++++++++ docs/05_quality/testing.md | 31 +++++ scripts/ci/test_verify_copy_docs_fresh.sh | 159 ++++++++++++++++++++++ scripts/ci/verify_copy_docs_fresh.sh | 57 ++++++++ 4 files changed, 319 insertions(+) create mode 100644 .github/workflows/copy-docs-freshness.yml create mode 100755 scripts/ci/test_verify_copy_docs_fresh.sh create mode 100755 scripts/ci/verify_copy_docs_fresh.sh diff --git a/.github/workflows/copy-docs-freshness.yml b/.github/workflows/copy-docs-freshness.yml new file mode 100644 index 00000000..59e903fd --- /dev/null +++ b/.github/workflows/copy-docs-freshness.yml @@ -0,0 +1,72 @@ +# SPDX-FileCopyrightText: 2026 soundminds.ai +# +# SPDX-License-Identifier: Apache-2.0 + +name: copy-docs-freshness + +# infra_generated_artifact_freshness_gate / Story 1.2. +# +# Catches the failure mode where a contributor edits a source guide under +# `docs/08_guides/` without re-running `node ui/scripts/copy-docs.mjs`, +# leaving the in-app `` reader serving stale `ui/public/docs/` +# bytes. Regenerates the public copies and fails the PR if +# `git status --porcelain -- ui/public/docs/` is non-empty (modified, +# untracked, or deleted) — `git diff --exit-code` would silently miss the +# untracked case (FR-1, FR-9). +# +# Lives in its OWN workflow file (mirrors `secrets-defense.yml`) on +# `pull_request:` with NO `paths` / `paths-ignore` filter, so a docs-only +# PR that pr.yml's `paths-ignore: ['docs/**']` filter skips still gets the +# freshness check (FR-3 — own-workflow-to-escape-paths-ignore precedent). +# +# Action SHAs pinned per the `chore_scorecard_pin_deps_postcss` posture +# (PR #430, 2026-06-03); Dependabot's github-actions ecosystem rotates the +# `# v6` comment + the SHA together. + +on: + pull_request: + branches: [main] + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + copy-docs-freshness: + name: copy-docs freshness (ui/public/docs sync to docs/08_guides) + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + # Enough history for the diagnostic `git status` to resolve cleanly. + fetch-depth: 50 + + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6 + with: + version: 9 + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: ui/pnpm-lock.yaml + + # `copy-docs.mjs` itself imports only `node:fs`/`node:path`/`node:url`, + # so it does NOT need `pnpm install`. The frozen install still runs + # so the runner caches `ui/node_modules` for any future step on this + # job that needs ESLint/TS/etc.; remove later if the job stays + # node-only. + - name: pnpm install (frozen) + run: pnpm --dir ui install --frozen-lockfile + + - name: Self-test the freshness guard + run: bash scripts/ci/test_verify_copy_docs_fresh.sh + + - name: Verify ui/public/docs/ is in sync with docs/08_guides/ + run: bash scripts/ci/verify_copy_docs_fresh.sh diff --git a/docs/05_quality/testing.md b/docs/05_quality/testing.md index 0807bf1e..557da163 100644 --- a/docs/05_quality/testing.md +++ b/docs/05_quality/testing.md @@ -231,6 +231,37 @@ interpreter). Fault injection is via the `INFRA_OPTUNA_EVAL_FAULT` env var, which triggers `os._exit(1)` at one of two seams (`after_trial_load_before_execute`, `after_tell_before_insert`). +## Generated-artifact freshness gates + +Three CI gates catch the failure mode where a developer edits a source +file but forgets to regenerate the committed artifact built from it. Each +gate **regenerates** the artifact in CI and fails the PR if +`git status --porcelain` reports drift — a contributor never has to +remember to run a regen step locally before pushing; CI does it for them +and the gate's failure output prints the one-paste fix command. + +Why `git status --porcelain` (and not `git diff --exit-code`): `git diff` +silently ignores untracked files. A freshly-added `DOCS` entry whose +public copy was never committed would slip through. `--porcelain` reports +the modified, untracked, AND deleted cases (the `M` / `??` / ` D` +markers) — every drift mode the gate exists to catch. + +| # | Gate | Workflow | Source → Output | Regenerator | Self-test | +|---|---|---|---|---|---| +| 1 | `copy-docs-freshness` | own file (`copy-docs-freshness.yml`) — runs on every PR with no `paths-ignore` filter (FR-3 escape from `pr.yml`'s `docs/**` paths-ignore so docs-only PRs still get the check) | `docs/08_guides/*.md` → `ui/public/docs/*.md` | `node ui/scripts/copy-docs.mjs` (prunes the dest to `{README.md} ∪ {DOCS[].dest}` per FR-9, so a renamed entry never leaves a stale public copy behind) | `scripts/ci/test_verify_copy_docs_fresh.sh` exercises clean / source-drift / untracked-AC-9 cases against a disposable `mktemp` git fixture | + +The fix command printed on failure: + +```bash +cd ui && node scripts/copy-docs.mjs && git add public/docs +``` + +The freshness-gate scripts (`scripts/ci/verify_copy_docs_fresh.sh` + its +self-test) follow the canonical `scripts/ci/` shape: `set -euo pipefail`, +SPDX header, `git status --porcelain` (never bare `git diff`), and a +sibling `test_.sh` that drives the guard against disposable +fixtures. + ## Where to look - [`backend/tests/conftest.py`](../../backend/tests/conftest.py) — shared diff --git a/scripts/ci/test_verify_copy_docs_fresh.sh b/scripts/ci/test_verify_copy_docs_fresh.sh new file mode 100755 index 00000000..97217a0c --- /dev/null +++ b/scripts/ci/test_verify_copy_docs_fresh.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2026 soundminds.ai +# +# SPDX-License-Identifier: Apache-2.0 + +# Self-test for `scripts/ci/verify_copy_docs_fresh.sh` +# (Story 1.2 of `infra_generated_artifact_freshness_gate`). +# +# Builds a disposable git fixture in a tmp directory containing the real +# `ui/scripts/copy-docs.mjs` + the guard + a minimal source guide + the +# matching generated output (committed once at fixture-init so a clean run +# is a no-op), then exercises three cases: +# +# 1. Clean tree → guard exits 0 +# 2. Source-drift → edit a source guide, the guard's regen +# rewrites the public copy, `git status` +# reports `M`, guard exits 1, stdout/stderr +# contains the canonical fix command +# 3. Untracked AC-9 case → `git rm --cached` a public copy (the file +# stays on disk but leaves the index), guard +# reports `??`, exits 1 +# +# Each case runs in a fresh fixture so the failure of one case never +# contaminates the next. The fixture uses git init / commit on disposable +# state — no operation touches the operator's primary checkout. +# +# Run locally: bash scripts/ci/test_verify_copy_docs_fresh.sh +# Run in CI: invoked by the `copy-docs-freshness` workflow. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +GUARD="${REPO_ROOT}/scripts/ci/verify_copy_docs_fresh.sh" +COPY_DOCS="${REPO_ROOT}/ui/scripts/copy-docs.mjs" + +PASS=0 +FAIL=0 + +if [[ ! -x "${GUARD}" && ! -r "${GUARD}" ]]; then + echo "FATAL: cannot find guard at ${GUARD}" >&2 + exit 2 +fi + +# Build a self-contained, committed fixture in $1. The fixture seeds: +# - docs/08_guides/quick-tour.md (the only "DOCS" source the test exercises) +# - ui/scripts/copy-docs.mjs (real script, copied in) +# - ui/public/docs/* (committed after one no-op script run, +# so a fresh re-run is clean) +# The other two DOCS entries (`tutorial-first-study.md`, +# `workflows-overview.md`) are intentionally absent in `docs/08_guides/`, +# so the script logs a WARNING for each and skips them — both `copied` and +# the prune set agree, leaving the working tree clean. +build_fixture() { + local fixture="$1" + mkdir -p "${fixture}/ui/scripts" "${fixture}/ui/public/docs" + mkdir -p "${fixture}/docs/08_guides" + + cp "${COPY_DOCS}" "${fixture}/ui/scripts/copy-docs.mjs" + echo "# quick-tour fixture body" > "${fixture}/docs/08_guides/quick-tour.md" + + # Seed the public copy by running the script once. The script writes + # README.md + copies the one available guide; prune is a no-op because + # the dir starts empty. + ( cd "${fixture}/ui" && node scripts/copy-docs.mjs >/dev/null ) + + # Commit the seed so subsequent `git status` baselines on this state. + ( + cd "${fixture}" + git init -q -b main + git config user.email "selftest@local" + git config user.name "self-test" + git add . + git commit -q -m "init" + ) +} + +# Run the guard against $1 (a fixture path), capturing stdout+stderr to $2. +# Returns the guard's exit code via $? (caller checks). +run_guard() { + local fixture="$1" + local logfile="$2" + ( cd "${fixture}" && \ + COPY_DOCS_FRESH_REPO_ROOT="${fixture}" \ + bash "${GUARD}" ) >"${logfile}" 2>&1 +} + +assert_eq() { + local expected="$1" + local actual="$2" + local name="$3" + if [[ "${actual}" -eq "${expected}" ]]; then + echo " ok ${name}" + PASS=$((PASS + 1)) + else + echo " FAIL ${name} (expected exit ${expected}, got ${actual})" + FAIL=$((FAIL + 1)) + fi +} + +assert_contains() { + local needle="$1" + local file="$2" + local name="$3" + if grep -qF -- "${needle}" "${file}"; then + echo " ok ${name}" + PASS=$((PASS + 1)) + else + echo " FAIL ${name} (did not find '${needle}' in ${file})" + FAIL=$((FAIL + 1)) + fi +} + +# --- Case 1: clean tree → guard exits 0 ---------------------------------- +echo "Case 1: clean tree" +TMP1="$(mktemp -d -t rl-copy-docs-fresh-1.XXXXXX)" +trap 'rm -rf "${TMP1}" "${TMP2:-}" "${TMP3:-}"' EXIT +build_fixture "${TMP1}" +LOG1="${TMP1}.log" +actual=0 +run_guard "${TMP1}" "${LOG1}" || actual=$? +assert_eq 0 "${actual}" "clean tree → exit 0" +assert_contains "OK: ui/public/docs/ is fresh." "${LOG1}" "clean tree → success message" + +# --- Case 2: source-drift → guard exits 1 + fix-command text ------------- +echo "Case 2: source-drift (edit source guide, leave public copy unchanged)" +TMP2="$(mktemp -d -t rl-copy-docs-fresh-2.XXXXXX)" +build_fixture "${TMP2}" +echo "# quick-tour DRIFTED" > "${TMP2}/docs/08_guides/quick-tour.md" +LOG2="${TMP2}.log" +actual=0 +run_guard "${TMP2}" "${LOG2}" || actual=$? +assert_eq 1 "${actual}" "source-drift → exit 1" +assert_contains "ui/public/docs/ is stale." "${LOG2}" "source-drift → error header" +assert_contains "cd ui && node scripts/copy-docs.mjs && git add public/docs" "${LOG2}" \ + "source-drift → canonical fix-command text" + +# --- Case 3: untracked AC-9 → guard exits 1 with `??` marker ------------- +echo "Case 3: untracked public copy (git rm --cached leaves file on disk)" +TMP3="$(mktemp -d -t rl-copy-docs-fresh-3.XXXXXX)" +build_fixture "${TMP3}" +# `git rm --cached` removes the file from the index but keeps it on disk. +# After `copy-docs.mjs` re-runs, the file content matches the source so +# it isn't modified — it's just untracked (`??`). +( cd "${TMP3}" && git rm --cached -q ui/public/docs/quick-tour.md ) +LOG3="${TMP3}.log" +actual=0 +run_guard "${TMP3}" "${LOG3}" || actual=$? +assert_eq 1 "${actual}" "untracked AC-9 → exit 1" +# The diagnostic block emits `git status --porcelain` lines. An untracked +# file is reported as `?? `; assert the marker is present. +assert_contains "?? ui/public/docs/quick-tour.md" "${LOG3}" \ + "untracked AC-9 → git status reports ?? marker" + +echo +echo "${PASS} passed, ${FAIL} failed" +if [[ "${FAIL}" -gt 0 ]]; then + exit 1 +fi diff --git a/scripts/ci/verify_copy_docs_fresh.sh b/scripts/ci/verify_copy_docs_fresh.sh new file mode 100755 index 00000000..851967d1 --- /dev/null +++ b/scripts/ci/verify_copy_docs_fresh.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2026 soundminds.ai +# +# SPDX-License-Identifier: Apache-2.0 + +# infra_generated_artifact_freshness_gate / Story 1.2 — FR-1 + FR-3 + FR-8 (Phase-1 half). +# +# Regenerates `ui/public/docs/` from `docs/08_guides/` and fails if +# `git status --porcelain -- ui/public/docs/` is non-empty (modified, +# untracked, or deleted). Catches the failure mode where an operator +# edits a source guide without re-running `node ui/scripts/copy-docs.mjs`. +# +# Uses `git status --porcelain` (not `git diff --exit-code`) so untracked +# files (e.g., a freshly-added DOCS entry whose public copy was not staged) +# are flagged — `git diff` would silently miss those. +# +# Usage: +# bash scripts/ci/verify_copy_docs_fresh.sh # standard local/CI run +# COPY_DOCS_FRESH_REPO_ROOT=/path/to/wt bash …/verify_copy_docs_fresh.sh +# # explicit repo-root override used by the self-test harness; the +# # default discovers it via `git rev-parse --show-toplevel`. +# +# Exits 0 when the tree is fresh, 1 when it is stale. + +set -euo pipefail + +# Resolve repo root. The override env var lets the self-test point at a +# disposable fixture without polluting the operator's working tree. +if [[ -n "${COPY_DOCS_FRESH_REPO_ROOT:-}" ]]; then + REPO_ROOT="${COPY_DOCS_FRESH_REPO_ROOT}" +else + REPO_ROOT="$(git rev-parse --show-toplevel)" +fi +cd "${REPO_ROOT}" + +# Regenerate. `copy-docs.mjs` is idempotent on a fresh tree (copy + prune). +# Run with `cd ui &&` so node resolves the script relative to that root — +# matches the canonical local-fix-command shape printed below. +( cd ui && node scripts/copy-docs.mjs ) + +# `git status --porcelain` reports modified, deleted, AND untracked files +# under the path. `git diff --exit-code` only catches modified/deleted — +# untracked-file regressions are the AC-9 case. +DRIFT="$(git status --porcelain -- ui/public/docs/)" + +if [[ -n "${DRIFT}" ]]; then + echo "ERROR: ui/public/docs/ is stale." >&2 + echo "Fix with:" >&2 + echo " cd ui && node scripts/copy-docs.mjs && git add public/docs" >&2 + echo >&2 + echo "Drift detected (diagnostic):" >&2 + printf '%s\n' "${DRIFT}" >&2 + exit 1 +fi + +echo "OK: ui/public/docs/ is fresh." From f49531f541f1fcf39270e9eaad5178dd9f1e0bc3 Mon Sep 17 00:00:00 2001 From: SoundMindsAI Date: Tue, 2 Jun 2026 23:06:34 -0400 Subject: [PATCH 03/10] docs(testing): clarify Phase 1 freshness-gate scope (GPT-5.5 Epic 1 phase-gate finding #3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GPT-5.5 phase-gate review flagged that the freshness-gates subsection opened with "Three CI gates" while only documenting one — the Phase 2 snapshot + types gates land later. Soften the lede to "a family of CI gates" + add an explicit Phase 1 / Phase 2 sentence so a reader at this commit sees an accurate map of what ships when. Findings #1 (prune set derivation) and #2 (cwd-robustness coverage) were rejected with cited counter-evidence in the PR adjudication summary. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: SoundMindsAI --- docs/05_quality/testing.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/05_quality/testing.md b/docs/05_quality/testing.md index 557da163..e08462fb 100644 --- a/docs/05_quality/testing.md +++ b/docs/05_quality/testing.md @@ -233,13 +233,20 @@ var, which triggers `os._exit(1)` at one of two seams ## Generated-artifact freshness gates -Three CI gates catch the failure mode where a developer edits a source -file but forgets to regenerate the committed artifact built from it. Each -gate **regenerates** the artifact in CI and fails the PR if +A family of CI gates catches the failure mode where a developer edits a +source file but forgets to regenerate the committed artifact built from +it. Each gate **regenerates** the artifact in CI and fails the PR if `git status --porcelain` reports drift — a contributor never has to remember to run a regen step locally before pushing; CI does it for them and the gate's failure output prints the one-paste fix command. +Phase 1 (the `copy-docs` gate documented below) ships first; Phase 2 +adds two more gates for the OpenAPI snapshot (`ui/openapi.json`) and the +generated `ui/src/lib/types.ts`, plus a single chained fix command +spanning all three. Each row in the table is appended as its owning +story lands (per the cross-story testing.md ownership declared in +`infra_generated_artifact_freshness_gate/implementation_plan.md` §11). + Why `git status --porcelain` (and not `git diff --exit-code`): `git diff` silently ignores untracked files. A freshly-added `DOCS` entry whose public copy was never committed would slip through. `--porcelain` reports From bbdcc002d942d626fa9ff1c05b6b141e5cf6ab47 Mon Sep 17 00:00:00 2001 From: SoundMindsAI Date: Tue, 2 Jun 2026 23:10:53 -0400 Subject: [PATCH 04/10] infra(openapi): offline deterministic exporter (Story 2.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Story 2.1 of infra_generated_artifact_freshness_gate (FR-4 / AC-4). A CLI entrypoint that emits the canonical OpenAPI schema with no running server, live Postgres, Redis, ES/OpenSearch/Solr, or OpenAI client — the foundation for Story 2.2's `ui/openapi.json` snapshot freshness gate. - backend/app/openapi_export.py — argparse CLI with --out (atomic tmpfile + os.replace) or stdout. build_openapi() stubs the *_FILE-mounted Settings inputs via tempfile.mkdtemp + REDIS_URL bare env (non-secret, per Absolute Rule #2). serialize() applies the canonical form (sort_keys=True, compact separators, ensure_ascii=False, trailing newline) so output is byte-stable macOS↔Linux. All diagnostics → stderr; stdout is byte-pure JSON. - Module docstring records the FR-4 import-graph spike (path (a) resolution): app.openapi() walks routes + Pydantic models and does NOT trigger FastAPI's lifespan — no asyncpg pool / Redis client / engine adapter is constructed at schema-build time. The companion unit test runs with a deliberately non-resolvable REDIS_URL host and asserts build_openapi() still succeeds, converting any future regression (a router opening a connection at import) into an immediate unit-test failure. - backend/tests/unit/test_openapi_export.py — 10 cases: parsed-key assertions (NOT a leading-byte prefix, per plan task 2.1.4 note), byte-stability across repeated calls, canonical-form invariants, no-service-containers smoke, stdout-vs-stderr discipline, atomic write verification (no .tmp leak), overwrite path, idempotency, and the `python -m`-style invocation smoke. Operator-path verification: `python -m backend.app.openapi_export` emits 52 paths and parses cleanly. Lint + mypy --strict clean. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: SoundMindsAI --- backend/app/openapi_export.py | 218 ++++++++++++++++++++++ backend/tests/unit/test_openapi_export.py | 195 +++++++++++++++++++ 2 files changed, 413 insertions(+) create mode 100644 backend/app/openapi_export.py create mode 100644 backend/tests/unit/test_openapi_export.py diff --git a/backend/app/openapi_export.py b/backend/app/openapi_export.py new file mode 100644 index 00000000..92f30dfa --- /dev/null +++ b/backend/app/openapi_export.py @@ -0,0 +1,218 @@ +# SPDX-FileCopyrightText: 2026 soundminds.ai +# +# SPDX-License-Identifier: Apache-2.0 + +r"""Offline, deterministic OpenAPI schema exporter. + +Emits the canonical OpenAPI document that ``backend.app.main.app.openapi()`` +produces, with **no** running server, live Postgres, Redis, Elasticsearch, +OpenSearch, Solr, or OpenAI client. CI uses this exporter to (re)write the +committed ``ui/openapi.json`` snapshot and fail the PR on +``git status --porcelain`` drift — Story 2.1 / FR-4 of +``infra_generated_artifact_freshness_gate``. + +Usage:: + + python -m backend.app.openapi_export # → stdout + python -m backend.app.openapi_export --out ui/openapi.json # → atomic file write + +All diagnostics go to stderr so stdout is byte-pure JSON. + +Import-graph spike (FR-4 / spec §19 — open question resolved here): + +The proven recipe is to set the secret-mounted ``*_FILE`` env vars to +dummy tmpdir files (``DATABASE_URL_FILE`` + ``POSTGRES_PASSWORD_FILE``) ++ the non-secret ``REDIS_URL`` to a localhost stub, then ``from +backend.app.main import app`` + ``app.openapi()``. This is **path (a)** +in the spec's two-option fork: + +* ``DATABASE_URL_FILE`` / ``POSTGRES_PASSWORD_FILE`` — secret-bearing, + follow Absolute Rule #2 (``*_FILE``-mounted-only). Dummy files contain + non-secret placeholder bytes; committing nothing exposes nothing. +* ``REDIS_URL`` — non-secret config (no credentials), allowed as a + bare env var per the Settings rules. + +``app.openapi()`` walks the registered route table + Pydantic models to +synthesize the schema. It does NOT trigger ``lifespan`` (FastAPI runs +``lifespan`` only on app boot), so no asyncpg pool, Redis client, ES / +OpenSearch / Solr client, or OpenAI client is constructed at import time +or at schema-build time. The companion unit test ``test_openapi_export`` +runs the exporter with no service containers reachable and asserts it +exits 0 — turning any future regression (a router that opens a +connection at import) into an immediate unit-test failure rather than +a silent CI hang. + +Canonical serialization (FR-4 + FR-6 determinism): + +``json.dumps(schema, sort_keys=True, separators=(",", ":"), +ensure_ascii=False) + "\\n"``. ``sort_keys=True`` alphabetises top-level +keys (so the document does NOT begin with ``"openapi":`` — tests assert +parsed keys, not a leading byte prefix). Compact separators + trailing +newline keep diffs minimal. Atomic write via tmpfile + ``os.replace`` +prevents a torn snapshot if the process is killed mid-write. + +Reuses the dummy-``*_FILE`` env setup pioneered by +``backend/tests/contract/test_data_table_query_params.py``. +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import tempfile +from pathlib import Path +from typing import Any + + +def _ensure_dummy_settings_env() -> None: + """Populate the minimum env vars Settings requires to import. + + Only sets a var when it is not already present in the environment — + callers (CI, the operator's shell) can override any of these with + the real values if they're available. The dummy files live in a + ``tempfile.mkdtemp()`` directory whose path is also published in + ``RELYLOOP_OPENAPI_EXPORT_TMP`` so a parent test process can inspect + them if needed (purely diagnostic; never required). + + The function is intentionally idempotent so an interactive Python + session that has already imported the module can re-call without + pointing at fresh tmpdirs. + """ + # Already populated → no-op. Avoid the temp-dir churn on hot paths + # (the unit test calls build_openapi() repeatedly). + if all( + os.environ.get(var) for var in ("DATABASE_URL_FILE", "POSTGRES_PASSWORD_FILE", "REDIS_URL") + ): + return + + tmp_dir = Path(tempfile.mkdtemp(prefix="relyloop-openapi-export-")) + os.environ.setdefault("RELYLOOP_OPENAPI_EXPORT_TMP", str(tmp_dir)) + + if not os.environ.get("DATABASE_URL_FILE"): + db_url_file = tmp_dir / "db_url" + # Driver prefix is `+asyncpg` because the runtime uses the async + # SQLAlchemy dialect; mismatched dialects are the canonical + # `bug_postgres_dialect_drift` shape this avoids. + db_url_file.write_text("postgresql+asyncpg://relyloop:placeholder@localhost/relyloop") + os.environ["DATABASE_URL_FILE"] = str(db_url_file) + + if not os.environ.get("POSTGRES_PASSWORD_FILE"): + pw_file = tmp_dir / "pw" + pw_file.write_text("placeholder") + os.environ["POSTGRES_PASSWORD_FILE"] = str(pw_file) + + # REDIS_URL is non-secret config per the Settings rules, so the bare + # env var is the supported form (Absolute Rule #2 governs SECRETS + # only — see CLAUDE.md "Settings & Secrets"). + os.environ.setdefault("REDIS_URL", "redis://localhost:6379/0") + + +def build_openapi() -> dict[str, Any]: + """Construct the FastAPI app and return its OpenAPI schema dict. + + Import-clean: no asyncpg pool, Redis client, or engine adapter is + instantiated at import time or by ``app.openapi()`` — those build + during ``lifespan``, which FastAPI does not invoke when the schema + is requested directly. See module docstring for the import-graph + spike rationale. + """ + _ensure_dummy_settings_env() + + # The settings cache must be cleared so the dummy env vars take + # effect even when this function is called from a process that + # already initialised Settings under different env (e.g. an + # interactive REPL or a test that imported `backend.app` earlier). + from backend.app.core.settings import get_settings + + get_settings.cache_clear() + + from backend.app.main import app + + return app.openapi() + + +def serialize(schema: dict[str, Any]) -> str: + """Canonical JSON encoding — see module docstring (FR-4 / FR-6). + + ``sort_keys=True`` makes the byte-output deterministic across + macOS/Linux. ``ensure_ascii=False`` lets non-ASCII bytes flow + through unescaped (the schema doesn't currently contain any, but + if a router description ever adds one we don't want the platform's + JSON-escape behaviour to vary). Compact ``separators`` + a single + trailing newline keep the diff minimal. + """ + return ( + json.dumps( + schema, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + ) + + "\n" + ) + + +def _write_atomic(path: Path, content: str) -> None: + """Write ``content`` to ``path`` atomically (tmp file + os.replace). + + Avoids a torn snapshot if the process is killed mid-write — readers + either see the previous content or the new content, never a partial + file. The tmp file is created in the same directory as ``path`` so + ``os.replace`` is a same-filesystem rename (atomic on POSIX/NTFS). + """ + path.parent.mkdir(parents=True, exist_ok=True) + # ``delete=False`` because os.replace handles the rename + cleanup. + with tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + dir=str(path.parent), + prefix=f".{path.name}.", + suffix=".tmp", + delete=False, + ) as tmp: + tmp.write(content) + tmp.flush() + os.fsync(tmp.fileno()) + tmp_path = Path(tmp.name) + os.replace(tmp_path, path) + + +def main(argv: list[str] | None = None) -> int: + """CLI entrypoint. Returns the process exit code.""" + parser = argparse.ArgumentParser( + prog="backend.app.openapi_export", + description=( + "Emit the canonical OpenAPI schema for the RelyLoop API. " + "With --out, atomically writes to that path; without --out, " + "writes byte-pure JSON to stdout. All diagnostics → stderr." + ), + ) + parser.add_argument( + "--out", + type=Path, + default=None, + help="Destination path (atomic write). Omit for stdout.", + ) + args = parser.parse_args(argv) + + try: + schema = build_openapi() + except Exception as exc: # noqa: BLE001 — top-level CLI guard + print(f"openapi-export: failed to build schema: {exc}", file=sys.stderr) + return 1 + + body = serialize(schema) + + if args.out is None: + sys.stdout.write(body) + return 0 + + _write_atomic(args.out, body) + print(f"openapi-export: wrote {args.out} ({len(body)} bytes)", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/backend/tests/unit/test_openapi_export.py b/backend/tests/unit/test_openapi_export.py new file mode 100644 index 00000000..6d121973 --- /dev/null +++ b/backend/tests/unit/test_openapi_export.py @@ -0,0 +1,195 @@ +# SPDX-FileCopyrightText: 2026 soundminds.ai +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for ``backend.app.openapi_export``. + +Story 2.1 of ``infra_generated_artifact_freshness_gate`` (FR-4 / AC-4). + +The exporter MUST work with **no** live Postgres / Redis / Elasticsearch / +OpenSearch / Solr / OpenAI client. These tests assert that explicitly — +none of them touches the network or `make up` infra. The fixture +intentionally does NOT receive a real ``client``/``db_session`` and +runs in the default ``backend.tests.unit/`` layer (CI's unit job has no +service containers). + +Test surface: + +1. ``build_openapi()`` returns a dict with the structural keys an + OpenAPI 3 document must carry (``openapi``, ``info``, ``paths``). + The plan (§ Tasks 2.1.4) explicitly notes the assertion must check + *parsed keys*, not a leading-byte prefix, because the canonical + ``json.dumps(sort_keys=True)`` alphabetises top-level keys (so the + first key is ``components``, not ``openapi``). + +2. ``serialize(build_openapi())`` is byte-stable across repeated calls + (FR-6 determinism / AC-7 backend half). + +3. The exporter runs with **no service env** beyond what + ``_ensure_dummy_settings_env()`` populates — this is the executable + enforcement of FR-4's import-graph claim. + +4. ``main(['--out', path])`` writes byte-pure JSON to ``path`` + atomically (no partial file on the destination during the write) + and exits 0. + +5. ``main([])`` writes byte-pure JSON to stdout and exits 0. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from backend.app import openapi_export + + +def test_build_openapi_returns_canonical_openapi_dict() -> None: + """Parsed keys, not a leading-byte prefix (sort_keys=True moves + ``components`` to the front).""" + schema = openapi_export.build_openapi() + + assert isinstance(schema, dict) + # The four keys every OpenAPI 3 doc the FastAPI helper produces + # carries. We don't assert order — `sort_keys=True` in serialize() + # alphabetises, so order is structural ("components" first). + for required in ("openapi", "info", "paths"): + assert required in schema, f"missing OpenAPI key: {required!r}" + + # The version field must look like "3.x" — both FastAPI 0.x and 1.x + # emit "3.0.x" / "3.1.x" depending on version; both are acceptable. + assert schema["openapi"].startswith("3."), schema["openapi"] + + # And there must be at least one known route (sanity check the app + # was actually imported, not a stub). `/healthz` is unprefixed + + # mandatory per CLAUDE.md Rule #6, so it's the most stable target. + assert "/healthz" in schema["paths"], "healthz route not in schema" + + +def test_serialize_is_byte_stable_across_repeated_calls() -> None: + """FR-6: a clean re-run on the same input produces identical bytes.""" + first = openapi_export.serialize(openapi_export.build_openapi()) + second = openapi_export.serialize(openapi_export.build_openapi()) + assert first == second, "serialize() output drifted across calls" + + +def test_serialize_uses_canonical_form() -> None: + """sort_keys + compact separators + trailing newline (FR-4).""" + raw = openapi_export.serialize({"b": 1, "a": 2}) + # sort_keys=True → "a" first. + assert raw == '{"a":2,"b":1}\n' + + # And the canonical form parses back to the original. + parsed = json.loads(raw) + assert parsed == {"a": 2, "b": 1} + + +def test_serialize_handles_real_schema_round_trip() -> None: + """The real schema's JSON output parses cleanly back to a dict.""" + schema = openapi_export.build_openapi() + body = openapi_export.serialize(schema) + assert body.endswith("\n"), "missing trailing newline" + reparsed = json.loads(body) + assert reparsed.keys() == schema.keys() + + +def test_exporter_runs_with_no_service_containers( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Smoke: FR-4 path (a) holds — no asyncpg pool / Redis / engine + client is constructed at import or at app.openapi() time. We prove + it by pointing the env at obviously-unreachable hosts and asserting + build_openapi() still returns a valid schema.""" + # Point at a deliberately non-resolvable host. If any of the engine + # / DB / Redis / OpenAI clients were instantiated at import time or + # by app.openapi(), the call would either hang on DNS or raise + # ConnectionError. We expect it to succeed. + monkeypatch.setenv("REDIS_URL", "redis://relyloop-no-such-host.invalid:6379/0") + # DATABASE_URL_FILE + POSTGRES_PASSWORD_FILE are populated by + # _ensure_dummy_settings_env() so they already work — we don't + # override them here. The Settings cache must be cleared to pick + # up the new REDIS_URL. + from backend.app.core.settings import get_settings + + get_settings.cache_clear() + + schema = openapi_export.build_openapi() + assert "/healthz" in schema["paths"] + + +def test_main_writes_to_stdout_when_out_omitted( + capsys: pytest.CaptureFixture[str], +) -> None: + """Stdout receives byte-pure JSON; all diagnostics → stderr.""" + rc = openapi_export.main([]) + captured = capsys.readouterr() + assert rc == 0 + # Stdout must parse as JSON (no diagnostic noise mixed in). + parsed = json.loads(captured.out) + assert "openapi" in parsed + # Stderr is allowed to carry diagnostics but the diagnostic-free + # path is preferred — main([]) shouldn't print anything to stderr + # when --out is omitted (the only stderr message is the "wrote N + # bytes" line guarded by --out). + assert captured.err == "" + + +def test_main_writes_atomic_file_when_out_provided( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """--out path is atomic-written; the diagnostic goes to stderr.""" + out = tmp_path / "openapi.json" + rc = openapi_export.main(["--out", str(out)]) + captured = capsys.readouterr() + assert rc == 0 + assert out.exists() + body = out.read_text() + assert body.endswith("\n") + parsed = json.loads(body) + assert "/healthz" in parsed["paths"] + # No stray .tmp file should survive the atomic write. + leftover_tmps = [p for p in tmp_path.iterdir() if p.suffix == ".tmp"] + assert leftover_tmps == [], f"atomic-write leaked: {leftover_tmps}" + # Diagnostic about file write went to stderr (not stdout). + assert captured.out == "" + assert "wrote" in captured.err.lower() + + +def test_main_overwrites_existing_out_path(tmp_path: Path) -> None: + """A pre-existing file is replaced — the gate's re-write path.""" + out = tmp_path / "openapi.json" + out.write_text("stale-bytes\n") + rc = openapi_export.main(["--out", str(out)]) + assert rc == 0 + body = out.read_text() + assert body != "stale-bytes\n" + # Round-trip parses cleanly. + parsed = json.loads(body) + assert parsed.get("openapi", "").startswith("3.") + + +def test_module_invocation_is_clean( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """Belt-and-braces: simulate the CLI by calling main() and capturing + output the way `python -m backend.app.openapi_export` would. Same + coverage as test_main_writes_to_stdout, but explicitly framed as the + `python -m` smoke.""" + rc = openapi_export.main([]) + captured = capsys.readouterr() + assert rc == 0 + assert json.loads(captured.out)["openapi"].startswith("3.") + + +def test_build_openapi_is_idempotent_in_a_single_process() -> None: + """Calling build_openapi() twice in the same process returns + structurally equivalent schemas. (Identity is not required — FastAPI + can rebuild internally — but `sorted(keys)` must match.)""" + a = openapi_export.build_openapi() + b = openapi_export.build_openapi() + assert sorted(a.keys()) == sorted(b.keys()) + assert sorted(a.get("paths", {}).keys()) == sorted(b.get("paths", {}).keys()) From 1a0151e8ec34cc45899d869a9d89393106ef26e2 Mon Sep 17 00:00:00 2001 From: SoundMindsAI Date: Tue, 2 Jun 2026 23:12:52 -0400 Subject: [PATCH 05/10] infra(openapi): commit canonical ui/openapi.json snapshot (Story 2.2 a) Story 2.2 task 1 of infra_generated_artifact_freshness_gate (FR-7). Generated by `python -m backend.app.openapi_export --out ui/openapi.json` using Story 2.1's exporter. 52 paths, canonical form (sort_keys=True, compact separators, ensure_ascii=False, trailing newline). REUSE-lint coverage: ui/openapi.json is automatically covered by the existing **/*.json glob at REUSE.toml:23, so no annotation needed (Risk R-3 already mitigated). Subsequent commit on this branch adds the snapshot-freshness guard + self-test + the generated-artifacts-fresh pr.yml job. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: SoundMindsAI --- ui/openapi.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 ui/openapi.json diff --git a/ui/openapi.json b/ui/openapi.json new file mode 100644 index 00000000..54c8d9c5 --- /dev/null +++ b/ui/openapi.json @@ -0,0 +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"]}}}} From 1e4c914ef0bb394d1d92b8a628bd7852804c49cf Mon Sep 17 00:00:00 2001 From: SoundMindsAI Date: Tue, 2 Jun 2026 23:16:54 -0400 Subject: [PATCH 06/10] infra(openapi): snapshot freshness gate + self-test + pr.yml job (Story 2.2 b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Story 2.2 task 2-4 of infra_generated_artifact_freshness_gate (FR-7 + FR-6 + FR-8 Phase-2 half). - scripts/ci/verify_openapi_snapshot_fresh.sh — regen via the offline exporter (Story 2.1), fail on `git status --porcelain` drift. Uses --porcelain (not --exit-code) so the untracked case (a first commit forgetting to git add the snapshot) is flagged. Supports an OPENAPI_SNAPSHOT_REGEN_SCRIPT path-override for the self-test fixture (script path, not shell command — avoids read -ra word- splitting and shell-quoting traps). - scripts/ci/test_verify_openapi_snapshot_fresh.sh — three cases against fresh mktemp git fixtures: clean (same bytes → exit 0), source-drift (different bytes → exit 1 with canonical fix-command text), untracked AC-9 (`git rm --cached` → ?? marker → exit 1). The override means the fixture doesn't need uv + the project venv — the exporter has its own Story-2.1 unit test; this self-test verifies the guard's diff-detection logic only. - .github/workflows/pr.yml — new `generated-artifacts-fresh` job mirroring license-inventory's structure (uv + Python + pnpm + node). Snapshot guard runs here; Story 2.3 appends the types-guard step to the same job. Not under paths-ignore — both backend and UI changes can invalidate the snapshot. - docs/05_quality/testing.md — appends gate #2 row to the freshness- gates table per the cross-story testing.md ownership declared in implementation_plan.md §11; documents both fix commands. Verification: 7/7 self-test cases green; live-repo guard re-runs the exporter and emits "OK: ui/openapi.json is fresh."; `uv run python -m backend.app.openapi_export` produces byte-identical output to the committed snapshot (determinism confirmed). Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: SoundMindsAI --- .github/workflows/pr.yml | 50 +++++ docs/05_quality/testing.md | 7 +- .../ci/test_verify_openapi_snapshot_fresh.sh | 171 ++++++++++++++++++ scripts/ci/verify_openapi_snapshot_fresh.sh | 72 ++++++++ 4 files changed, 299 insertions(+), 1 deletion(-) create mode 100755 scripts/ci/test_verify_openapi_snapshot_fresh.sh create mode 100755 scripts/ci/verify_openapi_snapshot_fresh.sh diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 33ef6c07..81aaf88d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -208,6 +208,56 @@ jobs: - name: Check license inventory run: uv run python scripts/gen_license_inventory.py --check + # ------------------------------------------------------------------------- + # Generated-artifact freshness — Phase 2 (`infra_generated_artifact_ + # freshness_gate`). Runs the snapshot guard (Story 2.2) here; Story 2.3 + # appends the types-guard step to this same job. The copy-docs gate + # (Story 1.2) lives in its OWN workflow file `copy-docs-freshness.yml` + # so it survives pr.yml's `docs/**` paths-ignore filter (FR-3 escape). + # + # This job is NOT under paths-ignore — backend (**/*.py) and ui (**/*.ts) + # changes can both invalidate the snapshot, so the gate must run on every + # code-bearing PR. Job structure mirrors `license-inventory` above — uv + # for the Python exporter, plus pnpm/node so the future types-guard + # step in Story 2.3 has the pinned `openapi-typescript` binary in + # `ui/node_modules`. + # ------------------------------------------------------------------------- + generated-artifacts-fresh: + name: generated-artifacts-fresh + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - name: Install uv + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + with: + enable-cache: true + version: "0.5.7" + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + with: + python-version: "3.13" + - name: Install Python deps (frozen) + run: uv sync --frozen + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6 + with: + version: 9 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: 22 + cache: "pnpm" + cache-dependency-path: ui/pnpm-lock.yaml + - name: Install pnpm deps (frozen) + # Needed so the future Story 2.3 types-guard step finds the pinned + # `openapi-typescript` binary in `ui/node_modules`. The exporter + # itself only needs Python, but installing pnpm here keeps the + # whole `generated-artifacts-fresh` job self-contained. + run: pnpm --dir ui install --frozen-lockfile + - name: Self-test snapshot guard + run: bash scripts/ci/test_verify_openapi_snapshot_fresh.sh + - name: Verify ui/openapi.json snapshot is fresh + run: bash scripts/ci/verify_openapi_snapshot_fresh.sh + # ------------------------------------------------------------------------- # Static checks — ALWAYS run (not gated by SKIP_HEAVY_CI). Split into two # parallel jobs by toolchain: the independent Python and Node dependency diff --git a/docs/05_quality/testing.md b/docs/05_quality/testing.md index e08462fb..b96c2569 100644 --- a/docs/05_quality/testing.md +++ b/docs/05_quality/testing.md @@ -256,11 +256,16 @@ markers) — every drift mode the gate exists to catch. | # | Gate | Workflow | Source → Output | Regenerator | Self-test | |---|---|---|---|---|---| | 1 | `copy-docs-freshness` | own file (`copy-docs-freshness.yml`) — runs on every PR with no `paths-ignore` filter (FR-3 escape from `pr.yml`'s `docs/**` paths-ignore so docs-only PRs still get the check) | `docs/08_guides/*.md` → `ui/public/docs/*.md` | `node ui/scripts/copy-docs.mjs` (prunes the dest to `{README.md} ∪ {DOCS[].dest}` per FR-9, so a renamed entry never leaves a stale public copy behind) | `scripts/ci/test_verify_copy_docs_fresh.sh` exercises clean / source-drift / untracked-AC-9 cases against a disposable `mktemp` git fixture | +| 2 | `generated-artifacts-fresh` (snapshot step) | `pr.yml` job — backend (`**/*.py`) + ui (`**/*.ts`) changes can both invalidate the snapshot, so the gate runs on every code-bearing PR | backend FastAPI route table → `ui/openapi.json` | `uv run python -m backend.app.openapi_export --out ui/openapi.json` (offline, no live services per Story 2.1) | `scripts/ci/test_verify_openapi_snapshot_fresh.sh` uses an `OPENAPI_SNAPSHOT_REGEN_SCRIPT` path-override + a disposable `mktemp` fixture to test the guard's diff-detection without needing `uv` in the fixture (the exporter has its own Story-2.1 unit test) | -The fix command printed on failure: +The fix commands printed on failure: ```bash +# Gate 1 (copy-docs) cd ui && node scripts/copy-docs.mjs && git add public/docs + +# Gate 2 (openapi.json snapshot) +uv run python -m backend.app.openapi_export --out ui/openapi.json && git add ui/openapi.json ``` The freshness-gate scripts (`scripts/ci/verify_copy_docs_fresh.sh` + its diff --git a/scripts/ci/test_verify_openapi_snapshot_fresh.sh b/scripts/ci/test_verify_openapi_snapshot_fresh.sh new file mode 100755 index 00000000..83113ec6 --- /dev/null +++ b/scripts/ci/test_verify_openapi_snapshot_fresh.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2026 soundminds.ai +# +# SPDX-License-Identifier: Apache-2.0 + +# Self-test for `scripts/ci/verify_openapi_snapshot_fresh.sh` +# (Story 2.2 of `infra_generated_artifact_freshness_gate`). +# +# Builds a disposable git fixture in a tmp directory containing a +# pre-committed `ui/openapi.json` (test bytes, NOT the real schema) +# and exercises three cases via the guard's +# `OPENAPI_SNAPSHOT_REGEN_CMD` override: +# +# 1. Clean tree → override re-writes the same bytes, tree +# stays clean, guard exits 0 +# 2. Source-drift → override writes DIFFERENT bytes, tree +# goes dirty, guard exits 1 with the +# canonical fix-command text +# 3. Untracked AC-9 case → `git rm --cached` the snapshot (file +# stays on disk but leaves the index), +# override writes the same bytes, +# guard reports `??` and exits 1 +# +# The override is a script PATH (not a shell command string) so we +# don't have to navigate `read -ra` word-splitting on a regen command +# that itself uses quoted args. Each test seeds a tiny fixture-local +# `regen.sh` and points `OPENAPI_SNAPSHOT_REGEN_SCRIPT` at it. +# +# Using a script-path override avoids needing `uv` + the project venv +# in the fixture — the exporter has its own Story-2.1 unit test; this +# self-test verifies the guard's diff-detection logic, not the exporter. +# +# Run locally: bash scripts/ci/test_verify_openapi_snapshot_fresh.sh +# Run in CI: invoked by the `generated-artifacts-fresh` job. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +GUARD="${REPO_ROOT}/scripts/ci/verify_openapi_snapshot_fresh.sh" + +PASS=0 +FAIL=0 + +if [[ ! -r "${GUARD}" ]]; then + echo "FATAL: cannot find guard at ${GUARD}" >&2 + exit 2 +fi + +# Deterministic test bytes — NOT the real schema. The guard's job is +# diff-detection; it doesn't care what the bytes mean. Keeping these +# small + obviously-fake makes the test self-explanatory. +CANONICAL_BYTES='{"openapi":"3.1.0","paths":{}}' +DRIFTED_BYTES='{"openapi":"3.1.0","paths":{"/drifted":{}}}' + +build_fixture() { + local fixture="$1" + mkdir -p "${fixture}/ui" + printf '%s\n' "${CANONICAL_BYTES}" > "${fixture}/ui/openapi.json" + ( + cd "${fixture}" + git init -q -b main + git config user.email "selftest@local" + git config user.name "self-test" + git add ui/openapi.json + git commit -q -m "init" + ) +} + +# Write a tiny regen-stub script into $1/regen.sh that writes $2 (raw +# bytes) to ui/openapi.json on each invocation. Returns the script path. +write_regen_script() { + local fixture="$1" + local bytes="$2" + local script="${fixture}/regen.sh" + cat > "${script}" < ui/openapi.json +EOF + chmod +x "${script}" + printf '%s\n' "${script}" +} + +# Run the guard against $1 (a fixture) with regen-script path $2, +# capturing stdout+stderr to $3. +run_guard() { + local fixture="$1" + local regen_script="$2" + local logfile="$3" + ( + cd "${fixture}" + OPENAPI_SNAPSHOT_FRESH_REPO_ROOT="${fixture}" \ + OPENAPI_SNAPSHOT_REGEN_SCRIPT="${regen_script}" \ + bash "${GUARD}" + ) >"${logfile}" 2>&1 +} + +assert_eq() { + local expected="$1" + local actual="$2" + local name="$3" + if [[ "${actual}" -eq "${expected}" ]]; then + echo " ok ${name}" + PASS=$((PASS + 1)) + else + echo " FAIL ${name} (expected exit ${expected}, got ${actual})" + FAIL=$((FAIL + 1)) + fi +} + +assert_contains() { + local needle="$1" + local file="$2" + local name="$3" + if grep -qF -- "${needle}" "${file}"; then + echo " ok ${name}" + PASS=$((PASS + 1)) + else + echo " FAIL ${name} (did not find '${needle}' in ${file})" + FAIL=$((FAIL + 1)) + fi +} + +# Each test gets its own fixture so failures don't contaminate later cases. +trap 'rm -rf "${TMP1:-}" "${TMP2:-}" "${TMP3:-}"' EXIT + +# --- Case 1: clean tree → guard exits 0 ---------------------------------- +echo "Case 1: clean tree" +TMP1="$(mktemp -d -t rl-openapi-snapshot-fresh-1.XXXXXX)" +build_fixture "${TMP1}" +LOG1="${TMP1}.log" +# Regen writes the SAME bytes that are already committed → no drift. +CLEAN_REGEN_SCRIPT="$(write_regen_script "${TMP1}" "${CANONICAL_BYTES}")" +actual=0 +run_guard "${TMP1}" "${CLEAN_REGEN_SCRIPT}" "${LOG1}" || actual=$? +assert_eq 0 "${actual}" "clean tree → exit 0" +assert_contains "OK: ui/openapi.json is fresh." "${LOG1}" "clean tree → success message" + +# --- Case 2: source-drift → guard exits 1 + fix-command text ------------- +echo "Case 2: source-drift (regen produces different bytes)" +TMP2="$(mktemp -d -t rl-openapi-snapshot-fresh-2.XXXXXX)" +build_fixture "${TMP2}" +LOG2="${TMP2}.log" +DRIFT_REGEN_SCRIPT="$(write_regen_script "${TMP2}" "${DRIFTED_BYTES}")" +actual=0 +run_guard "${TMP2}" "${DRIFT_REGEN_SCRIPT}" "${LOG2}" || actual=$? +assert_eq 1 "${actual}" "source-drift → exit 1" +assert_contains "ui/openapi.json is stale." "${LOG2}" "source-drift → error header" +assert_contains "uv run python -m backend.app.openapi_export --out ui/openapi.json && git add ui/openapi.json" \ + "${LOG2}" "source-drift → canonical fix-command text" + +# --- Case 3: untracked AC-9 → guard exits 1 with `??` marker ------------- +echo "Case 3: untracked snapshot (git rm --cached leaves file on disk)" +TMP3="$(mktemp -d -t rl-openapi-snapshot-fresh-3.XXXXXX)" +build_fixture "${TMP3}" +( cd "${TMP3}" && git rm --cached -q ui/openapi.json ) +LOG3="${TMP3}.log" +actual=0 +# Regen writes the same bytes back; the file just isn't tracked anymore. +UNTRACKED_REGEN_SCRIPT="$(write_regen_script "${TMP3}" "${CANONICAL_BYTES}")" +run_guard "${TMP3}" "${UNTRACKED_REGEN_SCRIPT}" "${LOG3}" || actual=$? +assert_eq 1 "${actual}" "untracked AC-9 → exit 1" +assert_contains "?? ui/openapi.json" "${LOG3}" \ + "untracked AC-9 → git status reports ?? marker" + +echo +echo "${PASS} passed, ${FAIL} failed" +if [[ "${FAIL}" -gt 0 ]]; then + exit 1 +fi diff --git a/scripts/ci/verify_openapi_snapshot_fresh.sh b/scripts/ci/verify_openapi_snapshot_fresh.sh new file mode 100755 index 00000000..c8481722 --- /dev/null +++ b/scripts/ci/verify_openapi_snapshot_fresh.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2026 soundminds.ai +# +# SPDX-License-Identifier: Apache-2.0 + +# infra_generated_artifact_freshness_gate / Story 2.2 — FR-7 + FR-6. +# +# Regenerates `ui/openapi.json` via the offline exporter (Story 2.1) and +# fails if `git status --porcelain -- ui/openapi.json` is non-empty +# (modified, untracked, or deleted). Catches the failure mode where a +# backend schema change ships without re-running the exporter, leaving +# the committed snapshot stale. +# +# Uses `git status --porcelain` (not `git diff --exit-code`) so the +# untracked-file regression (the FR-9 / AC-9 case — a first commit that +# forgot to `git add` the snapshot) is flagged. +# +# Usage: +# bash scripts/ci/verify_openapi_snapshot_fresh.sh +# +# Override env vars (intended for the self-test harness, NOT production): +# +# OPENAPI_SNAPSHOT_FRESH_REPO_ROOT +# Override `git rev-parse --show-toplevel` so the guard operates +# on a disposable fixture instead of the live repo. +# +# OPENAPI_SNAPSHOT_REGEN_SCRIPT +# Path to a bash script that performs the regen step. Defaults to +# running `uv run python -m backend.app.openapi_export --out +# ui/openapi.json` directly. The self-test points this at a small +# fixture-local stub so it doesn't need uv + the project venv in +# the fixture (the exporter has its own Story-2.1 unit test; the +# guard's job is diff-detection). Path form (not a command string) +# avoids `read -ra` word-splitting / quoting traps. +# +# Exits 0 when the snapshot is fresh, 1 when it is stale. + +set -euo pipefail + +if [[ -n "${OPENAPI_SNAPSHOT_FRESH_REPO_ROOT:-}" ]]; then + REPO_ROOT="${OPENAPI_SNAPSHOT_FRESH_REPO_ROOT}" +else + REPO_ROOT="$(git rev-parse --show-toplevel)" +fi +cd "${REPO_ROOT}" + +# Resolve regen invocation. Override is a SCRIPT PATH (not a shell +# command string) so we don't have to navigate `read -ra` word-splitting +# or shell-quoting traps for an env var with embedded quotes / spaces. +# The default array form keeps the production path argv-clean. +if [[ -n "${OPENAPI_SNAPSHOT_REGEN_SCRIPT:-}" ]]; then + REGEN_CMD=(bash "${OPENAPI_SNAPSHOT_REGEN_SCRIPT}") +else + REGEN_CMD=(uv run python -m backend.app.openapi_export --out ui/openapi.json) +fi + +"${REGEN_CMD[@]}" + +DRIFT="$(git status --porcelain -- ui/openapi.json)" + +if [[ -n "${DRIFT}" ]]; then + echo "ERROR: ui/openapi.json is stale." >&2 + echo "Fix with:" >&2 + echo " uv run python -m backend.app.openapi_export --out ui/openapi.json && git add ui/openapi.json" >&2 + echo >&2 + echo "Drift detected (diagnostic):" >&2 + printf '%s\n' "${DRIFT}" >&2 + exit 1 +fi + +echo "OK: ui/openapi.json is fresh." From 42b0b3b062ee5fce14455e07827fa5752d979374 Mon Sep 17 00:00:00 2001 From: SoundMindsAI Date: Tue, 2 Jun 2026 23:24:11 -0400 Subject: [PATCH 07/10] infra(types): determinism fix + types.ts freshness gate (Story 2.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Story 2.3 of infra_generated_artifact_freshness_gate (FR-5 + FR-2 + FR-6 types half). - ui/scripts/gen-types-banner.mjs (new) — pure, side-effect-free module exporting buildBanner(). The banner names the COMMITTED snapshot path (ui/openapi.json), not the live OPENAPI_URL value, so local-dev + CI-snapshot regens produce byte-identical banners (FR-5 source-invariance). Drops the false "CI does NOT regenerate" stance and names the generated-artifacts-fresh CI gate instead. - ui/scripts/gen-types.mjs — three changes: 1. Pinned-binary invocation via node_modules/.bin/openapi-typescript (no npx fallback) — fails loudly if pnpm install was skipped. 2. Imports buildBanner from the new pure module. 3. ESM entry-point guard — importing the module is a no-op. - ui/src/__tests__/scripts/gen-types-banner.test.ts (new) — 6 cases: byte-stability, invariance across OPENAPI_URL values, canonical Source-line, SPDX prefix preserved, freshness-gate stance. Automated AC-8. - scripts/ci/verify_types_fresh.sh + test_verify_types_fresh.sh — guard regenerates via canonical pnpm types:gen invocation; fails on git status --porcelain drift; prints chained fix command (Story 2.4). Self-test uses TYPES_FRESH_REGEN_SCRIPT path-override pattern from Story 2.2. 7/7 self-test cases green. - .github/workflows/pr.yml — appends self-test + types-guard steps to the existing generated-artifacts-fresh job (cross-story edit declared in implementation_plan.md §11). - docs/05_quality/testing.md — appends row #3 to the freshness-gates table + chained fix command. - ui/src/lib/types.ts — regenerated via the refactored gen-types.mjs + new buildBanner. PR §16 rollout requirement: introducing PR freshens all artifacts. Prettier-formatted post-regen. Tangential inline fix (per CLAUDE.md tangential-discoveries rule — <60 min, same subsystem, no design fork): - studies-table-ceiling-badge.test.tsx fixture omitted trial_count, which the backend marks required (int = 0 at backend/app/api/v1/ schemas.py:902, shipped with PR #421). Pre-existing test passed only against the stale types.ts; the freshness-gate regen surfaced the drift. Added trial_count: 0 with a citing comment. Verification: 17/17 scripts vitests green; 7/7 types-guard self-test green; pnpm typecheck clean; reuse-lint compliant (REUSE-IgnoreStart/ End wrappers added around an SPDX-shaped regex literal in gen-types-banner.test.ts that reuse-lint was mis-parsing as a real declaration). Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: SoundMindsAI --- .github/workflows/pr.yml | 4 + docs/05_quality/testing.md | 5 + scripts/ci/test_verify_types_fresh.sh | 158 + scripts/ci/verify_types_fresh.sh | 75 + ui/scripts/gen-types-banner.mjs | 59 + ui/scripts/gen-types.mjs | 135 +- .../studies-table-ceiling-badge.test.tsx | 8 + .../scripts/gen-types-banner.test.ts | 113 + ui/src/lib/types.ts | 3355 +++++++++-------- 9 files changed, 2204 insertions(+), 1708 deletions(-) create mode 100755 scripts/ci/test_verify_types_fresh.sh create mode 100755 scripts/ci/verify_types_fresh.sh create mode 100644 ui/scripts/gen-types-banner.mjs create mode 100644 ui/src/__tests__/scripts/gen-types-banner.test.ts diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 81aaf88d..a922a4d1 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -257,6 +257,10 @@ jobs: run: bash scripts/ci/test_verify_openapi_snapshot_fresh.sh - name: Verify ui/openapi.json snapshot is fresh run: bash scripts/ci/verify_openapi_snapshot_fresh.sh + - name: Self-test types guard + run: bash scripts/ci/test_verify_types_fresh.sh + - name: Verify ui/src/lib/types.ts is fresh + run: bash scripts/ci/verify_types_fresh.sh # ------------------------------------------------------------------------- # Static checks — ALWAYS run (not gated by SKIP_HEAVY_CI). Split into two diff --git a/docs/05_quality/testing.md b/docs/05_quality/testing.md index b96c2569..d3d46472 100644 --- a/docs/05_quality/testing.md +++ b/docs/05_quality/testing.md @@ -257,6 +257,7 @@ markers) — every drift mode the gate exists to catch. |---|---|---|---|---|---| | 1 | `copy-docs-freshness` | own file (`copy-docs-freshness.yml`) — runs on every PR with no `paths-ignore` filter (FR-3 escape from `pr.yml`'s `docs/**` paths-ignore so docs-only PRs still get the check) | `docs/08_guides/*.md` → `ui/public/docs/*.md` | `node ui/scripts/copy-docs.mjs` (prunes the dest to `{README.md} ∪ {DOCS[].dest}` per FR-9, so a renamed entry never leaves a stale public copy behind) | `scripts/ci/test_verify_copy_docs_fresh.sh` exercises clean / source-drift / untracked-AC-9 cases against a disposable `mktemp` git fixture | | 2 | `generated-artifacts-fresh` (snapshot step) | `pr.yml` job — backend (`**/*.py`) + ui (`**/*.ts`) changes can both invalidate the snapshot, so the gate runs on every code-bearing PR | backend FastAPI route table → `ui/openapi.json` | `uv run python -m backend.app.openapi_export --out ui/openapi.json` (offline, no live services per Story 2.1) | `scripts/ci/test_verify_openapi_snapshot_fresh.sh` uses an `OPENAPI_SNAPSHOT_REGEN_SCRIPT` path-override + a disposable `mktemp` fixture to test the guard's diff-detection without needing `uv` in the fixture (the exporter has its own Story-2.1 unit test) | +| 3 | `generated-artifacts-fresh` (types step) | same `pr.yml` job — chained after the snapshot step so they share the toolchain install | `ui/openapi.json` → `ui/src/lib/types.ts` | `OPENAPI_URL="$PWD/ui/openapi.json" pnpm --dir ui types:gen` (wraps the lockfile-pinned `openapi-typescript@7.x` binary; Story 2.3 ditched the `npx` fallback per FR-5) | `scripts/ci/test_verify_types_fresh.sh` uses a `TYPES_FRESH_REGEN_SCRIPT` path-override against a disposable fixture; `ui/src/__tests__/scripts/gen-types-banner.test.ts` covers banner source-invariance (FR-5 / AC-8) | The fix commands printed on failure: @@ -266,6 +267,10 @@ cd ui && node scripts/copy-docs.mjs && git add public/docs # Gate 2 (openapi.json snapshot) uv run python -m backend.app.openapi_export --out ui/openapi.json && git add ui/openapi.json + +# Gate 3 (types.ts) — refreshes the snapshot too, so use the chained +# fix landing in Story 2.4 (`scripts/regen-generated-artifacts.sh`) +bash scripts/regen-generated-artifacts.sh && git add ui/openapi.json ui/src/lib/types.ts ``` The freshness-gate scripts (`scripts/ci/verify_copy_docs_fresh.sh` + its diff --git a/scripts/ci/test_verify_types_fresh.sh b/scripts/ci/test_verify_types_fresh.sh new file mode 100755 index 00000000..cfff5fc1 --- /dev/null +++ b/scripts/ci/test_verify_types_fresh.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2026 soundminds.ai +# +# SPDX-License-Identifier: Apache-2.0 + +# Self-test for `scripts/ci/verify_types_fresh.sh` +# (Story 2.3 of `infra_generated_artifact_freshness_gate`). +# +# Builds a disposable git fixture in a tmp directory containing a +# pre-committed `ui/src/lib/types.ts` (test bytes, NOT the real +# generated types) and exercises three cases via the guard's +# `TYPES_FRESH_REGEN_SCRIPT` path-override: +# +# 1. Clean tree → override re-writes the same bytes, tree +# stays clean, guard exits 0 +# 2. Source-drift → override writes DIFFERENT bytes +# (simulating a snapshot change that +# produces different generated types), +# tree goes dirty, guard exits 1 with the +# canonical chained fix-command text +# 3. Untracked AC-9 case → `git rm --cached` types.ts, override +# writes the same bytes, guard reports `??` +# and exits 1 +# +# Using a script-path override avoids needing `pnpm` + `node_modules` + +# the project venv in the fixture — the banner has its own Story-2.3 +# vitest; this self-test verifies the guard's diff-detection logic only. +# +# Run locally: bash scripts/ci/test_verify_types_fresh.sh +# Run in CI: invoked by the `generated-artifacts-fresh` job. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +GUARD="${REPO_ROOT}/scripts/ci/verify_types_fresh.sh" + +PASS=0 +FAIL=0 + +if [[ ! -r "${GUARD}" ]]; then + echo "FATAL: cannot find guard at ${GUARD}" >&2 + exit 2 +fi + +CANONICAL_BYTES='// generated types — fixture v1' +DRIFTED_BYTES='// generated types — fixture v2-DRIFTED' + +build_fixture() { + local fixture="$1" + mkdir -p "${fixture}/ui/src/lib" + printf '%s\n' "${CANONICAL_BYTES}" > "${fixture}/ui/src/lib/types.ts" + ( + cd "${fixture}" + git init -q -b main + git config user.email "selftest@local" + git config user.name "self-test" + git add ui/src/lib/types.ts + git commit -q -m "init" + ) +} + +write_regen_script() { + local fixture="$1" + local bytes="$2" + local script="${fixture}/regen.sh" + cat > "${script}" < ui/src/lib/types.ts +EOF + chmod +x "${script}" + printf '%s\n' "${script}" +} + +run_guard() { + local fixture="$1" + local regen_script="$2" + local logfile="$3" + ( + cd "${fixture}" + TYPES_FRESH_REPO_ROOT="${fixture}" \ + TYPES_FRESH_REGEN_SCRIPT="${regen_script}" \ + bash "${GUARD}" + ) >"${logfile}" 2>&1 +} + +assert_eq() { + local expected="$1" + local actual="$2" + local name="$3" + if [[ "${actual}" -eq "${expected}" ]]; then + echo " ok ${name}" + PASS=$((PASS + 1)) + else + echo " FAIL ${name} (expected exit ${expected}, got ${actual})" + FAIL=$((FAIL + 1)) + fi +} + +assert_contains() { + local needle="$1" + local file="$2" + local name="$3" + if grep -qF -- "${needle}" "${file}"; then + echo " ok ${name}" + PASS=$((PASS + 1)) + else + echo " FAIL ${name} (did not find '${needle}' in ${file})" + FAIL=$((FAIL + 1)) + fi +} + +trap 'rm -rf "${TMP1:-}" "${TMP2:-}" "${TMP3:-}"' EXIT + +# --- Case 1: clean tree --------------------------------------------------- +echo "Case 1: clean tree" +TMP1="$(mktemp -d -t rl-types-fresh-1.XXXXXX)" +build_fixture "${TMP1}" +LOG1="${TMP1}.log" +CLEAN_SCRIPT="$(write_regen_script "${TMP1}" "${CANONICAL_BYTES}")" +actual=0 +run_guard "${TMP1}" "${CLEAN_SCRIPT}" "${LOG1}" || actual=$? +assert_eq 0 "${actual}" "clean tree → exit 0" +assert_contains "OK: ui/src/lib/types.ts is fresh." "${LOG1}" "clean tree → success message" + +# --- Case 2: source-drift ------------------------------------------------- +echo "Case 2: source-drift" +TMP2="$(mktemp -d -t rl-types-fresh-2.XXXXXX)" +build_fixture "${TMP2}" +LOG2="${TMP2}.log" +DRIFT_SCRIPT="$(write_regen_script "${TMP2}" "${DRIFTED_BYTES}")" +actual=0 +run_guard "${TMP2}" "${DRIFT_SCRIPT}" "${LOG2}" || actual=$? +assert_eq 1 "${actual}" "source-drift → exit 1" +assert_contains "ui/src/lib/types.ts is stale." "${LOG2}" "source-drift → error header" +assert_contains "scripts/regen-generated-artifacts.sh" \ + "${LOG2}" "source-drift → chained fix-command text" + +# --- Case 3: untracked AC-9 ---------------------------------------------- +echo "Case 3: untracked types.ts (git rm --cached)" +TMP3="$(mktemp -d -t rl-types-fresh-3.XXXXXX)" +build_fixture "${TMP3}" +( cd "${TMP3}" && git rm --cached -q ui/src/lib/types.ts ) +LOG3="${TMP3}.log" +UNTRACKED_SCRIPT="$(write_regen_script "${TMP3}" "${CANONICAL_BYTES}")" +actual=0 +run_guard "${TMP3}" "${UNTRACKED_SCRIPT}" "${LOG3}" || actual=$? +assert_eq 1 "${actual}" "untracked AC-9 → exit 1" +assert_contains "?? ui/src/lib/types.ts" "${LOG3}" \ + "untracked AC-9 → git status reports ?? marker" + +echo +echo "${PASS} passed, ${FAIL} failed" +if [[ "${FAIL}" -gt 0 ]]; then + exit 1 +fi diff --git a/scripts/ci/verify_types_fresh.sh b/scripts/ci/verify_types_fresh.sh new file mode 100755 index 00000000..03931d18 --- /dev/null +++ b/scripts/ci/verify_types_fresh.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2026 soundminds.ai +# +# SPDX-License-Identifier: Apache-2.0 + +# infra_generated_artifact_freshness_gate / Story 2.3 — FR-2 + FR-6. +# +# Regenerates `ui/src/lib/types.ts` from the committed `ui/openapi.json` +# snapshot via the package script `pnpm types:gen` (which wraps the +# lockfile-pinned `openapi-typescript` binary — Story 2.3 ditched the +# `npx` fallback per FR-5). Fails if `git status --porcelain -- ui/src/ +# lib/types.ts` is non-empty. +# +# The regen uses the **absolute filesystem path** to the snapshot +# (`OPENAPI_URL="$PWD/ui/openapi.json"`) — `openapi-typescript@7.5.2` +# accepts that form directly (verified in Story 2.3 task 2; no `file://` +# fallback needed). +# +# Uses `git status --porcelain` (not `git diff --exit-code`) so the +# untracked-file regression (a fresh commit forgetting to `git add` +# `types.ts`) is flagged. +# +# Usage: +# bash scripts/ci/verify_types_fresh.sh +# +# Override env vars (intended for the self-test harness, NOT production): +# +# TYPES_FRESH_REPO_ROOT +# Override `git rev-parse --show-toplevel` so the guard operates +# on a disposable fixture instead of the live repo. +# +# TYPES_FRESH_REGEN_SCRIPT +# Path to a bash script that performs the regen step. Defaults to +# running `OPENAPI_URL="$REPO_ROOT/ui/openapi.json" pnpm --dir ui +# types:gen` directly. The self-test points this at a small +# fixture-local stub so it doesn't need pnpm + node_modules in +# the fixture (the banner has its own Story-2.3 vitest; the +# guard's job is diff-detection). +# +# Exits 0 when types.ts is fresh, 1 when it is stale. + +set -euo pipefail + +if [[ -n "${TYPES_FRESH_REPO_ROOT:-}" ]]; then + REPO_ROOT="${TYPES_FRESH_REPO_ROOT}" +else + REPO_ROOT="$(git rev-parse --show-toplevel)" +fi +cd "${REPO_ROOT}" + +if [[ -n "${TYPES_FRESH_REGEN_SCRIPT:-}" ]]; then + REGEN_CMD=(bash "${TYPES_FRESH_REGEN_SCRIPT}") +else + # Pass the absolute path to the snapshot (FR-2 source form). The + # package script invokes `node scripts/gen-types.mjs`, which after + # Story 2.3 uses the pinned `openapi-typescript` binary (no `npx`). + REGEN_CMD=(env "OPENAPI_URL=${REPO_ROOT}/ui/openapi.json" pnpm --dir ui types:gen) +fi + +"${REGEN_CMD[@]}" + +DRIFT="$(git status --porcelain -- ui/src/lib/types.ts)" + +if [[ -n "${DRIFT}" ]]; then + echo "ERROR: ui/src/lib/types.ts is stale." >&2 + echo "Fix with:" >&2 + echo " bash scripts/regen-generated-artifacts.sh && git add ui/openapi.json ui/src/lib/types.ts" >&2 + echo >&2 + echo "Drift detected (diagnostic):" >&2 + printf '%s\n' "${DRIFT}" >&2 + exit 1 +fi + +echo "OK: ui/src/lib/types.ts is fresh." diff --git a/ui/scripts/gen-types-banner.mjs b/ui/scripts/gen-types-banner.mjs new file mode 100644 index 00000000..d58545a9 --- /dev/null +++ b/ui/scripts/gen-types-banner.mjs @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2026 soundminds.ai +// +// SPDX-License-Identifier: Apache-2.0 + +// @ts-check + +/** + * Pure, side-effect-free `buildBanner()` for `ui/src/lib/types.ts`. + * + * Story 2.3 of `infra_generated_artifact_freshness_gate` (FR-5). + * + * Why a separate module: + * + * 1. **Source-invariance** — the banner MUST be byte-identical regardless + * of which value `OPENAPI_URL` carries when the script runs. The + * previous embedded form `// Source: ${SOURCE_URL}` differed between + * a local-dev run (`http://localhost:8000/openapi.json`) and the + * CI-snapshot run (`$PWD/ui/openapi.json`), so the freshness gate + * would have flapped depending on developer environment. By making + * the banner a pure function of *no* inputs, the same bytes land + * every time. + * + * 2. **Testability** — `gen-types-banner.test.ts` imports this module + * directly and asserts the banner is byte-identical across multiple + * invocations. Importing `gen-types.mjs` itself would trigger + * generation (it shells out to `openapi-typescript`), which the + * entry-point guard in `gen-types.mjs` now prevents — but extracting + * the banner to its own pure module is the belt-and-braces version + * of that guarantee. + */ + +/** + * The canonical banner prepended to every regeneration of + * `ui/src/lib/types.ts`. Carries the SPDX header (the reuse-lint + * pre-commit hook rejects any tracked file without one; + * `openapi-typescript` strips the inline SPDX on every regen, so this + * wrapper re-prepends it). The "Source" line names the COMMITTED + * snapshot path (`ui/openapi.json`), not the live `OPENAPI_URL` value, + * so the banner is host-invariant. + * + * @returns {string} + */ +export function buildBanner() { + return `// SPDX-FileCopyrightText: 2026 soundminds.ai +// +// SPDX-License-Identifier: Apache-2.0 + +// GENERATED FILE — do not edit. Regenerate via: cd ui && pnpm types:gen +// Source: backend OpenAPI schema (canonical snapshot: ui/openapi.json) +// +// This file is CI-freshness-gated by the \`generated-artifacts-fresh\` +// job in \`.github/workflows/pr.yml\`. CI regenerates it from the +// committed \`ui/openapi.json\` snapshot and fails the PR if the +// committed bytes drift. Run \`scripts/regen-generated-artifacts.sh\` +// locally to refresh \`ui/openapi.json\` + \`ui/src/lib/types.ts\` +// in lockstep. + +`; +} diff --git a/ui/scripts/gen-types.mjs b/ui/scripts/gen-types.mjs index 2f16d1f0..8f2d2c95 100644 --- a/ui/scripts/gen-types.mjs +++ b/ui/scripts/gen-types.mjs @@ -4,6 +4,8 @@ // // SPDX-License-Identifier: Apache-2.0 +// @ts-check + /** * Wraps `openapi-typescript` so the GENERATED-FILE banner survives every * regeneration. Without this wrapper, running `pnpm types:gen` (or the @@ -11,52 +13,107 @@ * * Usage (from ui/): node scripts/gen-types.mjs * Or via the package script: pnpm types:gen + * + * Story 2.3 of `infra_generated_artifact_freshness_gate` (FR-5): + * + * 1. **Pinned binary, not `npx`.** Invokes the lockfile-pinned + * `node_modules/.bin/openapi-typescript[.cmd]` directly. Fails loudly + * if the binary is absent — no implicit `npx` download path that + * could pull a different version at runtime, and no network + * dependency to flake against. The dependency is pinned by + * `ui/pnpm-lock.yaml` (`openapi-typescript@7.x` in + * `ui/package.json`), so the version is reproducible. + * + * 2. **Source-invariant banner.** The banner is produced by + * `buildBanner()` in `gen-types-banner.mjs`, a pure module that + * takes no inputs. The banner names the COMMITTED snapshot path + * (`ui/openapi.json`), not the live `OPENAPI_URL` value, so a + * local-dev run + a CI-snapshot run produce byte-identical banners. + * Without this, the previous form `// Source: ${SOURCE_URL}` would + * cause the freshness gate to flap between environments. + * + * 3. **Entry-point guard.** Generation runs only when this module is + * invoked as the main script. Importing `gen-types.mjs` (e.g., from + * a vitest) does not shell out to `openapi-typescript` and does not + * mutate `ui/src/lib/types.ts`. The `buildBanner` test lives in + * `gen-types-banner.mjs`, which is genuinely side-effect-free; the + * guard here is belt-and-braces. */ import { execFileSync } from 'node:child_process'; -import { readFileSync, writeFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname, resolve } from 'node:path'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +import { buildBanner } from './gen-types-banner.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const UI_ROOT = resolve(__dirname, '..'); const OUTPUT = resolve(UI_ROOT, 'src/lib/types.ts'); -const SOURCE_URL = process.env.OPENAPI_URL ?? 'http://localhost:8000/openapi.json'; +// Default to the committed snapshot — CI uses this path, and a local +// regen against the snapshot produces byte-identical output. To +// regenerate from a running backend's live `/openapi.json` instead, +// export `OPENAPI_URL=http://localhost:8000/openapi.json`. +const DEFAULT_OPENAPI_PATH = resolve(UI_ROOT, 'openapi.json'); +const SOURCE_URL = process.env.OPENAPI_URL ?? DEFAULT_OPENAPI_PATH; -// SPDX header first so the file stays REUSE-compliant (the reuse-lint -// pre-commit hook rejects any tracked file without it); openapi-typescript -// strips it on every regen, so the wrapper re-prepends it here. -const BANNER = `// SPDX-FileCopyrightText: 2026 soundminds.ai -// -// SPDX-License-Identifier: Apache-2.0 +/** + * Path to the lockfile-pinned openapi-typescript binary. Windows + * pnpm installs `.cmd` shims; POSIX uses bare names. Fail loudly if + * the binary is missing — that signals `pnpm install --frozen-lockfile` + * was skipped, which would otherwise let `npx` pull a different version + * at runtime (the FR-5 bug this story fixes). + * + * @returns {string} + */ +function resolvePinnedBinary() { + const binDir = resolve(UI_ROOT, 'node_modules', '.bin'); + const candidates = + process.platform === 'win32' + ? ['openapi-typescript.cmd', 'openapi-typescript'] + : ['openapi-typescript']; + for (const candidate of candidates) { + const p = join(binDir, candidate); + if (existsSync(p)) { + return p; + } + } + throw new Error( + `gen-types.mjs: pinned openapi-typescript binary not found under ${binDir}.\n` + + `Run \`pnpm --dir ui install --frozen-lockfile\` first. We intentionally do NOT fall ` + + `back to npx — npx can resolve/download a different version at runtime, defeating the ` + + `lockfile pin (FR-5 of infra_generated_artifact_freshness_gate).`, + ); +} -// GENERATED FILE — do not edit. Regenerate via: cd ui && pnpm types:gen -// Source: ${SOURCE_URL} -// -// Prerequisite: the backend must be running (make up) so /openapi.json is reachable. -// CI does NOT regenerate this file — the committed version is the source of truth -// for the PR. If you need a fresh schema, run \`cd ui && pnpm types:gen\` locally. - -`; - -console.log(`Generating ${OUTPUT} from ${SOURCE_URL}…`); -// execFileSync (no shell) instead of execSync with an interpolated string: -// SOURCE_URL comes from the OPENAPI_URL env var, and an interpolated shell -// command would let a crafted value inject. Passing args as an array runs the -// binary directly with no shell, so there is nothing to inject into. On Windows -// the launcher is `npx.cmd` (no shell to resolve the `.cmd` extension), so pick -// the platform-correct executable name. -const NPX = process.platform === 'win32' ? 'npx.cmd' : 'npx'; -execFileSync(NPX, ['openapi-typescript', SOURCE_URL, '-o', OUTPUT], { - stdio: 'inherit', - cwd: UI_ROOT, -}); - -const generated = readFileSync(OUTPUT, 'utf8'); -if (generated.startsWith('// SPDX-FileCopyrightText')) { - // Banner already present (shouldn't happen, but be idempotent). - console.log('Banner already present; skipping prepend.'); -} else { - writeFileSync(OUTPUT, BANNER + generated, 'utf8'); - console.log('Banner prepended.'); +/** + * Run openapi-typescript via the pinned binary against SOURCE_URL, + * then prepend the canonical banner if it isn't already there. + */ +function generate() { + const bin = resolvePinnedBinary(); + // execFileSync (no shell) — SOURCE_URL comes from OPENAPI_URL env var, + // and a shell-interpolated command would let a crafted value inject. + // Array argv is shell-free. + console.log(`Generating ${OUTPUT} from ${SOURCE_URL}…`); + execFileSync(bin, [SOURCE_URL, '-o', OUTPUT], { + stdio: 'inherit', + cwd: UI_ROOT, + }); + + const generated = readFileSync(OUTPUT, 'utf8'); + if (generated.startsWith('// SPDX-FileCopyrightText')) { + // Banner already present (shouldn't happen — openapi-typescript + // strips inline headers on every regen — but be idempotent). + console.log('Banner already present; skipping prepend.'); + } else { + writeFileSync(OUTPUT, buildBanner() + generated, 'utf8'); + console.log('Banner prepended.'); + } +} + +// Entry-point guard: run generation only when invoked as the main +// script. Importing the module (e.g., from a vitest) is a no-op. +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + generate(); } diff --git a/ui/src/__tests__/components/studies/studies-table-ceiling-badge.test.tsx b/ui/src/__tests__/components/studies/studies-table-ceiling-badge.test.tsx index 1732bd9d..135e59b0 100644 --- a/ui/src/__tests__/components/studies/studies-table-ceiling-badge.test.tsx +++ b/ui/src/__tests__/components/studies/studies-table-ceiling-badge.test.tsx @@ -37,6 +37,14 @@ function renderBestMetricCell(overrides: Partial) { direction: 'maximize', created_at: '2026-05-29T00:00:00Z', completed_at: '2026-05-29T00:05:00Z', + // `trial_count` is a required `int = 0` field on the backend (per + // `backend/app/api/v1/schemas.py:902`, shipped with + // `feat_studies_convergence_visibility` / PR #421). The freshness- + // gate regen of `types.ts` surfaced that this fixture omitted it + // (it had been working by accident against a stale committed + // types.ts that didn't yet reflect the new required field) — + // tangential fix while shipping `infra_generated_artifact_freshness_gate`. + trial_count: 0, ...overrides, }; // The DataTable cell renderer only reads `row.original`; a minimal stub diff --git a/ui/src/__tests__/scripts/gen-types-banner.test.ts b/ui/src/__tests__/scripts/gen-types-banner.test.ts new file mode 100644 index 00000000..7c57412e --- /dev/null +++ b/ui/src/__tests__/scripts/gen-types-banner.test.ts @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2026 soundminds.ai +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Vitest for `ui/scripts/gen-types-banner.mjs` source-invariance + * (Story 2.3 of `infra_generated_artifact_freshness_gate`, FR-5 / + * AC-8 automated). + * + * `buildBanner()` is the canonical generated-file banner prepended to + * every regeneration of `ui/src/lib/types.ts`. It MUST be byte-identical + * regardless of which value `OPENAPI_URL` carries when the wrapping + * script runs — otherwise a local-dev regen ("http://localhost:8000/...") + * and a CI-snapshot regen ("$PWD/ui/openapi.json") would produce + * different banner bytes and the freshness gate would flap. + * + * The test enforces invariance by: + * + * 1. Calling `buildBanner()` with no inputs and asserting the same + * bytes come back across multiple invocations. + * 2. Mutating `process.env.OPENAPI_URL` to various values around the + * calls and asserting the banner is still identical — this is the + * structural proof that the function has no `OPENAPI_URL` input. + * 3. Asserting the banner contains the canonical Source line + * (`ui/openapi.json`) so a future "improvement" that re-introduces + * an `${OPENAPI_URL}` interpolation fails the test. + * 4. Asserting that importing `gen-types-banner.mjs` does NOT shell + * out to `openapi-typescript` — the test process completes without + * spawning the binary (side-effect-free import). + */ + +import { describe, expect, it } from 'vitest'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore — the .mjs ships its own JSDoc types; vitest resolves it natively. +import { buildBanner } from '../../../scripts/gen-types-banner.mjs'; + +describe('gen-types-banner.mjs — buildBanner()', () => { + it('returns the same bytes across repeated calls (source-invariant)', () => { + const a = buildBanner(); + const b = buildBanner(); + expect(a).toBe(b); + }); + + it('is invariant when OPENAPI_URL changes (proves no env input)', () => { + const original = process.env.OPENAPI_URL; + try { + process.env.OPENAPI_URL = 'http://localhost:8000/openapi.json'; + const fromLive = buildBanner(); + process.env.OPENAPI_URL = '/abs/path/to/ui/openapi.json'; + const fromAbs = buildBanner(); + delete process.env.OPENAPI_URL; + const fromUnset = buildBanner(); + + expect(fromLive).toBe(fromAbs); + expect(fromAbs).toBe(fromUnset); + } finally { + if (original === undefined) { + delete process.env.OPENAPI_URL; + } else { + process.env.OPENAPI_URL = original; + } + } + }); + + it('points at the committed snapshot, not the live URL', () => { + const banner = buildBanner(); + // The Source line must name the canonical snapshot. Without this + // assertion, a future edit that re-introduces an + // `// Source: ${SOURCE_URL}` interpolation would slip through the + // invariance test (both runs in that test would pick up the SAME + // host-specific URL because they read the same process env). + expect(banner).toMatch(/Source: backend OpenAPI schema/); + expect(banner).toMatch(/canonical snapshot: ui\/openapi\.json/); + // And it should NOT mention localhost or any environment-specific + // URL — those are the prior-bad-form strings. + expect(banner).not.toMatch(/localhost/); + expect(banner).not.toMatch(/http:\/\//); + }); + + it('starts with the SPDX header (so re-prepended files stay REUSE-clean)', () => { + const banner = buildBanner(); + expect(banner.startsWith('// SPDX-FileCopyrightText:')).toBe(true); + // REUSE-IgnoreStart — the regex literal below LOOKS like an SPDX + // declaration to `reuse lint`, which would then try to parse the + // trailing `\.0/);` JavaScript syntax as an SPDX expression and + // fail. The Ignore markers tell reuse-lint to skip this region. + expect(banner).toMatch(/SPDX-License-Identifier: Apache-2\.0/); + // REUSE-IgnoreEnd + }); + + it('documents the CI freshness gate (not the obsolete "CI does NOT regenerate" line)', () => { + const banner = buildBanner(); + // Positive assertion: the gate is named. + expect(banner).toMatch(/CI-freshness-gated/); + expect(banner).toMatch(/generated-artifacts-fresh/); + // Negative assertion: the old false stance is gone. + expect(banner).not.toMatch(/CI does NOT regenerate/); + }); + + it('importing this module does not run openapi-typescript', () => { + // Structural assertion: if importing the module had a side effect + // (e.g., a top-level `generate()` call), `process.argv[0]` would + // need to be `node` AND `process.argv[1]` would need to be the + // wrapper script. By the time vitest runs us, `process.argv[1]` + // points at vitest's worker, not at gen-types*.mjs. So the import + // above already proved the no-side-effect guarantee — it returned + // cleanly without spawning a binary or writing to types.ts. + // Belt-and-braces: confirm the import resolved (no exception + // thrown above) and the export is callable. + expect(typeof buildBanner).toBe('function'); + }); +}); diff --git a/ui/src/lib/types.ts b/ui/src/lib/types.ts index efd8a5d1..c63715d2 100644 --- a/ui/src/lib/types.ts +++ b/ui/src/lib/types.ts @@ -3,11 +3,14 @@ // SPDX-License-Identifier: Apache-2.0 // GENERATED FILE — do not edit. Regenerate via: cd ui && pnpm types:gen -// Source: http://localhost:8000/openapi.json +// Source: backend OpenAPI schema (canonical snapshot: ui/openapi.json) // -// Prerequisite: the backend must be running (make up) so /openapi.json is reachable. -// CI does NOT regenerate this file — the committed version is the source of truth -// for the PR. If you need a fresh schema, run `cd ui && pnpm types:gen` locally. +// This file is CI-freshness-gated by the `generated-artifacts-fresh` +// job in `.github/workflows/pr.yml`. CI regenerates it from the +// committed `ui/openapi.json` snapshot and fails the PR if the +// committed bytes drift. Run `scripts/regen-generated-artifacts.sh` +// locally to refresh `ui/openapi.json` + `ui/src/lib/types.ts` +// in lockstep. /** * This file was auto-generated by openapi-typescript. @@ -15,36 +18,27 @@ */ export interface paths { - '/healthz': { + '/api/v1/_test/auto-followup/seed-chain': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Healthz - * @description Probe each subsystem in parallel and return the documented JSON shape. - * - * Args: - * settings: Application settings (DB URL, ES/OS URLs, OpenAI base URL, etc.) - * redis_client: Redis client for ping probe + capability-cache read - * es_client: shared httpx client for ES + OpenSearch HTTP probes - * db: Async DB session for the registered-clusters aggregate (Story 3.5) - * - * Returns: - * JSONResponse with the HealthResponse body and HTTP 200 (healthy) or 503 (degraded). + * Seed an auto-followup chain of N+1 linked studies + * @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. */ - get: operations['healthz_healthz_get']; - put?: never; - post?: never; + post: operations['seed_auto_followup_chain_endpoint_api_v1__test_auto_followup_seed_chain_post']; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/clusters/test-connection': { + '/api/v1/_test/demo/reseed': { parameters: { query?: never; header?: never; @@ -54,188 +48,151 @@ export interface paths { get?: never; put?: never; /** - * Test Connection - * @description Probe a cluster config WITHOUT persisting (infra_adapter_solr Story A9). + * Enqueue a demo-state reseed (dev-only, async) + * @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. * - * Powers the registration modal's "Test connection" button. Always 200 — - * transport failures surface as ``reachable=false`` with ``error`` set. - * Invalid engine×auth pairings 400 BEFORE the network call. + * Per ``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. */ - post: operations['test_connection_api_v1_clusters_test_connection_post']; + post: operations['reseed_demo_api_v1__test_demo_reseed_post']; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/clusters/{cluster_id}/reprobe': { + '/api/v1/_test/demo/reseed/status': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Reprobe Cluster - * @description Re-run cluster capability probe (Story A9 / spec FR-2 + AC-14). - * - * Concurrent calls serialize on ``SELECT … FOR UPDATE``. On probe failure - * the row's engine_config is NOT updated (the transaction rolls back). + * Poll the current demo-reseed progress (dev-only) + * @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. */ - post: operations['reprobe_cluster_api_v1_clusters__cluster_id__reprobe_post']; + get: operations['reseed_demo_status_api_v1__test_demo_reseed_status_get']; + put?: never; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/clusters': { + '/api/v1/_test/digests/{digest_id}': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** - * List Clusters - * @description List clusters with cursor pagination + ``X-Total-Count`` header. - * - * ``?q=`` is a Postgres FTS match against the cluster's ``search_vector`` - * (name + base_url); 2–200 chars. Filter-only — ordering unchanged per - * spec FR-1. ``?sort=`` is one of the values in - * :data:`~backend.app.api.v1.schemas.ClusterSortKey`; the cursor is - * sort-aware so the keyset predicate matches the active ORDER BY - * (feat_data_table_primitive Stories 1.2 + 1.3). - */ - get: operations['list_clusters_api_v1_clusters_get']; + get?: never; put?: never; + post?: never; /** - * Create Cluster - * @description Register a cluster (FR-5 / AC-1). + * Hard-delete a digest (test-only) + * @description FR-2: Hard-delete the digest row. No FK children — no preflight needed. */ - post: operations['create_cluster_api_v1_clusters_post']; - delete?: never; + delete: operations['delete_test_digest_api_v1__test_digests__digest_id__delete']; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/clusters/{cluster_id}': { + '/api/v1/_test/judgment-lists/{judgment_list_id}': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** - * Get Cluster Detail - * @description Return cluster row + cached/fresh health probe. - */ - get: operations['get_cluster_detail_api_v1_clusters__cluster_id__get']; + get?: never; put?: never; post?: never; /** - * Delete Cluster - * @description Soft-delete a cluster (AC-8). Returns 204 with no body. + * Hard-delete a judgment_list (test-only) + * @description FR-4 — hard-delete the judgment_list row. + * + * Judgments cascade-delete via existing FK. Preflight-checks ``studies`` + * (non-cascade); 409 if any study references the judgment_list. */ - delete: operations['delete_cluster_api_v1_clusters__cluster_id__delete']; + delete: operations['delete_test_judgment_list_api_v1__test_judgment_lists__judgment_list_id__delete']; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/clusters/{cluster_id}/schema': { + '/api/v1/_test/proposals/{proposal_id}': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** - * Get Cluster Schema - * @description Return the field schema for ``target`` (FR-4 / AC-2). - */ - get: operations['get_cluster_schema_api_v1_clusters__cluster_id__schema_get']; + get?: never; put?: never; post?: never; - delete?: never; + /** + * Hard-delete a proposal (test-only) + * @description FR-1: Hard-delete the proposal row. No FK children — no preflight needed. + */ + delete: operations['delete_test_proposal_api_v1__test_proposals__proposal_id__delete']; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/clusters/{cluster_id}/ubi-readiness': { + '/api/v1/_test/query-sets/{query_set_id}': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; + post?: never; /** - * Get Cluster Ubi Readiness - * @description Classify ``(cluster, query_set, target)`` on the UBI rung ladder. - * - * feat_ubi_judgments FR-7. - * - * Required query params: ``query_set_id`` + ``target`` (Spec FR-7 + - * cycle-3 D-10c: the endpoint MUST 422 without them — the classifier - * can't compute a per-target rung without an application filter). - * - * Error envelopes (all per spec §7.5): - * * ``404 CLUSTER_NOT_FOUND`` — cluster row missing or soft-deleted. - * * ``404 QUERY_SET_NOT_FOUND`` — query set row missing. - * * ``422 VALIDATION_ERROR`` — missing required query params (FastAPI's - * built-in handler, surfaces via ``api/errors.py``). - * * ``503 CLUSTER_UNREACHABLE`` — adapter cannot reach the cluster. + * Hard-delete a query_set (test-only) + * @description FR-5 — hard-delete the query_set row. * - * The result is cached for 60 s in Redis per - * ``(cluster_id, query_set_id, target)`` so back-to-back dialog-open - * and dialog-submit calls don't re-probe. + * Queries cascade-delete via existing FK. Preflight-checks ``studies`` + * + ``judgment_lists`` (both non-cascade); 409 with resource-specific + * code if either references. */ - get: operations['get_cluster_ubi_readiness_api_v1_clusters__cluster_id__ubi_readiness_get']; - put?: never; - post?: never; - delete?: never; + delete: operations['delete_test_query_set_api_v1__test_query_sets__query_set_id__delete']; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/clusters/{cluster_id}/targets': { + '/api/v1/_test/query-templates/{template_id}': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; + post?: never; /** - * List Cluster Targets - * @description List targets (indices/collections) on the cluster (FR-1 / AC-1). - * - * Thin passthrough to ``ElasticAdapter.list_targets()`` (which filters out - * system indices whose names start with ``.``). Mirrors the ``get_cluster_schema`` - * pattern: ``get_cluster`` → ``acquire_adapter`` async context → adapter call - * → translate exceptions via the ``_err()`` helper to the spec §7.5 envelope. + * Hard-delete a query_template (test-only) + * @description FR-6 — hard-delete the query_template row. * - * Error mapping: - * * cluster missing or soft-deleted → 404 ``CLUSTER_NOT_FOUND`` (retryable=false) - * * adapter raises ``TargetsForbiddenError`` (ACL 401/403) → 403 - * ``TARGETS_FORBIDDEN`` (retryable=false) — frontend auto-engages manual mode - * * adapter raises ``ClusterUnreachableError`` (5xx / connection failure) → 503 - * ``CLUSTER_UNREACHABLE`` (retryable=true) + * No FK children cascade with template. Preflight-checks ``studies``, + * ``proposals``, and ``judgment_lists.current_template_id`` in + * **fixed priority order: STUDY > PROPOSAL > JUDGMENT_LIST** (per + * spec §FR-6) — first match wins. */ - get: operations['list_cluster_targets_api_v1_clusters__cluster_id__targets_get']; - put?: never; - post?: never; - delete?: never; + delete: operations['delete_test_query_template_api_v1__test_query_templates__template_id__delete']; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/clusters/{cluster_id}/run_query': { + '/api/v1/_test/studies/seed-completed': { parameters: { query?: never; header?: never; @@ -245,43 +202,41 @@ export interface paths { get?: never; put?: never; /** - * Run Query - * @description Execute one query DSL fragment against the cluster (FR-6 / AC-3). + * Seed a completed study + digest + (optional) pending proposal + * @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. */ - post: operations['run_query_api_v1_clusters__cluster_id__run_query_post']; + post: operations['seed_completed_study_api_v1__test_studies_seed_completed_post']; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/clusters/{cluster_id}/targets/{target}/documents': { + '/api/v1/_test/studies/{study_id}': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; + post?: never; /** - * List Target Documents - * @description Paginated _id + truncated _source preview for a target (FR-3). + * Hard-delete a study (test-only) + * @description FR-3 — hard-delete the study row. * - * The endpoint asks the adapter for ``limit + 1`` rows so it can detect - * end-of-data exactly (no extra round-trip). Only the first ``limit`` rows - * are returned; ``next_cursor`` encodes the ES ``hits[i].sort`` of the - * last visible row when ``has_more`` is True. ``X-Total-Count`` header - * carries the engine's ``hits.total.value``. + * Trials cascade-delete via existing FK. Preflight-checks ``proposals`` + * + ``digests`` (both non-cascade); 409 if any dependent rows reference + * the study. */ - get: operations['list_target_documents_api_v1_clusters__cluster_id__targets__target__documents_get']; - put?: never; - post?: never; - delete?: never; + delete: operations['delete_test_study_api_v1__test_studies__study_id__delete']; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/clusters/{cluster_id}/targets/{target}/documents/{doc_id}': { + '/api/v1/clusters': { parameters: { query?: never; header?: never; @@ -289,56 +244,54 @@ export interface paths { cookie?: never; }; /** - * Get Target Document - * @description Fetch one document by ``_id`` (FR-4). + * List Clusters + * @description List clusters with cursor pagination + ``X-Total-Count`` header. * - * FastAPI's ``{doc_id:path}`` converter round-trips slashes verbatim, so - * operator IDs containing ``/`` are supported (D-17 / AC-16). Returns the - * adapter ``Document`` shape directly; on ``found: false`` returns 404 - * ``DOCUMENT_NOT_FOUND`` (distinct from ``TARGET_NOT_FOUND``). + * ``?q=`` is a Postgres FTS match against the cluster's ``search_vector`` + * (name + base_url); 2–200 chars. Filter-only — ordering unchanged per + * spec FR-1. ``?sort=`` is one of the values in + * :data:`~backend.app.api.v1.schemas.ClusterSortKey`; the cursor is + * sort-aware so the keyset predicate matches the active ORDER BY + * (feat_data_table_primitive Stories 1.2 + 1.3). */ - get: operations['get_target_document_api_v1_clusters__cluster_id__targets__target__documents__doc_id__get']; + get: operations['list_clusters_api_v1_clusters_get']; put?: never; - post?: never; + /** + * Create Cluster + * @description Register a cluster (FR-5 / AC-1). + */ + post: operations['create_cluster_api_v1_clusters_post']; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/query-templates': { + '/api/v1/clusters/test-connection': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** - * List Query Templates - * @description List query templates with cursor pagination + X-Total-Count header. - * - * ``?q=`` FTS match (name). ``?sort=`` sort-aware cursor (Story 1.3). - * ``?engine_type=`` filters by engine (Story 1.4). - */ - get: operations['list_query_templates_api_v1_query_templates_get']; + get?: never; put?: never; /** - * Create Query Template - * @description Register a query template (FR-2 + AC-7). + * Test Connection + * @description Probe a cluster config WITHOUT persisting (infra_adapter_solr Story A9). * - * AC-7: a body containing ``{{ os.system('rm -rf /') }}`` surfaces as - * 400 ``INVALID_TEMPLATE_SYNTAX`` (the AST walk catches the ``Call`` - * node before reaching the meta-vars cross-check that would otherwise - * classify ``os`` as ``UndeclaredParamUsed``). + * Powers the registration modal's "Test connection" button. Always 200 — + * transport failures surface as ``reachable=false`` with ``error`` set. + * Invalid engine×auth pairings 400 BEFORE the network call. */ - post: operations['create_query_template_api_v1_query_templates_post']; + post: operations['test_connection_api_v1_clusters_test_connection_post']; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/query-templates/{template_id}': { + '/api/v1/clusters/{cluster_id}': { parameters: { query?: never; header?: never; @@ -346,66 +299,66 @@ export interface paths { cookie?: never; }; /** - * Get Query Template Detail - * @description Return a query template by id. + * Get Cluster Detail + * @description Return cluster row + cached/fresh health probe. */ - get: operations['get_query_template_detail_api_v1_query_templates__template_id__get']; + get: operations['get_cluster_detail_api_v1_clusters__cluster_id__get']; put?: never; post?: never; - delete?: never; + /** + * Delete Cluster + * @description Soft-delete a cluster (AC-8). Returns 204 with no body. + */ + delete: operations['delete_cluster_api_v1_clusters__cluster_id__delete']; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/query-sets': { + '/api/v1/clusters/{cluster_id}/reprobe': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** - * List Query Sets - * @description List query sets with cursor pagination + X-Total-Count. - * - * ``?q=`` is FTS match against ``search_vector`` (name). ``?sort=`` is a - * :data:`QuerySetSortKey` value; cursor is sort-aware. - */ - get: operations['list_query_sets_api_v1_query_sets_get']; + get?: never; put?: never; /** - * Create Query Set - * @description Register a query set under a cluster (FR-3). + * Reprobe Cluster + * @description Re-run cluster capability probe (Story A9 / spec FR-2 + AC-14). + * + * Concurrent calls serialize on ``SELECT … FOR UPDATE``. On probe failure + * the row's engine_config is NOT updated (the transaction rolls back). */ - post: operations['create_query_set_api_v1_query_sets_post']; + post: operations['reprobe_cluster_api_v1_clusters__cluster_id__reprobe_post']; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/query-sets/{query_set_id}': { + '/api/v1/clusters/{cluster_id}/run_query': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Get Query Set Detail - * @description Return a query set by id (includes ``query_count``). + * Run Query + * @description Execute one query DSL fragment against the cluster (FR-6 / AC-3). */ - get: operations['get_query_set_detail_api_v1_query_sets__query_set_id__get']; - put?: never; - post?: never; + post: operations['run_query_api_v1_clusters__cluster_id__run_query_post']; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/query-sets/{query_set_id}/queries': { + '/api/v1/clusters/{cluster_id}/schema': { parameters: { query?: never; header?: never; @@ -413,55 +366,19 @@ export interface paths { cookie?: never; }; /** - * List Queries In Set - * @description List per-query rows under a query set, with derived ``judgment_count``. + * Get Cluster Schema + * @description Return the field schema for ``target`` (FR-4 / AC-2). */ - get: operations['list_queries_in_set_api_v1_query_sets__query_set_id__queries_get']; + get: operations['get_cluster_schema_api_v1_clusters__cluster_id__schema_get']; put?: never; - /** - * Bulk Add Queries - * @description Bulk-add queries to a set (FR-3 + AC-8). - * - * Dispatches on Content-Type: - * - * * ``application/json`` → :class:`BulkQueriesJsonRequest` Pydantic-parse. - * * ``text/csv`` → :func:`parse_queries_csv` (AC-8). - * - * Other content types → 415-equivalent surfaced as 400 ``INVALID_CSV`` - * (the documented error code for content-type-mismatch in spec §7.5). - */ - post: operations['bulk_add_queries_api_v1_query_sets__query_set_id__queries_post']; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/query-sets/{query_set_id}/queries/{query_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** - * Delete Query Endpoint - * @description Hard-delete a query. FK-guarded — 409 if any judgment references it. - */ - delete: operations['delete_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__delete']; - options?: never; - head?: never; - /** - * Update Query Endpoint - * @description Partial-update a query. Whole-object replace on ``query_metadata``. - */ - patch: operations['update_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__patch']; - trace?: never; - }; - '/api/v1/studies': { + '/api/v1/clusters/{cluster_id}/targets': { parameters: { query?: never; header?: never; @@ -469,33 +386,31 @@ export interface paths { cookie?: never; }; /** - * List Studies - * @description List studies with cursor pagination + X-Total-Count. + * List Cluster Targets + * @description List targets (indices/collections) on the cluster (FR-1 / AC-1). * - * ``?status=`` is typed as :data:`StudyStatusWire` so FastAPI returns - * 422 ``VALIDATION_ERROR`` for unsupported values. ``?q=`` is a Postgres - * FTS match against ``search_vector`` (name + target). ``?sort=`` is a - * :data:`StudySortKey` value (``:``); the cursor is - * sort-aware (feat_data_table_primitive Stories 1.2 + 1.3). + * Thin passthrough to ``ElasticAdapter.list_targets()`` (which filters out + * system indices whose names start with ``.``). Mirrors the ``get_cluster_schema`` + * pattern: ``get_cluster`` → ``acquire_adapter`` async context → adapter call + * → translate exceptions via the ``_err()`` helper to the spec §7.5 envelope. * - * ``?target=`` (feat_index_document_browser FR-5) scopes the list to - * studies targeting a single index/collection. Composes with all other - * filters via AND. + * Error mapping: + * * cluster missing or soft-deleted → 404 ``CLUSTER_NOT_FOUND`` (retryable=false) + * * adapter raises ``TargetsForbiddenError`` (ACL 401/403) → 403 + * ``TARGETS_FORBIDDEN`` (retryable=false) — frontend auto-engages manual mode + * * adapter raises ``ClusterUnreachableError`` (5xx / connection failure) → 503 + * ``CLUSTER_UNREACHABLE`` (retryable=true) */ - get: operations['list_studies_api_v1_studies_get']; + get: operations['list_cluster_targets_api_v1_clusters__cluster_id__targets_get']; put?: never; - /** - * Create Study - * @description Create a study (FR-1 + AC-1) and enqueue the orchestrator job. - */ - post: operations['create_study_api_v1_studies_post']; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/studies/{study_id}': { + '/api/v1/clusters/{cluster_id}/targets/{target}/documents': { parameters: { query?: never; header?: never; @@ -503,10 +418,16 @@ export interface paths { cookie?: never; }; /** - * Get Study Detail - * @description Return a study by id (includes ``trials_summary``). + * List Target Documents + * @description Paginated _id + truncated _source preview for a target (FR-3). + * + * The endpoint asks the adapter for ``limit + 1`` rows so it can detect + * end-of-data exactly (no extra round-trip). Only the first ``limit`` rows + * are returned; ``next_cursor`` encodes the ES ``hits[i].sort`` of the + * last visible row when ``has_more`` is True. ``X-Total-Count`` header + * carries the engine's ``hits.total.value``. */ - get: operations['get_study_detail_api_v1_studies__study_id__get']; + get: operations['list_target_documents_api_v1_clusters__cluster_id__targets__target__documents_get']; put?: never; post?: never; delete?: never; @@ -515,40 +436,32 @@ export interface paths { patch?: never; trace?: never; }; - '/api/v1/studies/{study_id}/cancel': { + '/api/v1/clusters/{cluster_id}/targets/{target}/documents/{doc_id}': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Cancel Study - * @description Cancel a study (Story 2.3, FR-8 + AC-8/AC-9). - * - * Optionally cascades to in-flight chain children. - * - * ``?cascade=true`` (default): routes through - * :func:`services.study_state.cancel_study_with_chain_cascade` — - * cancels the parent (if in-flight) AND recursively cancels in-flight - * descendants. Tolerates terminal parents (recurses through completed - * intermediates to reach an in-flight grandchild). + * Get Target Document + * @description Fetch one document by ``_id`` (FR-4). * - * ``?cascade=false``: routes through the original - * :func:`services.study_state.cancel_study` — single-study cancel, - * preserves the existing 409 error contract on terminal parents - * (AC-9 wire contract). + * FastAPI's ``{doc_id:path}`` converter round-trips slashes verbatim, so + * operator IDs containing ``/`` are supported (D-17 / AC-16). Returns the + * adapter ``Document`` shape directly; on ``found: false`` returns 404 + * ``DOCUMENT_NOT_FOUND`` (distinct from ``TARGET_NOT_FOUND``). */ - post: operations['cancel_study_api_v1_studies__study_id__cancel_post']; + get: operations['get_target_document_api_v1_clusters__cluster_id__targets__target__documents__doc_id__get']; + put?: never; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/studies/{study_id}/children': { + '/api/v1/clusters/{cluster_id}/ubi-readiness': { parameters: { query?: never; header?: never; @@ -556,18 +469,27 @@ export interface paths { cookie?: never; }; /** - * List Study Children - * @description List direct child studies of a parent (FR-10 + D-13). + * Get Cluster Ubi Readiness + * @description Classify ``(cluster, query_set, target)`` on the UBI rung ladder. * - * Returns ``{"data": [], "next_cursor": null}`` for a study with no - * children — empty data array, NOT 404. 404 only fires when the parent - * study itself is missing. + * feat_ubi_judgments FR-7. * - * Per D-13 (direct-children-only): does NOT return transitive - * descendants. The chain panel renders parent ↑ + direct children ↓; - * operators walk lineage one hop per page navigation. + * Required query params: ``query_set_id`` + ``target`` (Spec FR-7 + + * cycle-3 D-10c: the endpoint MUST 422 without them — the classifier + * can't compute a per-target rung without an application filter). + * + * Error envelopes (all per spec §7.5): + * * ``404 CLUSTER_NOT_FOUND`` — cluster row missing or soft-deleted. + * * ``404 QUERY_SET_NOT_FOUND`` — query set row missing. + * * ``422 VALIDATION_ERROR`` — missing required query params (FastAPI's + * built-in handler, surfaces via ``api/errors.py``). + * * ``503 CLUSTER_UNREACHABLE`` — adapter cannot reach the cluster. + * + * The result is cached for 60 s in Redis per + * ``(cluster_id, query_set_id, target)`` so back-to-back dialog-open + * and dialog-submit calls don't re-probe. */ - get: operations['list_study_children_api_v1_studies__study_id__children_get']; + get: operations['get_cluster_ubi_readiness_api_v1_clusters__cluster_id__ubi_readiness_get']; put?: never; post?: never; delete?: never; @@ -576,7 +498,7 @@ export interface paths { patch?: never; trace?: never; }; - '/api/v1/studies/{study_id}/trials': { + '/api/v1/config-repos': { parameters: { query?: never; header?: never; @@ -584,23 +506,38 @@ export interface paths { cookie?: never; }; /** - * List Study Trials - * @description List trials in a study (FR-6). - * - * Sort variants per spec §7.4: ``primary_metric_desc`` (default), - * ``primary_metric_asc``, ``ended_at_desc``, ``ended_at_asc``, - * ``optuna_trial_number_asc``. + * List Config Repos Endpoint + * @description Cursor-paginated config-repo list, newest first. */ - get: operations['list_study_trials_api_v1_studies__study_id__trials_get']; + get: operations['list_config_repos_endpoint_api_v1_config_repos_get']; put?: never; - post?: never; + /** + * Create Config Repo Endpoint + * @description Register a new config repo. ``provider`` is server-derived from ``repo_url``. + * + * Preflight order matches spec FR-3: + * + * 1. ``validate_repo_url(repo_url)`` → 400 ``UNSUPPORTED_PROVIDER`` for + * non-GitHub URLs (AC-8). GitLab + Bitbucket arrive at MVP3. + * 2. ``./secrets/{auth_ref}`` must exist → else 400 ``AUTH_REF_NOT_FOUND`` + * (AC-9). The contents check defers to the worker — operators may + * populate the file between registration and first PR-open. + * 3. ``name`` uniqueness check → 409 ``CONFIG_REPO_NAME_TAKEN`` on collision. + * 4. Insert with server-derived ``provider="github"``. + * 5. **feat_github_webhook Story 4.2** — when ``webhook_secret_ref`` is + * populated, best-effort enqueue ``register_webhook`` against the + * newly created config_repo id. Enqueue failure (Redis down, pool + * absent, transient blip) does NOT break the 201 — it logs WARN + * and the operator drives recovery via the runbook. + */ + post: operations['create_config_repo_endpoint_api_v1_config_repos_post']; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/studies/{study_id}/chain': { + '/api/v1/config-repos/{config_repo_id}': { parameters: { query?: never; header?: never; @@ -608,15 +545,19 @@ export interface paths { cookie?: never; }; /** - * Get Study Chain - * @description Return the rolled-up chain summary for the study and its lineage (FR-3). + * Get Config Repo Endpoint + * @description Detail by id; 404 ``CONFIG_REPO_NOT_FOUND`` if missing. * - * Walks to the chain anchor, aggregates the completed-link subset into a - * best link + cumulative lift + derived stop reason, and emits per-link - * deltas. The anchor's ``delta_from_prev`` is always ``None`` (spec §8.3). - * Returns ``404 STUDY_NOT_FOUND`` when the study does not exist. + * feat_config_repo_baseline_tracking FR-4 — when + * ``last_merged_proposal_id`` is set, embed the pointed-at proposal as a + * :class:`ProposalSummary` with ``is_currently_live=True``. The embed-side + * derivation uses the pointer context directly (NOT the generic + * ``proposals → clusters → config_repos`` JOIN used elsewhere) so the + * badge renders correctly even when the proposal's cluster was later + * unwired from this config_repo (spec §19 "Cluster-with-config_repo- + * rotated" decision-log entry). */ - get: operations['get_study_chain_api_v1_studies__study_id__chain_get']; + get: operations['get_config_repo_endpoint_api_v1_config_repos__config_repo_id__get']; put?: never; post?: never; delete?: never; @@ -625,62 +566,59 @@ export interface paths { patch?: never; trace?: never; }; - '/api/v1/judgments/generate': { + '/api/v1/conversations': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Generate Judgments - * @description Create a judgment_lists row + enqueue the worker. + * List Conversations Endpoint + * @description List conversations newest-first with per-row message_count + X-Total-Count header. * - * Delegates the full preflight + INSERT + Arq enqueue to - * :func:`backend.app.services.agent_judgments_dispatch.start_judgment_generation` - * so the chat-agent ``generate_judgments_llm`` tool reuses the exact same - * checks (no duplicated preflight). Wire behavior is identical — same error - * codes, same status codes, same response shape. + * ``?since=`` (Story 1.5 — closes api-conventions.md drift) filters by + * ``created_at >= since``. ``?q=`` (Story 1.2) is a Postgres FTS match + * against ``search_vector`` (coalesce(title, '')); 2-200 chars. */ - post: operations['generate_judgments_api_v1_judgments_generate_post']; + get: operations['list_conversations_endpoint_api_v1_conversations_get']; + put?: never; + /** + * Create Conversation Endpoint + * @description Create a new conversation. Title is optional (FR-1 auto-generates from first message). + */ + post: operations['create_conversation_endpoint_api_v1_conversations_post']; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/judgments/generate-from-ubi': { + '/api/v1/conversations/{conversation_id}': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; + /** + * Get Conversation Endpoint + * @description Return the conversation's full message history. + */ + get: operations['get_conversation_endpoint_api_v1_conversations__conversation_id__get']; put?: never; + post?: never; /** - * Generate Judgments From Ubi - * @description Start a UBI-derived judgment generation job. - * - * Delegates to - * :func:`backend.app.services.agent_judgments_dispatch.start_ubi_judgment_generation` - * which runs the full FR-4 preflight (U-A..U-H) before INSERT + Arq - * enqueue. The Pydantic ``model_validator`` on - * :class:`CreateJudgmentListFromUbiRequest` already enforces the - * hybrid conditional (``current_template_id`` + ``rubric`` required - * iff ``converter == 'hybrid_ubi_llm'``); the dispatcher trusts the - * validated request. + * Delete Conversation Endpoint + * @description Soft-delete the conversation; subsequent reads return 404. */ - post: operations['generate_judgments_from_ubi_api_v1_judgments_generate_from_ubi_post']; - delete?: never; + delete: operations['delete_conversation_endpoint_api_v1_conversations__conversation_id__delete']; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/judgment-lists/import': { + '/api/v1/conversations/{conversation_id}/messages': { parameters: { query?: never; header?: never; @@ -690,14 +628,18 @@ export interface paths { get?: never; put?: never; /** - * Import Judgment List - * @description Create a judgment_lists row with status='complete' + bulk-insert judgments. + * Post Message Endpoint + * @description Send a user message and stream the assistant turn as SSE. * - * Tutorial path; no OpenAI involvement. Every supplied judgment must - * reference a ``query_id`` that exists in ``body.query_set_id`` — - * mismatches → 400 ``QUERY_NOT_IN_SET``. + * Preflight (in order; returns plain JSON envelope, NOT a partial stream): + * A. Conversation exists → else 404 ``CONVERSATION_NOT_FOUND``. + * B. ``Settings.openai_api_key`` populated → else 503 ``OPENAI_NOT_CONFIGURED``. + * C. Daily budget peek under cap → else 503 ``OPENAI_BUDGET_EXCEEDED``. + * + * Successful preflight returns a ``StreamingResponse(text/event-stream)`` + * driven by :func:`agent_chat.send_user_message`. */ - post: operations['import_judgment_list_api_v1_judgment_lists_import_post']; + post: operations['post_message_endpoint_api_v1_conversations__conversation_id__messages_post']; delete?: never; options?: never; head?: never; @@ -744,6 +686,30 @@ export interface paths { patch?: never; trace?: never; }; + '/api/v1/judgment-lists/import': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Import Judgment List + * @description Create a judgment_lists row with status='complete' + bulk-insert judgments. + * + * Tutorial path; no OpenAI involvement. Every supplied judgment must + * reference a ``query_id`` that exists in ``body.query_set_id`` — + * mismatches → 400 ``QUERY_NOT_IN_SET``. + */ + post: operations['import_judgment_list_api_v1_judgment_lists_import_post']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/api/v1/judgment-lists/{judgment_list_id}': { parameters: { query?: never; @@ -761,6 +727,30 @@ export interface paths { patch?: never; trace?: never; }; + '/api/v1/judgment-lists/{judgment_list_id}/calibration': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Calibrate Judgment List + * @description Compute Cohen's + weighted kappa from supplied human samples. + * + * Pairs are built by joining each sample with the existing + * ``source='llm'`` judgment at ``(query_id, doc_id)`` — overridden rows + * (``source='human'``) are excluded (per spec FR-5 + GPT-5.5 cycle 1 F12). + */ + post: operations['calibrate_judgment_list_api_v1_judgment_lists__judgment_list_id__calibration_post']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/api/v1/judgment-lists/{judgment_list_id}/judgments': { parameters: { query?: never; @@ -804,7 +794,7 @@ export interface paths { patch: operations['override_judgment_api_v1_judgment_lists__judgment_list_id__judgments__judgment_id__patch']; trace?: never; }; - '/api/v1/judgment-lists/{judgment_list_id}/calibration': { + '/api/v1/judgments/generate': { parameters: { query?: never; header?: never; @@ -814,40 +804,45 @@ export interface paths { get?: never; put?: never; /** - * Calibrate Judgment List - * @description Compute Cohen's + weighted kappa from supplied human samples. + * Generate Judgments + * @description Create a judgment_lists row + enqueue the worker. * - * Pairs are built by joining each sample with the existing - * ``source='llm'`` judgment at ``(query_id, doc_id)`` — overridden rows - * (``source='human'``) are excluded (per spec FR-5 + GPT-5.5 cycle 1 F12). + * Delegates the full preflight + INSERT + Arq enqueue to + * :func:`backend.app.services.agent_judgments_dispatch.start_judgment_generation` + * so the chat-agent ``generate_judgments_llm`` tool reuses the exact same + * checks (no duplicated preflight). Wire behavior is identical — same error + * codes, same status codes, same response shape. */ - post: operations['calibrate_judgment_list_api_v1_judgment_lists__judgment_list_id__calibration_post']; + post: operations['generate_judgments_api_v1_judgments_generate_post']; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/studies/{study_id}/digest': { + '/api/v1/judgments/generate-from-ubi': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Get Study Digest - * @description Fetch the digest for a completed study. + * Generate Judgments From Ubi + * @description Start a UBI-derived judgment generation job. * - * Returns 404 ``DIGEST_NOT_READY`` (``retryable=true``) when: - * - the study is not in ``status='completed'``, OR - * - the study is completed but the worker hasn't written the digest yet - * (worker lag, or a worker-side terminal failure like - * ``OPENAI_NOT_CONFIGURED`` deferred the run). + * Delegates to + * :func:`backend.app.services.agent_judgments_dispatch.start_ubi_judgment_generation` + * which runs the full FR-4 preflight (U-A..U-H) before INSERT + Arq + * enqueue. The Pydantic ``model_validator`` on + * :class:`CreateJudgmentListFromUbiRequest` already enforces the + * hybrid conditional (``current_template_id`` + ``rubric`` required + * iff ``converter == 'hybrid_ubi_llm'``); the dispatcher trusts the + * validated request. */ - get: operations['get_study_digest_api_v1_studies__study_id__digest_get']; - put?: never; - post?: never; + post: operations['generate_judgments_from_ubi_api_v1_judgments_generate_from_ubi_post']; delete?: never; options?: never; head?: never; @@ -904,26 +899,6 @@ export interface paths { patch?: never; trace?: never; }; - '/api/v1/proposals/{proposal_id}/reject': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Reject Proposal Endpoint - * @description AC-5: ``pending → rejected`` transition; 409 INVALID_STATE_TRANSITION otherwise. - */ - post: operations['reject_proposal_endpoint_api_v1_proposals__proposal_id__reject_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; '/api/v1/proposals/{proposal_id}/open_pr': { parameters: { query?: never; @@ -949,46 +924,27 @@ export interface paths { patch?: never; trace?: never; }; - '/api/v1/config-repos': { + '/api/v1/proposals/{proposal_id}/reject': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** - * List Config Repos Endpoint - * @description Cursor-paginated config-repo list, newest first. - */ - get: operations['list_config_repos_endpoint_api_v1_config_repos_get']; + get?: never; put?: never; /** - * Create Config Repo Endpoint - * @description Register a new config repo. ``provider`` is server-derived from ``repo_url``. - * - * Preflight order matches spec FR-3: - * - * 1. ``validate_repo_url(repo_url)`` → 400 ``UNSUPPORTED_PROVIDER`` for - * non-GitHub URLs (AC-8). GitLab + Bitbucket arrive at MVP3. - * 2. ``./secrets/{auth_ref}`` must exist → else 400 ``AUTH_REF_NOT_FOUND`` - * (AC-9). The contents check defers to the worker — operators may - * populate the file between registration and first PR-open. - * 3. ``name`` uniqueness check → 409 ``CONFIG_REPO_NAME_TAKEN`` on collision. - * 4. Insert with server-derived ``provider="github"``. - * 5. **feat_github_webhook Story 4.2** — when ``webhook_secret_ref`` is - * populated, best-effort enqueue ``register_webhook`` against the - * newly created config_repo id. Enqueue failure (Redis down, pool - * absent, transient blip) does NOT break the 201 — it logs WARN - * and the operator drives recovery via the runbook. + * Reject Proposal Endpoint + * @description AC-5: ``pending → rejected`` transition; 409 INVALID_STATE_TRANSITION otherwise. */ - post: operations['create_config_repo_endpoint_api_v1_config_repos_post']; + post: operations['reject_proposal_endpoint_api_v1_proposals__proposal_id__reject_post']; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/config-repos/{config_repo_id}': { + '/api/v1/query-sets': { parameters: { query?: never; header?: never; @@ -996,28 +952,26 @@ export interface paths { cookie?: never; }; /** - * Get Config Repo Endpoint - * @description Detail by id; 404 ``CONFIG_REPO_NOT_FOUND`` if missing. + * List Query Sets + * @description List query sets with cursor pagination + X-Total-Count. * - * feat_config_repo_baseline_tracking FR-4 — when - * ``last_merged_proposal_id`` is set, embed the pointed-at proposal as a - * :class:`ProposalSummary` with ``is_currently_live=True``. The embed-side - * derivation uses the pointer context directly (NOT the generic - * ``proposals → clusters → config_repos`` JOIN used elsewhere) so the - * badge renders correctly even when the proposal's cluster was later - * unwired from this config_repo (spec §19 "Cluster-with-config_repo- - * rotated" decision-log entry). + * ``?q=`` is FTS match against ``search_vector`` (name). ``?sort=`` is a + * :data:`QuerySetSortKey` value; cursor is sort-aware. */ - get: operations['get_config_repo_endpoint_api_v1_config_repos__config_repo_id__get']; + get: operations['list_query_sets_api_v1_query_sets_get']; put?: never; - post?: never; + /** + * Create Query Set + * @description Register a query set under a cluster (FR-3). + */ + post: operations['create_query_set_api_v1_query_sets_post']; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/conversations': { + '/api/v1/query-sets/{query_set_id}': { parameters: { query?: never; header?: never; @@ -1025,27 +979,19 @@ export interface paths { cookie?: never; }; /** - * List Conversations Endpoint - * @description List conversations newest-first with per-row message_count + X-Total-Count header. - * - * ``?since=`` (Story 1.5 — closes api-conventions.md drift) filters by - * ``created_at >= since``. ``?q=`` (Story 1.2) is a Postgres FTS match - * against ``search_vector`` (coalesce(title, '')); 2-200 chars. + * Get Query Set Detail + * @description Return a query set by id (includes ``query_count``). */ - get: operations['list_conversations_endpoint_api_v1_conversations_get']; + get: operations['get_query_set_detail_api_v1_query_sets__query_set_id__get']; put?: never; - /** - * Create Conversation Endpoint - * @description Create a new conversation. Title is optional (FR-1 auto-generates from first message). - */ - post: operations['create_conversation_endpoint_api_v1_conversations_post']; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/conversations/{conversation_id}': { + '/api/v1/query-sets/{query_set_id}/queries': { parameters: { query?: never; header?: never; @@ -1053,23 +999,31 @@ export interface paths { cookie?: never; }; /** - * Get Conversation Endpoint - * @description Return the conversation's full message history. + * List Queries In Set + * @description List per-query rows under a query set, with derived ``judgment_count``. */ - get: operations['get_conversation_endpoint_api_v1_conversations__conversation_id__get']; + get: operations['list_queries_in_set_api_v1_query_sets__query_set_id__queries_get']; put?: never; - post?: never; /** - * Delete Conversation Endpoint - * @description Soft-delete the conversation; subsequent reads return 404. + * Bulk Add Queries + * @description Bulk-add queries to a set (FR-3 + AC-8). + * + * Dispatches on Content-Type: + * + * * ``application/json`` → :class:`BulkQueriesJsonRequest` Pydantic-parse. + * * ``text/csv`` → :func:`parse_queries_csv` (AC-8). + * + * Other content types → 415-equivalent surfaced as 400 ``INVALID_CSV`` + * (the documented error code for content-type-mismatch in spec §7.5). */ - delete: operations['delete_conversation_endpoint_api_v1_conversations__conversation_id__delete']; + post: operations['bulk_add_queries_api_v1_query_sets__query_set_id__queries_post']; + delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/conversations/{conversation_id}/messages': { + '/api/v1/query-sets/{query_set_id}/queries/{query_id}': { parameters: { query?: never; header?: never; @@ -1078,106 +1032,128 @@ export interface paths { }; get?: never; put?: never; - /** - * Post Message Endpoint - * @description Send a user message and stream the assistant turn as SSE. - * - * Preflight (in order; returns plain JSON envelope, NOT a partial stream): - * A. Conversation exists → else 404 ``CONVERSATION_NOT_FOUND``. - * B. ``Settings.openai_api_key`` populated → else 503 ``OPENAI_NOT_CONFIGURED``. - * C. Daily budget peek under cap → else 503 ``OPENAI_BUDGET_EXCEEDED``. - * - * Successful preflight returns a ``StreamingResponse(text/event-stream)`` - * driven by :func:`agent_chat.send_user_message`. + post?: never; + /** + * Delete Query Endpoint + * @description Hard-delete a query. FK-guarded — 409 if any judgment references it. */ - post: operations['post_message_endpoint_api_v1_conversations__conversation_id__messages_post']; - delete?: never; + delete: operations['delete_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__delete']; options?: never; head?: never; - patch?: never; + /** + * Update Query Endpoint + * @description Partial-update a query. Whole-object replace on ``query_metadata``. + */ + patch: operations['update_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__patch']; trace?: never; }; - '/api/v1/_test/studies/seed-completed': { + '/api/v1/query-templates': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; + /** + * List Query Templates + * @description List query templates with cursor pagination + X-Total-Count header. + * + * ``?q=`` FTS match (name). ``?sort=`` sort-aware cursor (Story 1.3). + * ``?engine_type=`` filters by engine (Story 1.4). + */ + get: operations['list_query_templates_api_v1_query_templates_get']; put?: never; /** - * Seed a completed study + digest + (optional) pending proposal - * @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. + * Create Query Template + * @description Register a query template (FR-2 + AC-7). + * + * AC-7: a body containing ``{{ os.system('rm -rf /') }}`` surfaces as + * 400 ``INVALID_TEMPLATE_SYNTAX`` (the AST walk catches the ``Call`` + * node before reaching the meta-vars cross-check that would otherwise + * classify ``os`` as ``UndeclaredParamUsed``). */ - post: operations['seed_completed_study_api_v1__test_studies_seed_completed_post']; + post: operations['create_query_template_api_v1_query_templates_post']; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/_test/auto-followup/seed-chain': { + '/api/v1/query-templates/{template_id}': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Seed an auto-followup chain of N+1 linked studies - * @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. + * Get Query Template Detail + * @description Return a query template by id. */ - post: operations['seed_auto_followup_chain_endpoint_api_v1__test_auto_followup_seed_chain_post']; + get: operations['get_query_template_detail_api_v1_query_templates__template_id__get']; + put?: never; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/_test/proposals/{proposal_id}': { + '/api/v1/studies': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; + /** + * List Studies + * @description List studies with cursor pagination + X-Total-Count. + * + * ``?status=`` is typed as :data:`StudyStatusWire` so FastAPI returns + * 422 ``VALIDATION_ERROR`` for unsupported values. ``?q=`` is a Postgres + * FTS match against ``search_vector`` (name + target). ``?sort=`` is a + * :data:`StudySortKey` value (``:``); the cursor is + * sort-aware (feat_data_table_primitive Stories 1.2 + 1.3). + * + * ``?target=`` (feat_index_document_browser FR-5) scopes the list to + * studies targeting a single index/collection. Composes with all other + * filters via AND. + */ + get: operations['list_studies_api_v1_studies_get']; put?: never; - post?: never; /** - * Hard-delete a proposal (test-only) - * @description FR-1: Hard-delete the proposal row. No FK children — no preflight needed. + * Create Study + * @description Create a study (FR-1 + AC-1) and enqueue the orchestrator job. */ - delete: operations['delete_test_proposal_api_v1__test_proposals__proposal_id__delete']; + post: operations['create_study_api_v1_studies_post']; + delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/_test/digests/{digest_id}': { + '/api/v1/studies/{study_id}': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; - post?: never; /** - * Hard-delete a digest (test-only) - * @description FR-2: Hard-delete the digest row. No FK children — no preflight needed. + * Get Study Detail + * @description Return a study by id (includes ``trials_summary``). */ - delete: operations['delete_test_digest_api_v1__test_digests__digest_id__delete']; + get: operations['get_study_detail_api_v1_studies__study_id__get']; + put?: never; + post?: never; + delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/_test/studies/{study_id}': { + '/api/v1/studies/{study_id}/cancel': { parameters: { query?: never; header?: never; @@ -1186,116 +1162,134 @@ export interface paths { }; get?: never; put?: never; - post?: never; /** - * Hard-delete a study (test-only) - * @description FR-3 — hard-delete the study row. + * Cancel Study + * @description Cancel a study (Story 2.3, FR-8 + AC-8/AC-9). * - * Trials cascade-delete via existing FK. Preflight-checks ``proposals`` - * + ``digests`` (both non-cascade); 409 if any dependent rows reference - * the study. + * Optionally cascades to in-flight chain children. + * + * ``?cascade=true`` (default): routes through + * :func:`services.study_state.cancel_study_with_chain_cascade` — + * cancels the parent (if in-flight) AND recursively cancels in-flight + * descendants. Tolerates terminal parents (recurses through completed + * intermediates to reach an in-flight grandchild). + * + * ``?cascade=false``: routes through the original + * :func:`services.study_state.cancel_study` — single-study cancel, + * preserves the existing 409 error contract on terminal parents + * (AC-9 wire contract). */ - delete: operations['delete_test_study_api_v1__test_studies__study_id__delete']; + post: operations['cancel_study_api_v1_studies__study_id__cancel_post']; + delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/_test/judgment-lists/{judgment_list_id}': { + '/api/v1/studies/{study_id}/chain': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; - post?: never; /** - * Hard-delete a judgment_list (test-only) - * @description FR-4 — hard-delete the judgment_list row. + * Get Study Chain + * @description Return the rolled-up chain summary for the study and its lineage (FR-3). * - * Judgments cascade-delete via existing FK. Preflight-checks ``studies`` - * (non-cascade); 409 if any study references the judgment_list. + * Walks to the chain anchor, aggregates the completed-link subset into a + * best link + cumulative lift + derived stop reason, and emits per-link + * deltas. The anchor's ``delta_from_prev`` is always ``None`` (spec §8.3). + * Returns ``404 STUDY_NOT_FOUND`` when the study does not exist. */ - delete: operations['delete_test_judgment_list_api_v1__test_judgment_lists__judgment_list_id__delete']; + get: operations['get_study_chain_api_v1_studies__study_id__chain_get']; + put?: never; + post?: never; + delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/_test/query-sets/{query_set_id}': { + '/api/v1/studies/{study_id}/children': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; - post?: never; /** - * Hard-delete a query_set (test-only) - * @description FR-5 — hard-delete the query_set row. + * List Study Children + * @description List direct child studies of a parent (FR-10 + D-13). * - * Queries cascade-delete via existing FK. Preflight-checks ``studies`` - * + ``judgment_lists`` (both non-cascade); 409 with resource-specific - * code if either references. + * Returns ``{"data": [], "next_cursor": null}`` for a study with no + * children — empty data array, NOT 404. 404 only fires when the parent + * study itself is missing. + * + * Per D-13 (direct-children-only): does NOT return transitive + * descendants. The chain panel renders parent ↑ + direct children ↓; + * operators walk lineage one hop per page navigation. */ - delete: operations['delete_test_query_set_api_v1__test_query_sets__query_set_id__delete']; + get: operations['list_study_children_api_v1_studies__study_id__children_get']; + put?: never; + post?: never; + delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/_test/query-templates/{template_id}': { + '/api/v1/studies/{study_id}/digest': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; - post?: never; /** - * Hard-delete a query_template (test-only) - * @description FR-6 — hard-delete the query_template row. + * Get Study Digest + * @description Fetch the digest for a completed study. * - * No FK children cascade with template. Preflight-checks ``studies``, - * ``proposals``, and ``judgment_lists.current_template_id`` in - * **fixed priority order: STUDY > PROPOSAL > JUDGMENT_LIST** (per - * spec §FR-6) — first match wins. + * Returns 404 ``DIGEST_NOT_READY`` (``retryable=true``) when: + * - the study is not in ``status='completed'``, OR + * - the study is completed but the worker hasn't written the digest yet + * (worker lag, or a worker-side terminal failure like + * ``OPENAI_NOT_CONFIGURED`` deferred the run). */ - delete: operations['delete_test_query_template_api_v1__test_query_templates__template_id__delete']; + get: operations['get_study_digest_api_v1_studies__study_id__digest_get']; + put?: never; + post?: never; + delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/_test/demo/reseed': { + '/api/v1/studies/{study_id}/trials': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Enqueue a demo-state reseed (dev-only, async) - * @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. + * List Study Trials + * @description List trials in a study (FR-6). * - * Per ``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. + * Sort variants per spec §7.4: ``primary_metric_desc`` (default), + * ``primary_metric_asc``, ``ended_at_desc``, ``ended_at_asc``, + * ``optuna_trial_number_asc``. */ - post: operations['reseed_demo_api_v1__test_demo_reseed_post']; + get: operations['list_study_trials_api_v1_studies__study_id__trials_get']; + put?: never; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/_test/demo/reseed/status': { + '/healthz': { parameters: { query?: never; header?: never; @@ -1303,10 +1297,19 @@ export interface paths { cookie?: never; }; /** - * Poll the current demo-reseed progress (dev-only) - * @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. + * Healthz + * @description Probe each subsystem in parallel and return the documented JSON shape. + * + * Args: + * settings: Application settings (DB URL, ES/OS URLs, OpenAI base URL, etc.) + * redis_client: Redis client for ping probe + capability-cache read + * es_client: shared httpx client for ES + OpenSearch HTTP probes + * db: Async DB session for the registered-clusters aggregate (Story 3.5) + * + * Returns: + * JSONResponse with the HealthResponse body and HTTP 200 (healthy) or 503 (degraded). */ - get: operations['reseed_demo_status_api_v1__test_demo_reseed_status_get']; + get: operations['healthz_healthz_get']; put?: never; post?: never; delete?: never; @@ -1361,10 +1364,10 @@ export interface components { * @description Bootstrap percentile CI on the winner's per-query metric values. */ CIShape: { - /** Low */ - low: number; /** High */ high: number; + /** Low */ + low: number; /** * Method * @constant @@ -1383,26 +1386,26 @@ export interface components { CalibrationResponse: { /** Cohens Kappa */ cohens_kappa: number | null; - /** Weighted Kappa */ - weighted_kappa: number | null; + /** N Samples */ + n_samples: number; /** Per Class */ per_class: { [key: string]: number; }; - /** N Samples */ - n_samples: number; /** Warning */ warning: string | null; + /** Weighted Kappa */ + weighted_kappa: number | null; }; /** * CalibrationSample * @description One row in :class:`CalibrationSamplesRequest`. */ CalibrationSample: { - /** Query Id */ - query_id: string; /** Doc Id */ doc_id: string; + /** Query Id */ + query_id: string; /** * Rating * @enum {integer} @@ -1425,13 +1428,13 @@ export interface components { * as choices. */ CategoricalParam: { + /** Choices */ + choices: (string | number | boolean)[]; /** * @description discriminator enum property added by openapi-typescript * @enum {string} */ type: 'categorical'; - /** Choices */ - choices: (string | number | boolean)[]; }; /** * ClusterAggregateHealth @@ -1444,10 +1447,10 @@ export interface components { * ``unreachable``. */ ClusterAggregateHealth: { - /** Registered */ - registered: number; /** Healthy */ healthy: number; + /** Registered */ + registered: number; /** Unreachable */ unreachable: number; }; @@ -1456,22 +1459,6 @@ export interface components { * @description ``GET /api/v1/clusters/{id}`` response. */ ClusterDetail: { - /** Id */ - id: string; - /** Name */ - name: string; - /** - * Engine Type - * @enum {string} - */ - engine_type: 'elasticsearch' | 'opensearch' | 'solr'; - /** - * Environment - * @enum {string} - */ - environment: 'prod' | 'staging' | 'dev'; - /** Base Url */ - base_url: string; /** * Auth Kind * @enum {string} @@ -1483,20 +1470,36 @@ export interface components { | 'opensearch_sigv4' | 'solr_basic' | 'solr_apikey'; + /** Base Url */ + base_url: string; + /** + * Created At + * Format: date-time + */ + created_at: string; /** Engine Config */ engine_config?: { [key: string]: unknown; } | null; + /** + * Engine Type + * @enum {string} + */ + engine_type: 'elasticsearch' | 'opensearch' | 'solr'; + /** + * Environment + * @enum {string} + */ + environment: 'prod' | 'staging' | 'dev'; + health_check: components['schemas']['HealthCheckResult']; + /** Id */ + id: string; + /** Name */ + name: string; /** Notes */ notes?: string | null; /** Target Filter */ target_filter?: string | null; - /** - * Created At - * Format: date-time - */ - created_at: string; - health_check: components['schemas']['HealthCheckResult']; }; /** * ClusterListResponse @@ -1505,32 +1508,16 @@ export interface components { ClusterListResponse: { /** Data */ data: components['schemas']['ClusterSummary'][]; - /** Next Cursor */ - next_cursor: string | null; /** Has More */ has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; }; /** * ClusterSummary * @description List-view; drops engine_config + notes for brevity. */ ClusterSummary: { - /** Id */ - id: string; - /** Name */ - name: string; - /** - * Engine Type - * @enum {string} - */ - engine_type: 'elasticsearch' | 'opensearch' | 'solr'; - /** - * Environment - * @enum {string} - */ - environment: 'prod' | 'staging' | 'dev'; - /** Base Url */ - base_url: string; /** * Auth Kind * @enum {string} @@ -1542,14 +1529,30 @@ export interface components { | 'opensearch_sigv4' | 'solr_basic' | 'solr_apikey'; - /** Target Filter */ - target_filter?: string | null; + /** Base Url */ + base_url: string; /** * Created At * Format: date-time */ created_at: string; + /** + * Engine Type + * @enum {string} + */ + engine_type: 'elasticsearch' | 'opensearch' | 'solr'; + /** + * Environment + * @enum {string} + */ + environment: 'prod' | 'staging' | 'dev'; health_check: components['schemas']['HealthCheckResult']; + /** Id */ + id: string; + /** Name */ + name: string; + /** Target Filter */ + target_filter?: string | null; }; /** * ConfidenceShape @@ -1561,22 +1564,34 @@ export interface components { * row itself is missing). */ ConfidenceShape: { - headline: components['schemas']['HeadlineShape']; ci_95: components['schemas']['CIShape'] | null; - runner_up_gap: components['schemas']['RunnerUpGapShape'] | null; - late_trial_stddev: components['schemas']['LateTrialStddevShape'] | null; convergence: components['schemas']['ConvergenceShape'] | null; + headline: components['schemas']['HeadlineShape']; + late_trial_stddev: components['schemas']['LateTrialStddevShape'] | null; per_query_outcomes: components['schemas']['PerQueryOutcomesShape'] | null; + runner_up_gap: components['schemas']['RunnerUpGapShape'] | null; }; /** * ConfigRepoDetail * @description ``GET /api/v1/config-repos/{id}`` response + ``POST`` 201 body. */ ConfigRepoDetail: { + /** Auth Ref */ + auth_ref: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Default Branch */ + default_branch: string; /** Id */ id: string; + last_merged_proposal?: components['schemas']['ProposalSummary'] | null; /** Name */ name: string; + /** Pr Base Branch */ + pr_base_branch: string; /** * Provider * @constant @@ -1584,22 +1599,10 @@ export interface components { provider: 'github'; /** Repo Url */ repo_url: string; - /** Default Branch */ - default_branch: string; - /** Pr Base Branch */ - pr_base_branch: string; - /** Auth Ref */ - auth_ref: string; - /** Webhook Secret Ref */ - webhook_secret_ref: string | null; /** Webhook Registration Error */ webhook_registration_error: string | null; - last_merged_proposal?: components['schemas']['ProposalSummary'] | null; - /** - * Created At - * Format: date-time - */ - created_at: string; + /** Webhook Secret Ref */ + webhook_secret_ref: string | null; }; /** * ConfigReposListResponse @@ -1608,10 +1611,10 @@ export interface components { ConfigReposListResponse: { /** Data */ data: components['schemas']['ConfigRepoDetail'][]; - /** Next Cursor */ - next_cursor: string | null; /** Has More */ has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; }; /** * ConnectionTestRequest @@ -1624,18 +1627,18 @@ export interface components { * as ``CreateClusterRequest``. */ ConnectionTestRequest: { - /** Engine Type */ - engine_type: string; - /** Base Url */ - base_url: string; /** Auth Kind */ auth_kind: string; + /** Base Url */ + base_url: string; /** Credentials Ref */ credentials_ref: string; /** Engine Config */ engine_config?: { [key: string]: unknown; } | null; + /** Engine Type */ + engine_type: string; }; /** * ConnectionTestResult @@ -1647,6 +1650,12 @@ export interface components { * network call. (Cycle-delta F1.) */ ConnectionTestResult: { + /** Engine Capabilities */ + engine_capabilities?: { + [key: string]: unknown; + } | null; + /** Error */ + error?: string | null; /** Reachable */ reachable: boolean; /** @@ -1656,12 +1665,6 @@ export interface components { status: 'green' | 'yellow' | 'red' | 'unreachable'; /** Version */ version?: string | null; - /** Engine Capabilities */ - engine_capabilities?: { - [key: string]: unknown; - } | null; - /** Error */ - error?: string | null; }; /** * ConvergenceShape @@ -1670,30 +1673,30 @@ export interface components { ConvergenceShape: { /** Best At Trial */ best_at_trial: number; - /** Total Trials */ - total_trials: number; /** * Regime * @enum {string} */ regime: 'early_held' | 'late_rising' | 'noisy'; + /** Total Trials */ + total_trials: number; }; /** * ConversationDetail * @description ``GET /api/v1/conversations/{id}`` response. */ ConversationDetail: { - /** Id */ - id: string; - /** Title */ - title: string | null; /** * Created At * Format: date-time */ created_at: string; + /** Id */ + id: string; /** Messages */ messages: components['schemas']['MessageWire'][]; + /** Title */ + title: string | null; }; /** * ConversationSummary @@ -1711,21 +1714,21 @@ export interface components { * ``created_at``. */ ConversationSummary: { - /** Id */ - id: string; - /** Title */ - title: string | null; /** * Created At * Format: date-time */ created_at: string; - /** Message Count */ - message_count: number; - /** Last Message Preview */ - last_message_preview?: string | null; + /** Id */ + id: string; /** Last Message At */ last_message_at?: string | null; + /** Last Message Preview */ + last_message_preview?: string | null; + /** Message Count */ + message_count: number; + /** Title */ + title: string | null; }; /** * ConversationsListResponse @@ -1734,10 +1737,10 @@ export interface components { ConversationsListResponse: { /** Data */ data: components['schemas']['ConversationSummary'][]; - /** Next Cursor */ - next_cursor: string | null; /** Has More */ has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; }; /** * CreateClusterRequest @@ -1746,25 +1749,25 @@ export interface components { * See module docstring for the deliberate ``str`` vs ``Literal`` split. */ CreateClusterRequest: { - /** Name */ - name: string; - /** Engine Type */ - engine_type: string; - /** - * Environment - * @enum {string} - */ - environment: 'prod' | 'staging' | 'dev'; - /** Base Url */ - base_url: string; /** Auth Kind */ auth_kind: string; + /** Base Url */ + base_url: string; /** Credentials Ref */ credentials_ref: string; /** Engine Config */ engine_config?: { [key: string]: unknown; } | null; + /** Engine Type */ + engine_type: string; + /** + * Environment + * @enum {string} + */ + environment: 'prod' | 'staging' | 'dev'; + /** Name */ + name: string; /** Notes */ notes?: string | null; /** @@ -1783,22 +1786,22 @@ export interface components { * ``UNSUPPORTED_PROVIDER`` at the router layer. */ CreateConfigRepoRequest: { - /** Name */ - name: string; - /** Repo Url */ - repo_url: string; + /** Auth Ref */ + auth_ref: string; /** * Default Branch * @default main */ default_branch: string; + /** Name */ + name: string; /** * Pr Base Branch * @default main */ pr_base_branch: string; - /** Auth Ref */ - auth_ref: string; + /** Repo Url */ + repo_url: string; /** Webhook Secret Ref */ webhook_secret_ref?: string | null; }; @@ -1822,23 +1825,8 @@ export interface components { * the LLM so accepting them silently would mask operator error). */ CreateJudgmentListFromUbiRequest: { - /** Name */ - name: string; - /** Description */ - description?: string | null; - /** Query Set Id */ - query_set_id: string; /** Cluster Id */ cluster_id: string; - /** Target */ - target: string; - /** - * Since - * Format: date-time - */ - since: string; - /** Until */ - until?: string | null; /** * Converter * @enum {string} @@ -1848,46 +1836,61 @@ export interface components { converter_config?: { [key: string]: unknown; } | null; + /** Current Template Id */ + current_template_id?: string | null; + /** Description */ + description?: string | null; /** * Llm Fill Threshold * @default 20 */ llm_fill_threshold: number | null; - /** - * Min Impressions Threshold - * @default 100 - */ - min_impressions_threshold: number | null; /** * Mapping Strategy * @default reject * @enum {string} */ mapping_strategy: 'reject' | 'first_match' | 'most_recent'; - /** Current Template Id */ - current_template_id?: string | null; + /** + * Min Impressions Threshold + * @default 100 + */ + min_impressions_threshold: number | null; + /** Name */ + name: string; + /** Query Set Id */ + query_set_id: string; /** Rubric */ rubric?: string | null; + /** + * Since + * Format: date-time + */ + since: string; + /** Target */ + target: string; + /** Until */ + until?: string | null; }; /** * CreateJudgmentListGenerateRequest * @description Body for ``POST /api/v1/judgments/generate`` (Story 3.1). */ CreateJudgmentListGenerateRequest: { - /** Name */ - name: string; - /** Description */ - description?: string | null; - /** Query Set Id */ - query_set_id: string; /** Cluster Id */ cluster_id: string; - /** Target */ - target: string; /** Current Template Id */ current_template_id: string; + /** Description */ + description?: string | null; + /** Name */ + name: string; + /** Query Set Id */ + query_set_id: string; /** Rubric */ rubric: string; + /** Target */ + target: string; }; /** * CreateProposalRequest @@ -1896,8 +1899,6 @@ export interface components { CreateProposalRequest: { /** Cluster Id */ cluster_id: string; - /** Template Id */ - template_id: string; /** Config Diff */ config_diff: { [key: string]: unknown; @@ -1906,6 +1907,8 @@ export interface components { metric_delta?: { [key: string]: unknown; } | null; + /** Template Id */ + template_id: string; }; /** * CreateQuerySetRequest @@ -1916,32 +1919,32 @@ export interface components { * is documented drift tracked at * ``docs/00_overview/planned_features/chore_spec_query_set_cluster_id_drift/idea.md``. */ - CreateQuerySetRequest: { - /** Name */ - name: string; - /** Description */ - description?: string | null; + CreateQuerySetRequest: { /** Cluster Id */ cluster_id: string; + /** Description */ + description?: string | null; + /** Name */ + name: string; }; /** * CreateQueryTemplateRequest * @description Request body for ``POST /api/v1/query-templates``. */ CreateQueryTemplateRequest: { - /** Name */ - name: string; - /** - * Engine Type - * @enum {string} - */ - engine_type: 'elasticsearch' | 'opensearch' | 'solr'; /** Body */ body: string; /** Declared Params */ declared_params?: { [key: string]: string; }; + /** + * Engine Type + * @enum {string} + */ + engine_type: 'elasticsearch' | 'opensearch' | 'solr'; + /** Name */ + name: string; /** Parent Id */ parent_id?: string | null; }; @@ -1959,30 +1962,30 @@ export interface components { * was spawned from a digest "Run this followup" action (FR-11). */ CreateStudyRequest: { - /** Name */ - name: string; /** Cluster Id */ cluster_id: string; - /** Target */ - target: string; - /** Template Id */ - template_id: string; - /** Query Set Id */ - query_set_id: string; + config: components['schemas']['StudyConfigSpec']; /** Judgment List Id */ judgment_list_id: string; - /** Search Space */ - search_space: { - [key: string]: unknown; - }; + /** Name */ + name: string; objective: components['schemas']['ObjectiveSpec']; - config: components['schemas']['StudyConfigSpec']; parent?: components['schemas']['ParentFollowupRef'] | null; /** * Parent Study Id * @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. */ parent_study_id?: string | null; + /** Query Set Id */ + query_set_id: string; + /** Search Space */ + search_space: { + [key: string]: unknown; + }; + /** Target */ + target: string; + /** Template Id */ + template_id: string; }; /** * CurvePoint @@ -1995,10 +1998,10 @@ export interface components { * earlier trials, sign-corrected to the study's optimization direction. */ CurvePoint: { - /** Trial Number */ - trial_number: number; /** Best So Far */ best_so_far: number; + /** Trial Number */ + trial_number: number; }; /** * DigestResponse @@ -2011,10 +2014,15 @@ export interface components { * malformed JSONB payloads never crash the response. */ DigestResponse: { + /** + * Generated At + * Format: date-time + */ + generated_at: string; + /** Generated By */ + generated_by: string; /** Id */ id: string; - /** Study Id */ - study_id: string; /** Narrative */ narrative: string; /** Parameter Importance */ @@ -2025,15 +2033,10 @@ export interface components { recommended_config: { [key: string]: unknown; }; + /** Study Id */ + study_id: string; /** Suggested Followups */ suggested_followups: components['schemas']['FollowupItem'][]; - /** Generated By */ - generated_by: string; - /** - * Generated At - * Format: date-time - */ - generated_at: string; }; /** * Document @@ -2062,10 +2065,10 @@ export interface components { DocumentListResponse: { /** Data */ data: components['schemas']['DocumentSummary'][]; - /** Next Cursor */ - next_cursor: string | null; /** Has More */ has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; }; /** * DocumentSummary @@ -2088,14 +2091,14 @@ export interface components { * @description One field returned by ``get_schema``. */ FieldSpec: { - /** Name */ - name: string; - /** Type */ - type: string; /** Analyzer */ analyzer?: string | null; /** Doc Count */ doc_count?: number | null; + /** Name */ + name: string; + /** Type */ + type: string; }; /** * FloatParam @@ -2105,13 +2108,6 @@ export interface components { * (Optuna's ``suggest_float(..., log=True)``); requires ``low > 0``. */ FloatParam: { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: 'float'; - /** Low */ - low: number; /** High */ high: number; /** @@ -2119,6 +2115,13 @@ export interface components { * @default false */ log: boolean; + /** Low */ + low: number; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: 'float'; }; FollowupItem: | components['schemas']['NarrowFollowup'] @@ -2158,20 +2161,24 @@ export interface components { * create-study endpoint (``schemas.py:214``). */ HeadlineShape: { - /** Metric */ - metric: string; - /** Value */ - value: number; /** K */ k: number | null; + /** Metric */ + metric: string; /** N Queries */ n_queries: number | null; + /** Value */ + value: number; }; /** * HealthCheckResult * @description Wire shape of the per-cluster health probe (mirrors ``HealthStatus``). */ HealthCheckResult: { + /** Checked At */ + checked_at: string; + /** Error */ + error?: string | null; /** * Status * @enum {string} @@ -2179,16 +2186,18 @@ export interface components { status: 'green' | 'yellow' | 'red' | 'unreachable'; /** Version */ version?: string | null; - /** Checked At */ - checked_at: string; - /** Error */ - error?: string | null; }; /** * HealthResponse * @description The /healthz response body. Same shape for HTTP 200 and 503. */ HealthResponse: { + openai_capabilities: components['schemas']['OpenAICapabilities']; + /** + * Openai Endpoint + * @description Configured OPENAI_BASE_URL + */ + openai_endpoint: string; /** * Status * @enum {string} @@ -2196,73 +2205,67 @@ export interface components { status: 'ok' | 'degraded'; subsystems: components['schemas']['Subsystems']; /** - * Openai Endpoint - * @description Configured OPENAI_BASE_URL + * Uptime Seconds + * @description Seconds since the API process started */ - openai_endpoint: string; - openai_capabilities: components['schemas']['OpenAICapabilities']; + uptime_seconds: number; /** * Version * @description Application version (relyloop_git_sha) */ version: string; - /** - * Uptime Seconds - * @description Seconds since the API process started - */ - uptime_seconds: number; }; /** * ImportJudgmentItem * @description One row in :class:`ImportJudgmentListRequest`. */ ImportJudgmentItem: { - /** Query Id */ - query_id: string; /** Doc Id */ doc_id: string; + /** Notes */ + notes?: string | null; + /** Query Id */ + query_id: string; /** * Rating * @enum {integer} */ rating: 0 | 1 | 2 | 3; - /** Notes */ - notes?: string | null; }; /** * ImportJudgmentListRequest * @description Body for ``POST /api/v1/judgment-lists/import`` (Story 3.2). */ ImportJudgmentListRequest: { - /** Name */ - name: string; + /** Cluster Id */ + cluster_id: string; /** Description */ description?: string | null; + /** Judgments */ + judgments: components['schemas']['ImportJudgmentItem'][]; + /** Name */ + name: string; /** Query Set Id */ query_set_id: string; - /** Cluster Id */ - cluster_id: string; - /** Target */ - target: string; /** Rubric */ rubric: string; - /** Judgments */ - judgments: components['schemas']['ImportJudgmentItem'][]; + /** Target */ + target: string; }; /** * IntParam * @description Integer parameter inclusive of both bounds. */ IntParam: { + /** High */ + high: number; + /** Low */ + low: number; /** * @description discriminator enum property added by openapi-typescript * @enum {string} */ type: 'int'; - /** Low */ - low: number; - /** High */ - high: number; }; /** * JudgmentListDetail @@ -2276,45 +2279,45 @@ export interface components { * affordance. */ JudgmentListDetail: { + /** Calibration */ + calibration: { + [key: string]: unknown; + } | null; + /** Cluster Id */ + cluster_id: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Current Template Id */ + current_template_id: string | null; + /** Description */ + description: string | null; + /** Failed Reason */ + failed_reason: string | null; + /** Generation Params */ + generation_params: { + [key: string]: unknown; + } | null; /** Id */ id: string; + /** Judgment Count */ + judgment_count: number; /** Name */ name: string; - /** Description */ - description: string | null; /** Query Set Id */ query_set_id: string; - /** Cluster Id */ - cluster_id: string; - /** Target */ - target: string; - /** Current Template Id */ - current_template_id: string | null; /** Rubric */ rubric: string; + source_breakdown: components['schemas']['_SourceBreakdown']; /** * Status * @enum {string} */ status: 'generating' | 'complete' | 'failed'; - /** Failed Reason */ - failed_reason: string | null; - /** Judgment Count */ - judgment_count: number; - source_breakdown: components['schemas']['_SourceBreakdown']; - /** Calibration */ - calibration: { - [key: string]: unknown; - } | null; - /** Generation Params */ - generation_params: { - [key: string]: unknown; - } | null; - /** - * Created At - * Format: date-time - */ - created_at: string; + /** Target */ + target: string; }; /** * JudgmentListJudgmentsResponse @@ -2323,10 +2326,10 @@ export interface components { JudgmentListJudgmentsResponse: { /** Data */ data: components['schemas']['JudgmentRow'][]; - /** Next Cursor */ - next_cursor: string | null; /** Has More */ has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; }; /** * JudgmentListListResponse @@ -2335,10 +2338,10 @@ export interface components { JudgmentListListResponse: { /** Data */ data: components['schemas']['JudgmentListSummary'][]; - /** Next Cursor */ - next_cursor: string | null; /** Has More */ has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; }; /** * JudgmentListRef @@ -2359,42 +2362,53 @@ export interface components { * @description List-view row on ``GET /api/v1/judgment-lists``. */ JudgmentListSummary: { + /** Cluster Id */ + cluster_id: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Description */ + description: string | null; /** Id */ id: string; /** Name */ name: string; - /** Description */ - description: string | null; /** Query Set Id */ query_set_id: string; - /** Cluster Id */ - cluster_id: string; - /** Target */ - target: string; /** * Status * @enum {string} */ status: 'generating' | 'complete' | 'failed'; - /** - * Created At - * Format: date-time - */ - created_at: string; + /** Target */ + target: string; }; /** * JudgmentRow * @description ``GET /api/v1/judgment-lists/{id}/judgments`` row + PATCH response. */ JudgmentRow: { + /** Confidence */ + confidence: number | null; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Doc Id */ + doc_id: string; /** Id */ id: string; /** Judgment List Id */ judgment_list_id: string; + /** Notes */ + notes: string | null; /** Query Id */ query_id: string; - /** Doc Id */ - doc_id: string; + /** Rater Ref */ + rater_ref: string | null; /** * Rating * @enum {integer} @@ -2405,35 +2419,33 @@ export interface components { * @enum {string} */ source: 'llm' | 'human' | 'click'; - /** Rater Ref */ - rater_ref: string | null; - /** Confidence */ - confidence: number | null; - /** Notes */ - notes: string | null; - /** - * Created At - * Format: date-time - */ - created_at: string; }; /** * LateTrialStddevShape * @description Sample stddev of ``primary_metric`` over the late-trial window. */ LateTrialStddevShape: { + /** Min Window Required */ + min_window_required: number; /** Value */ value: number; /** Window Size */ window_size: number; - /** Min Window Required */ - min_window_required: number; }; /** * MessageWire * @description One row of ``GET /api/v1/conversations/{id}.messages``. */ MessageWire: { + /** Content */ + content: { + [key: string]: unknown; + }; + /** + * Created At + * Format: date-time + */ + created_at: string; /** Id */ id: string; /** @@ -2441,21 +2453,12 @@ export interface components { * @enum {string} */ role: 'user' | 'assistant' | 'tool'; - /** Content */ - content: { - [key: string]: unknown; - }; /** Tool Calls */ tool_calls?: | { [key: string]: unknown; }[] | null; - /** - * Created At - * Format: date-time - */ - created_at: string; }; /** * NarrowFollowup @@ -2485,18 +2488,18 @@ export interface components { */ ObjectiveSpec: { /** - * Metric + * Direction + * @default maximize * @enum {string} */ - metric: 'ndcg' | 'map' | 'precision' | 'recall' | 'mrr'; + direction: 'maximize' | 'minimize'; /** K */ k?: (1 | 3 | 5 | 10 | 20 | 50 | 100) | null; /** - * Direction - * @default maximize + * Metric * @enum {string} */ - direction: 'maximize' | 'minimize'; + metric: 'ndcg' | 'map' | 'precision' | 'recall' | 'mrr'; }; /** * OpenAICapabilities @@ -2511,17 +2514,6 @@ export interface components { * ``5xx -> upstream outage``, ``null -> network unreachable / cache miss``. */ OpenAICapabilities: { - /** - * 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 {string} - */ - models_endpoint: 'ok' | 'fail' | 'untested'; - /** - * Models Endpoint Status Code - * @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. - */ - models_endpoint_status_code: number | null; /** * Chat * @description Chat completion probe result @@ -2534,6 +2526,17 @@ export interface components { * @enum {string} */ function_calling: 'ok' | 'fail' | 'untested'; + /** + * 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 {string} + */ + models_endpoint: 'ok' | 'fail' | 'untested'; + /** + * Models Endpoint Status Code + * @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. + */ + models_endpoint_status_code: number | null; /** * Structured Output * @description JSON-schema response_format probe result @@ -2550,6 +2553,8 @@ export interface components { * after the PR is open. */ OpenPrResponse: { + /** Message */ + message: string; /** Proposal Id */ proposal_id: string; /** @@ -2557,8 +2562,6 @@ export interface components { * @constant */ status: 'pending'; - /** Message */ - message: string; }; /** * OverrideJudgmentRequest @@ -2570,10 +2573,10 @@ export interface components { * value manually and raises the domain code (per GPT-5.5 cycle 1 F4). */ OverrideJudgmentRequest: { - /** Rating */ - rating: number; /** Notes */ notes?: string | null; + /** Rating */ + rating: number; }; /** * ParentFollowupRef @@ -2591,34 +2594,34 @@ export interface components { * ``PROPOSAL_NOT_FOUND``. */ ParentFollowupRef: { - /** Proposal Id */ - proposal_id: string; /** Followup Index */ followup_index: number; + /** Proposal Id */ + proposal_id: string; }; /** * PerQueryOutcomesShape * @description Per-query outcome counts + the top-5 named regressors and improvers. */ PerQueryOutcomesShape: { - /** Improved */ - improved: number; - /** Unchanged */ - unchanged: number; - /** Regressed */ - regressed: number; /** * Comparison Against * @enum {string} */ comparison_against: 'runner_up' | 'baseline'; - /** Top Regressors */ - top_regressors: components['schemas']['RegressorRowShape'][]; + /** Improved */ + improved: number; + /** Regressed */ + regressed: number; /** * Top Improvers * @default [] */ top_improvers: components['schemas']['RegressorRowShape'][]; + /** Top Regressors */ + top_regressors: components['schemas']['RegressorRowShape'][]; + /** Unchanged */ + unchanged: number; }; /** * ProposalDetail @@ -2628,84 +2631,84 @@ export interface components { * and ``POST /api/v1/proposals/{id}/reject``. */ ProposalDetail: { - /** Id */ - id: string; - /** Study Id */ - study_id: string | null; - study_summary: components['schemas']['_StudySummary'] | null; - /** Study Trial Id */ - study_trial_id: string | null; cluster: components['schemas']['_ClusterEmbed']; - template: components['schemas']['_TemplateEmbed']; /** Config Diff */ config_diff: { [key: string]: unknown; }; + /** + * Created At + * Format: date-time + */ + created_at: string; + digest: components['schemas']['_DigestEmbed'] | null; + /** Id */ + id: string; + /** + * Is Currently Live + * @default false + */ + is_currently_live: boolean; /** Metric Delta */ metric_delta: { [key: string]: unknown; } | null; - /** - * Status - * @enum {string} - */ - status: 'pending' | 'pr_opened' | 'pr_merged' | 'rejected'; - /** Pr Url */ - pr_url: string | null; - /** Pr State */ - pr_state: ('open' | 'closed' | 'merged') | null; /** Pr Merged At */ pr_merged_at: string | null; /** Pr Open Error */ pr_open_error: string | null; + /** Pr State */ + pr_state: ('open' | 'closed' | 'merged') | null; + /** Pr Url */ + pr_url: string | null; /** Rejected Reason */ rejected_reason: string | null; /** - * Is Currently Live - * @default false - */ - is_currently_live: boolean; - digest: components['schemas']['_DigestEmbed'] | null; - /** - * Created At - * Format: date-time + * Status + * @enum {string} */ - created_at: string; + status: 'pending' | 'pr_opened' | 'pr_merged' | 'rejected'; + /** Study Id */ + study_id: string | null; + study_summary: components['schemas']['_StudySummary'] | null; + /** Study Trial Id */ + study_trial_id: string | null; + template: components['schemas']['_TemplateEmbed']; }; /** * ProposalSummary * @description Row in the ``GET /api/v1/proposals`` list response. */ ProposalSummary: { - /** Id */ - id: string; - /** Study Id */ - study_id: string | null; cluster: components['schemas']['_ClusterEmbed']; - template: components['schemas']['_TemplateEmbed']; /** - * Status - * @enum {string} + * Created At + * Format: date-time */ - status: 'pending' | 'pr_opened' | 'pr_merged' | 'rejected'; - /** Pr State */ - pr_state: ('open' | 'closed' | 'merged') | null; - /** Pr Url */ - pr_url: string | null; - /** Metric Delta */ - metric_delta: { - [key: string]: unknown; - } | null; + created_at: string; + /** Id */ + id: string; /** * Is Currently Live * @default false */ is_currently_live: boolean; + /** Metric Delta */ + metric_delta: { + [key: string]: unknown; + } | null; + /** Pr State */ + pr_state: ('open' | 'closed' | 'merged') | null; + /** Pr Url */ + pr_url: string | null; /** - * Created At - * Format: date-time + * Status + * @enum {string} */ - created_at: string; + status: 'pending' | 'pr_opened' | 'pr_merged' | 'rejected'; + /** Study Id */ + study_id: string | null; + template: components['schemas']['_TemplateEmbed']; }; /** * ProposalsListResponse @@ -2714,10 +2717,10 @@ export interface components { ProposalsListResponse: { /** Data */ data: components['schemas']['ProposalSummary'][]; - /** Next Cursor */ - next_cursor: string | null; /** Has More */ has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; }; /** * QueryHasJudgmentsDetail @@ -2735,17 +2738,17 @@ export interface components { * @constant */ error_code: 'QUERY_HAS_JUDGMENTS'; + /** Judgment Lists */ + judgment_lists: components['schemas']['JudgmentListRef'][]; /** Message */ message: string; + /** Overflow Count */ + overflow_count: number; /** * Retryable * @constant */ retryable: false; - /** Judgment Lists */ - judgment_lists: components['schemas']['JudgmentListRef'][]; - /** Overflow Count */ - overflow_count: number; }; /** * QueryHasJudgmentsEnvelope @@ -2761,10 +2764,10 @@ export interface components { QueryListResponse: { /** Data */ data: components['schemas']['QueryRow'][]; - /** Next Cursor */ - next_cursor: string | null; /** Has More */ has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; }; /** * QueryRow @@ -2778,37 +2781,37 @@ export interface components { QueryRow: { /** Id */ id: string; - /** Query Text */ - query_text: string; - /** Reference Answer */ - reference_answer: string | null; + /** Judgment Count */ + judgment_count: number; /** Query Metadata */ query_metadata: { [key: string]: unknown; } | null; - /** Judgment Count */ - judgment_count: number; + /** Query Text */ + query_text: string; + /** Reference Answer */ + reference_answer: string | null; }; /** * QuerySetDetail * @description ``GET /api/v1/query-sets/{id}`` response. */ QuerySetDetail: { - /** Id */ - id: string; - /** Name */ - name: string; - /** Description */ - description: string | null; /** Cluster Id */ cluster_id: string; - /** Query Count */ - query_count: number; /** * Created At * Format: date-time */ created_at: string; + /** Description */ + description: string | null; + /** Id */ + id: string; + /** Name */ + name: string; + /** Query Count */ + query_count: number; }; /** * QuerySetListResponse @@ -2817,20 +2820,16 @@ export interface components { QuerySetListResponse: { /** Data */ data: components['schemas']['QuerySetSummary'][]; - /** Next Cursor */ - next_cursor: string | null; /** Has More */ has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; }; /** * QuerySetSummary * @description List-view shape; omits ``query_count`` to avoid N+1 counts at list time. */ QuerySetSummary: { - /** Id */ - id: string; - /** Name */ - name: string; /** Cluster Id */ cluster_id: string; /** @@ -2838,36 +2837,40 @@ export interface components { * Format: date-time */ created_at: string; + /** Id */ + id: string; + /** Name */ + name: string; }; /** * QueryTemplateDetail * @description ``GET /api/v1/query-templates/{id}`` response. */ QueryTemplateDetail: { - /** Id */ - id: string; - /** Name */ - name: string; - /** - * Engine Type - * @enum {string} - */ - engine_type: 'elasticsearch' | 'opensearch' | 'solr'; /** Body */ body: string; - /** Declared Params */ - declared_params: { - [key: string]: string; - }; - /** Version */ - version: number; - /** Parent Id */ - parent_id: string | null; /** * Created At * Format: date-time */ created_at: string; + /** Declared Params */ + declared_params: { + [key: string]: string; + }; + /** + * Engine Type + * @enum {string} + */ + engine_type: 'elasticsearch' | 'opensearch' | 'solr'; + /** Id */ + id: string; + /** Name */ + name: string; + /** Parent Id */ + parent_id: string | null; + /** Version */ + version: number; }; /** * QueryTemplateListResponse @@ -2876,32 +2879,32 @@ export interface components { QueryTemplateListResponse: { /** Data */ data: components['schemas']['QueryTemplateSummary'][]; - /** Next Cursor */ - next_cursor: string | null; /** Has More */ has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; }; /** * QueryTemplateSummary * @description List-view shape; drops ``body`` + ``declared_params`` for brevity. */ QueryTemplateSummary: { - /** Id */ - id: string; - /** Name */ - name: string; + /** + * Created At + * Format: date-time + */ + created_at: string; /** * Engine Type * @enum {string} */ engine_type: 'elasticsearch' | 'opensearch' | 'solr'; + /** Id */ + id: string; + /** Name */ + name: string; /** Version */ version: number; - /** - * Created At - * Format: date-time - */ - created_at: string; }; /** * RegressorRowShape @@ -2914,16 +2917,16 @@ export interface components { * type keeps the schema and the per-row renderer compact. */ RegressorRowShape: { + /** Comparison Score */ + comparison_score: number; + /** Delta */ + delta: number; /** Query Id */ query_id: string; /** Query Text */ query_text: string; /** Winner Score */ winner_score: number; - /** Comparison Score */ - comparison_score: number; - /** Delta */ - delta: number; }; /** * RejectProposalRequest @@ -2942,32 +2945,34 @@ export interface components { * handler reads it in one round-trip. */ ReseedStatusResponse: { - /** - * Status - * @enum {string} - */ - status: 'idle' | 'running' | 'complete' | 'failed'; - /** Started At */ - started_at?: string | null; + /** Current Step */ + current_step?: string | null; + /** Failed Reason */ + failed_reason?: string | null; /** Finished At */ finished_at?: string | null; + /** + * Scenarios Completed + * @default 0 + */ + scenarios_completed: number; + /** Scenarios Skipped */ + scenarios_skipped?: string[]; /** * Scenarios Total * @default 0 */ scenarios_total: number; + /** Started At */ + started_at?: string | null; /** - * Scenarios Completed - * @default 0 + * Status + * @enum {string} */ - scenarios_completed: number; - /** Current Step */ - current_step?: string | null; - /** Failed Reason */ - failed_reason?: string | null; - summary?: components['schemas']['ReseedSummary'] | null; + status: 'idle' | 'running' | 'complete' | 'failed'; /** Steps */ steps?: string[]; + summary?: components['schemas']['ReseedSummary'] | null; }; /** * ReseedSummary @@ -2980,14 +2985,14 @@ export interface components { ReseedSummary: { /** Clusters Created */ clusters_created: number; + /** Duration Ms */ + duration_ms: number; + /** Proposals Created */ + proposals_created: number; /** Query Sets Created */ query_sets_created: number; /** Studies Completed */ studies_completed: number; - /** Proposals Created */ - proposals_created: number; - /** Duration Ms */ - duration_ms: number; }; /** * RunQueryHit @@ -3008,12 +3013,12 @@ export interface components { * @description ``POST /api/v1/clusters/{id}/run_query`` body. */ RunQueryRequest: { - /** Target */ - target: string; /** Query Dsl */ query_dsl: { [key: string]: unknown; }; + /** Target */ + target: string; /** * Top K * @default 10 @@ -3037,27 +3042,27 @@ export interface components { * is present. */ RunnerUpGapShape: { - /** Value */ - value: number; /** * Classification * @enum {string} */ classification: 'robust_plateau' | 'sharp_peak'; - /** Top10 Within */ - top10_within: number; /** Runner Up Metric */ runner_up_metric: number; + /** Top10 Within */ + top10_within: number; + /** Value */ + value: number; }; /** * Schema * @description An index / collection's field schema. */ Schema: { - /** Name */ - name: string; /** Fields */ fields: components['schemas']['FieldSpec'][]; + /** Name */ + name: string; }; /** * SearchSpace @@ -3096,12 +3101,6 @@ export interface components { SeedAutoFollowupChainRequest: { /** Cluster Id */ cluster_id: string; - /** Query Set Id */ - query_set_id: string; - /** Template Id */ - template_id: string; - /** Judgment List Id */ - judgment_list_id: string; /** * Depth * @description Number of chain hops to seed. depth=1 → root + leaf (2 nodes). depth=2 → root + 1 middle + leaf (3 nodes). @@ -3119,18 +3118,24 @@ export interface components { * @default true */ in_flight_middle: boolean; + /** Judgment List Id */ + judgment_list_id: string; + /** Query Set Id */ + query_set_id: string; + /** Template Id */ + template_id: string; }; /** * SeedAutoFollowupChainResponse * @description IDs of every node in the seeded chain, in parent→child order. */ SeedAutoFollowupChainResponse: { - /** Root Id */ - root_id: string; - /** Middle Ids */ - middle_ids: string[]; /** Leaf Id */ leaf_id: string; + /** Middle Ids */ + middle_ids: string[]; + /** Root Id */ + root_id: string; }; /** * SeedCompletedStudyRequest @@ -3143,27 +3148,15 @@ export interface components { SeedCompletedStudyRequest: { /** Cluster Id */ cluster_id: string; - /** Query Set Id */ - query_set_id: string; - /** Template Id */ - template_id: string; - /** Judgment List Id */ - judgment_list_id: string; - /** - * With Pending Proposal - * @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. - * @default true - */ - with_pending_proposal: boolean; /** - * Winner Per Query - * @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). + * Extra Trial Metrics + * @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. */ - winner_per_query?: { - [key: string]: { - [key: string]: unknown; - }; - } | null; + extra_trial_metrics?: number[] | null; + /** Judgment List Id */ + judgment_list_id: string; + /** Query Set Id */ + query_set_id: string; /** * Runner Up Per Query * @description Optional per-query metrics for the runner-up trial; pairs with `winner_per_query`. @@ -3182,31 +3175,48 @@ export interface components { [key: string]: unknown; }[] | null; + /** Template Id */ + template_id: string; + /** + * Winner Per Query + * @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). + */ + winner_per_query?: { + [key: string]: { + [key: string]: unknown; + }; + } | null; + /** + * With Pending Proposal + * @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. + * @default true + */ + with_pending_proposal: boolean; }; /** * SeedCompletedStudyResponse * @description IDs of the inserted rows; mirrors :class:`SeededStudyTriple`. */ SeedCompletedStudyResponse: { - /** Study Id */ - study_id: string; /** Digest Id */ digest_id: string; /** Proposal Id */ proposal_id: string | null; + /** Study Id */ + study_id: string; }; /** * SendMessageRequest * @description ``POST /api/v1/conversations/{id}/messages`` body (Story 3.2). */ SendMessageRequest: { + content: components['schemas']['SendMessageRequestContent']; /** * Role * @default user * @constant */ role: 'user'; - content: components['schemas']['SendMessageRequestContent']; }; /** * SendMessageRequestContent @@ -3221,39 +3231,39 @@ export interface components { * @description One link in the rolled-up overnight-chain summary (feat_overnight_autopilot §8.3). */ StudyChainLink: { - /** Id */ - id: string; - /** Name */ - name: string; - /** - * Status - * @enum {string} - */ - status: 'queued' | 'running' | 'completed' | 'cancelled' | 'failed'; - /** Best Metric */ - best_metric: number | null; + /** Auto Followup Depth Remaining */ + auto_followup_depth_remaining: number | null; /** Baseline Metric */ baseline_metric: number | null; + /** Best Metric */ + best_metric: number | null; + /** Completed At */ + completed_at: string | null; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Delta From Prev */ + delta_from_prev: number | null; /** * Direction * @enum {string} */ direction: 'maximize' | 'minimize'; - /** Delta From Prev */ - delta_from_prev: number | null; - /** Proposal Id */ - proposal_id: string | null; - /** Auto Followup Depth Remaining */ - auto_followup_depth_remaining: number | null; /** Failed Reason */ failed_reason: string | null; + /** Id */ + id: string; + /** Name */ + name: string; + /** Proposal Id */ + proposal_id: string | null; /** - * Created At - * Format: date-time + * Status + * @enum {string} */ - created_at: string; - /** Completed At */ - completed_at: string | null; + status: 'queued' | 'running' | 'completed' | 'cancelled' | 'failed'; }; /** * StudyChainResponse @@ -3273,6 +3283,10 @@ export interface components { * @enum {string} */ direction: 'maximize' | 'minimize'; + /** Links */ + links: components['schemas']['StudyChainLink'][]; + /** Proposal Id For Best Link */ + proposal_id_for_best_link: string | null; /** * Stop Reason * @enum {string} @@ -3284,10 +3298,6 @@ export interface components { | 'parent_failed' | 'cancelled' | 'in_flight'; - /** Proposal Id For Best Link */ - proposal_id_for_best_link: string | null; - /** Links */ - links: components['schemas']['StudyChainLink'][]; }; /** * StudyConfigSpec @@ -3303,28 +3313,28 @@ export interface components { * contract. */ StudyConfigSpec: { + /** Auto Followup Depth */ + auto_followup_depth?: number | null; + /** Baseline Params */ + baseline_params?: { + [key: string]: string | number | boolean | null; + } | null; /** Max Trials */ max_trials?: number | null; - /** Time Budget Min */ - time_budget_min?: number | null; /** Parallelism */ parallelism?: number | null; - /** Trial Timeout S */ - trial_timeout_s?: number | null; - /** Sampler */ - sampler?: ('tpe' | 'random') | null; /** Pruner */ pruner?: ('median' | 'none') | null; - /** Seed */ - seed?: number | null; + /** Sampler */ + sampler?: ('tpe' | 'random') | null; /** Secondary Metrics */ secondary_metrics?: string[] | null; - /** Baseline Params */ - baseline_params?: { - [key: string]: string | number | boolean | null; - } | null; - /** Auto Followup Depth */ - auto_followup_depth?: number | null; + /** Seed */ + seed?: number | null; + /** Time Budget Min */ + time_budget_min?: number | null; + /** Trial Timeout S */ + trial_timeout_s?: number | null; }; /** * StudyConvergenceShape @@ -3343,95 +3353,95 @@ export interface components { * sub-shape stays on its inner module. The two coexist on ``StudyDetail`` * (``confidence.convergence`` is the inner one; ``convergence`` is this * one), and FastAPI emits both under their bare class names in the - * OpenAPI schema — no fully-qualified disambiguation noise leaks to the - * frontend. - */ - StudyConvergenceShape: { - /** - * Verdict - * @enum {string} - */ - verdict: 'converged' | 'still_improving' | 'too_few_trials'; + * OpenAPI schema — no fully-qualified disambiguation noise leaks to the + * frontend. + */ + StudyConvergenceShape: { + /** Best So Far Curve */ + best_so_far_curve: components['schemas']['CurvePoint'][]; /** * Direction * @enum {string} */ direction: 'maximize' | 'minimize'; - /** Window Size */ - window_size: number; /** Epsilon */ epsilon: number; - /** Warmup Floor */ - warmup_floor: number; - /** Total Complete Trials */ - total_complete_trials: number; /** Improvement In Window */ improvement_in_window: number; - /** Best So Far Curve */ - best_so_far_curve: components['schemas']['CurvePoint'][]; + /** Total Complete Trials */ + total_complete_trials: number; + /** + * Verdict + * @enum {string} + */ + verdict: 'converged' | 'still_improving' | 'too_few_trials'; + /** Warmup Floor */ + warmup_floor: number; + /** Window Size */ + window_size: number; }; /** * StudyDetail * @description ``GET /api/v1/studies/{id}`` response + ``POST/cancel`` response. */ StudyDetail: { - /** Id */ - id: string; - /** Name */ - name: string; + /** Baseline Metric */ + baseline_metric: number | null; + /** Baseline Trial Id */ + baseline_trial_id: string | null; + /** Best Metric */ + best_metric: number | null; + /** Best Trial Id */ + best_trial_id: string | null; /** Cluster Id */ cluster_id: string; - /** Target */ - target: string; - /** Template Id */ - template_id: string; - /** Query Set Id */ - query_set_id: string; - /** Judgment List Id */ - judgment_list_id: string; - /** Search Space */ - search_space: { - [key: string]: unknown; - }; - /** Objective */ - objective: { - [key: string]: unknown; - }; + /** Completed At */ + completed_at: string | null; + confidence?: components['schemas']['ConfidenceShape'] | null; /** Config */ config: { [key: string]: unknown; }; + convergence?: components['schemas']['StudyConvergenceShape'] | null; /** - * Status - * @enum {string} + * Created At + * Format: date-time */ - status: 'queued' | 'running' | 'completed' | 'cancelled' | 'failed'; + created_at: string; /** Failed Reason */ failed_reason: string | null; + /** Id */ + id: string; + /** Judgment List Id */ + judgment_list_id: string; + /** Name */ + name: string; + /** Objective */ + objective: { + [key: string]: unknown; + }; /** Optuna Study Name */ optuna_study_name: string; /** Parent Study Id */ parent_study_id: string | null; - /** Baseline Metric */ - baseline_metric: number | null; - /** Baseline Trial Id */ - baseline_trial_id: string | null; - /** Best Metric */ - best_metric: number | null; - /** Best Trial Id */ - best_trial_id: string | null; - /** - * Created At - * Format: date-time - */ - created_at: string; + /** Query Set Id */ + query_set_id: string; + /** Search Space */ + search_space: { + [key: string]: unknown; + }; /** Started At */ started_at: string | null; - /** Completed At */ - completed_at: string | null; + /** + * Status + * @enum {string} + */ + status: 'queued' | 'running' | 'completed' | 'cancelled' | 'failed'; + /** Target */ + target: string; + /** Template Id */ + template_id: string; trials_summary: components['schemas']['TrialsSummaryShape']; - confidence?: components['schemas']['ConfidenceShape'] | null; - convergence?: components['schemas']['StudyConvergenceShape'] | null; }; /** * StudyListResponse @@ -3440,42 +3450,49 @@ export interface components { StudyListResponse: { /** Data */ data: components['schemas']['StudySummary'][]; - /** Next Cursor */ - next_cursor: string | null; /** Has More */ has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; }; /** * StudySummary * @description List-view shape. */ StudySummary: { - /** Id */ - id: string; - /** Name */ - name: string; + /** Best Metric */ + best_metric: number | null; /** Cluster Id */ cluster_id: string; + /** Completed At */ + completed_at: string | null; + /** Convergence Verdict */ + convergence_verdict?: ('converged' | 'still_improving' | 'too_few_trials') | null; /** - * Status - * @enum {string} + * Created At + * Format: date-time */ - status: 'queued' | 'running' | 'completed' | 'cancelled' | 'failed'; - /** Best Metric */ - best_metric: number | null; + created_at: string; /** * Direction * @default maximize * @enum {string} */ direction: 'maximize' | 'minimize'; + /** Id */ + id: string; + /** Name */ + name: string; /** - * Created At - * Format: date-time + * Status + * @enum {string} */ - created_at: string; - /** Completed At */ - completed_at: string | null; + status: 'queued' | 'running' | 'completed' | 'cancelled' | 'failed'; + /** + * Trial Count + * @default 0 + */ + trial_count: number; }; /** * Subsystems @@ -3489,29 +3506,31 @@ export interface components { */ db: 'ok' | 'down'; /** - * Redis - * @description Redis reachability + * Elasticsearch + * @description Local Elasticsearch container reachability * @enum {string} */ - redis: 'ok' | 'down'; + elasticsearch: 'reachable' | 'unreachable'; + /** @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`. */ + elasticsearch_clusters: components['schemas']['ClusterAggregateHealth']; /** * 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 {string} */ openai: 'configured' | 'missing_key' | 'incapable'; - /** - * Elasticsearch - * @description Local Elasticsearch container reachability - * @enum {string} - */ - elasticsearch: 'reachable' | 'unreachable'; /** * Opensearch * @description Local OpenSearch container reachability * @enum {string} */ opensearch: 'reachable' | 'unreachable'; + /** + * Redis + * @description Redis reachability + * @enum {string} + */ + redis: 'ok' | 'down'; /** * Solr * @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. @@ -3519,8 +3538,6 @@ export interface components { * @enum {string} */ solr: 'reachable' | 'unreachable' | 'not_configured'; - /** @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`. */ - elasticsearch_clusters: components['schemas']['ClusterAggregateHealth']; }; /** * SwapTemplateFollowup @@ -3542,19 +3559,19 @@ export interface components { kind: 'swap_template'; /** Rationale */ rationale: string; + search_space: components['schemas']['SearchSpace']; /** Template Id */ template_id: string; - search_space: components['schemas']['SearchSpace']; }; /** * TargetInfo * @description One target (index / collection) on a cluster. */ TargetInfo: { - /** Name */ - name: string; /** Doc Count */ doc_count?: number | null; + /** Name */ + name: string; }; /** * TargetListResponse @@ -3591,10 +3608,23 @@ export interface components { * @description ``GET /api/v1/studies/{id}/trials`` response row. */ TrialDetail: { + /** Duration Ms */ + duration_ms: number | null; + /** Ended At */ + ended_at: string | null; + /** Error */ + error: string | null; /** Id */ id: string; - /** Study Id */ - study_id: string; + /** + * Is Baseline + * @default false + */ + is_baseline: boolean; + /** Metrics */ + metrics: { + [key: string]: unknown; + }; /** Optuna Trial Number */ optuna_trial_number: number; /** Params */ @@ -3603,28 +3633,15 @@ export interface components { }; /** Primary Metric */ primary_metric: number | null; - /** Metrics */ - metrics: { - [key: string]: unknown; - }; - /** Duration Ms */ - duration_ms: number | null; + /** Started At */ + started_at: string | null; /** * Status * @enum {string} */ status: 'complete' | 'failed' | 'pruned'; - /** Error */ - error: string | null; - /** Started At */ - started_at: string | null; - /** Ended At */ - ended_at: string | null; - /** - * Is Baseline - * @default false - */ - is_baseline: boolean; + /** Study Id */ + study_id: string; }; /** * TrialListResponse @@ -3633,26 +3650,26 @@ export interface components { TrialListResponse: { /** Data */ data: components['schemas']['TrialDetail'][]; - /** Next Cursor */ - next_cursor: string | null; /** Has More */ has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; }; /** * TrialsSummaryShape * @description The ``trials_summary`` field embedded in :class:`StudyDetail`. */ TrialsSummaryShape: { - /** Total */ - total: number; + /** Best Primary Metric */ + best_primary_metric: number | null; /** Complete */ complete: number; /** Failed */ failed: number; /** Pruned */ pruned: number; - /** Best Primary Metric */ - best_primary_metric: number | null; + /** Total */ + total: number; }; /** * UbiReadinessResponse @@ -3667,19 +3684,19 @@ export interface components { */ UbiReadinessResponse: { /** - * Rung - * @enum {string} + * Checked At + * Format: date-time */ - rung: 'rung_0' | 'rung_1' | 'rung_2' | 'rung_3'; + checked_at: string; /** Covered Pairs Pct */ covered_pairs_pct: number | null; /** Head Covered */ head_covered: boolean | null; /** - * Checked At - * Format: date-time + * Rung + * @enum {string} */ - checked_at: string; + rung: 'rung_0' | 'rung_1' | 'rung_2' | 'rung_3'; }; /** * UpdateQueryRequest @@ -3694,27 +3711,27 @@ export interface components { * than the SQL ``NotNullViolation``). */ UpdateQueryRequest: { - /** Query Text */ - query_text?: string | null; - /** Reference Answer */ - reference_answer?: string | null; /** Query Metadata */ query_metadata?: { [key: string]: unknown; } | null; + /** Query Text */ + query_text?: string | null; + /** Reference Answer */ + reference_answer?: string | null; }; /** ValidationError */ ValidationError: { + /** Context */ + ctx?: Record; + /** Input */ + input?: unknown; /** Location */ loc: (string | number)[]; /** Message */ msg: string; /** Error Type */ type: string; - /** Input */ - input?: unknown; - /** Context */ - ctx?: Record; }; /** * WidenFollowup @@ -3735,14 +3752,14 @@ export interface components { * @description Inline cluster summary on proposal responses. */ _ClusterEmbed: { - /** Id */ - id: string; - /** Name */ - name: string; /** Engine Type */ engine_type: string; /** Environment */ environment?: string | null; + /** Id */ + id: string; + /** Name */ + name: string; }; /** * _DigestEmbed @@ -3752,6 +3769,11 @@ export interface components { * now a discriminated-union list (see ``DigestResponse``). */ _DigestEmbed: { + /** + * Generated At + * Format: date-time + */ + generated_at: string; /** Id */ id: string; /** Narrative */ @@ -3766,11 +3788,6 @@ export interface components { }; /** Suggested Followups */ suggested_followups: components['schemas']['FollowupItem'][]; - /** - * Generated At - * Format: date-time - */ - generated_at: string; }; /** * _SourceBreakdown @@ -3783,50 +3800,50 @@ export interface components { * buckets separately so operators see the mix at a glance. */ _SourceBreakdown: { - /** Llm */ - llm: number; - /** Human */ - human: number; /** Click */ click: number; + /** Human */ + human: number; + /** Llm */ + llm: number; }; /** * _StudySummary * @description Inline study summary on the proposal-detail response. */ _StudySummary: { - /** Id */ - id: string; - /** Name */ - name: string; - /** Status */ - status: string; /** Best Metric */ best_metric: number | null; /** Best Trial Id */ best_trial_id: string | null; - /** Query Set */ - query_set: { - [key: string]: unknown; - }; + /** Id */ + id: string; /** Judgment List */ judgment_list: { [key: string]: unknown; }; + /** Name */ + name: string; + /** Query Set */ + query_set: { + [key: string]: unknown; + }; + /** Status */ + status: string; }; /** * _TemplateEmbed * @description Inline template summary on proposal responses. */ _TemplateEmbed: { + /** Engine Type */ + engine_type?: string | null; /** Id */ id: string; /** Name */ name: string; /** Version */ version: number; - /** Engine Type */ - engine_type?: string | null; }; }; responses: never; @@ -3837,36 +3854,7 @@ export interface components { } export type $defs = Record; export interface operations { - healthz_healthz_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HealthResponse']; - }; - }; - /** @description One or more required subsystems is down */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HealthResponse']; - }; - }; - }; - }; - test_connection_api_v1_clusters_test_connection_post: { + seed_auto_followup_chain_endpoint_api_v1__test_auto_followup_seed_chain_post: { parameters: { query?: never; header?: never; @@ -3875,17 +3863,17 @@ export interface operations { }; requestBody: { content: { - 'application/json': components['schemas']['ConnectionTestRequest']; + 'application/json': components['schemas']['SeedAutoFollowupChainRequest']; }; }; responses: { /** @description Successful Response */ - 200: { + 201: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['ConnectionTestResult']; + 'application/json': components['schemas']['SeedAutoFollowupChainResponse']; }; }; /** @description Validation Error */ @@ -3899,13 +3887,11 @@ export interface operations { }; }; }; - reprobe_cluster_api_v1_clusters__cluster_id__reprobe_post: { + reseed_demo_api_v1__test_demo_reseed_post: { parameters: { query?: never; header?: never; - path: { - cluster_id: string; - }; + path?: never; cookie?: never; }; requestBody?: never; @@ -3916,40 +3902,14 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['ClusterDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; + 'application/json': components['schemas']['ReseedStatusResponse']; }; }; }; }; - list_clusters_api_v1_clusters_get: { + reseed_demo_status_api_v1__test_demo_reseed_status_get: { parameters: { - query?: { - cursor?: string | null; - limit?: number; - since?: string | null; - q?: string | null; - sort?: - | ( - | 'name:asc' - | 'name:desc' - | 'created_at:asc' - | 'created_at:desc' - | 'environment:asc' - | 'environment:desc' - ) - | null; - engine_type?: ('elasticsearch' | 'opensearch' | 'solr') | null; - environment?: ('prod' | 'staging' | 'dev') | null; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -3962,72 +3922,28 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['ClusterListResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - create_cluster_api_v1_clusters_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateClusterRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ClusterDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; + 'application/json': components['schemas']['ReseedStatusResponse']; }; }; }; }; - get_cluster_detail_api_v1_clusters__cluster_id__get: { + delete_test_digest_api_v1__test_digests__digest_id__delete: { parameters: { query?: never; header?: never; path: { - cluster_id: string; + digest_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Successful Response */ - 200: { + 204: { headers: { [name: string]: unknown; }; - content: { - 'application/json': components['schemas']['ClusterDetail']; - }; + content?: never; }; /** @description Validation Error */ 422: { @@ -4040,12 +3956,12 @@ export interface operations { }; }; }; - delete_cluster_api_v1_clusters__cluster_id__delete: { + delete_test_judgment_list_api_v1__test_judgment_lists__judgment_list_id__delete: { parameters: { query?: never; header?: never; path: { - cluster_id: string; + judgment_list_id: string; }; cookie?: never; }; @@ -4069,27 +3985,23 @@ export interface operations { }; }; }; - get_cluster_schema_api_v1_clusters__cluster_id__schema_get: { + delete_test_proposal_api_v1__test_proposals__proposal_id__delete: { parameters: { - query: { - target: string; - }; + query?: never; header?: never; path: { - cluster_id: string; + proposal_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Successful Response */ - 200: { + 204: { headers: { [name: string]: unknown; }; - content: { - 'application/json': components['schemas']['Schema']; - }; + content?: never; }; /** @description Validation Error */ 422: { @@ -4102,28 +4014,23 @@ export interface operations { }; }; }; - get_cluster_ubi_readiness_api_v1_clusters__cluster_id__ubi_readiness_get: { + delete_test_query_set_api_v1__test_query_sets__query_set_id__delete: { parameters: { - query: { - query_set_id: string; - target: string; - }; + query?: never; header?: never; path: { - cluster_id: string; + query_set_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Successful Response */ - 200: { + 204: { headers: { [name: string]: unknown; }; - content: { - 'application/json': components['schemas']['UbiReadinessResponse']; - }; + content?: never; }; /** @description Validation Error */ 422: { @@ -4136,62 +4043,23 @@ export interface operations { }; }; }; - list_cluster_targets_api_v1_clusters__cluster_id__targets_get: { + delete_test_query_template_api_v1__test_query_templates__template_id__delete: { parameters: { query?: never; header?: never; path: { - cluster_id: string; + template_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['TargetListResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - run_query_api_v1_clusters__cluster_id__run_query_post: { - parameters: { - query?: { - timeout_s?: number; - }; - header?: never; - path: { - cluster_id: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['RunQueryRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 200: { + 204: { headers: { [name: string]: unknown; }; - content: { - 'application/json': components['schemas']['RunQueryResponse']; - }; + content?: never; }; /** @description Validation Error */ 422: { @@ -4204,29 +4072,26 @@ export interface operations { }; }; }; - list_target_documents_api_v1_clusters__cluster_id__targets__target__documents_get: { + seed_completed_study_api_v1__test_studies_seed_completed_post: { parameters: { - query?: { - cursor?: string | null; - limit?: number; - fields?: string | null; - }; + query?: never; header?: never; - path: { - cluster_id: string; - target: string; - }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + 'application/json': components['schemas']['SeedCompletedStudyRequest']; + }; + }; responses: { /** @description Successful Response */ - 200: { + 201: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['DocumentListResponse']; + 'application/json': components['schemas']['SeedCompletedStudyResponse']; }; }; /** @description Validation Error */ @@ -4240,27 +4105,23 @@ export interface operations { }; }; }; - get_target_document_api_v1_clusters__cluster_id__targets__target__documents__doc_id__get: { + delete_test_study_api_v1__test_studies__study_id__delete: { parameters: { query?: never; header?: never; path: { - cluster_id: string; - target: string; - doc_id: string; + study_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Successful Response */ - 200: { + 204: { headers: { [name: string]: unknown; }; - content: { - 'application/json': components['schemas']['Document']; - }; + content?: never; }; /** @description Validation Error */ 422: { @@ -4273,7 +4134,7 @@ export interface operations { }; }; }; - list_query_templates_api_v1_query_templates_get: { + list_clusters_api_v1_clusters_get: { parameters: { query?: { cursor?: string | null; @@ -4286,13 +4147,12 @@ export interface operations { | 'name:desc' | 'created_at:asc' | 'created_at:desc' - | 'engine_type:asc' - | 'engine_type:desc' - | 'version:asc' - | 'version:desc' + | 'environment:asc' + | 'environment:desc' ) | null; engine_type?: ('elasticsearch' | 'opensearch' | 'solr') | null; + environment?: ('prod' | 'staging' | 'dev') | null; }; header?: never; path?: never; @@ -4306,7 +4166,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['QueryTemplateListResponse']; + 'application/json': components['schemas']['ClusterListResponse']; }; }; /** @description Validation Error */ @@ -4320,7 +4180,7 @@ export interface operations { }; }; }; - create_query_template_api_v1_query_templates_post: { + create_cluster_api_v1_clusters_post: { parameters: { query?: never; header?: never; @@ -4329,7 +4189,7 @@ export interface operations { }; requestBody: { content: { - 'application/json': components['schemas']['CreateQueryTemplateRequest']; + 'application/json': components['schemas']['CreateClusterRequest']; }; }; responses: { @@ -4339,7 +4199,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['QueryTemplateDetail']; + 'application/json': components['schemas']['ClusterDetail']; }; }; /** @description Validation Error */ @@ -4353,16 +4213,18 @@ export interface operations { }; }; }; - get_query_template_detail_api_v1_query_templates__template_id__get: { + test_connection_api_v1_clusters_test_connection_post: { parameters: { query?: never; header?: never; - path: { - template_id: string; - }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + 'application/json': components['schemas']['ConnectionTestRequest']; + }; + }; responses: { /** @description Successful Response */ 200: { @@ -4370,7 +4232,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['QueryTemplateDetail']; + 'application/json': components['schemas']['ConnectionTestResult']; }; }; /** @description Validation Error */ @@ -4384,17 +4246,13 @@ export interface operations { }; }; }; - list_query_sets_api_v1_query_sets_get: { + get_cluster_detail_api_v1_clusters__cluster_id__get: { parameters: { - query?: { - cursor?: string | null; - limit?: number; - since?: string | null; - q?: string | null; - sort?: ('name:asc' | 'name:desc' | 'created_at:asc' | 'created_at:desc') | null; - }; + query?: never; header?: never; - path?: never; + path: { + cluster_id: string; + }; cookie?: never; }; requestBody?: never; @@ -4405,7 +4263,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['QuerySetListResponse']; + 'application/json': components['schemas']['ClusterDetail']; }; }; /** @description Validation Error */ @@ -4419,27 +4277,23 @@ export interface operations { }; }; }; - create_query_set_api_v1_query_sets_post: { + delete_cluster_api_v1_clusters__cluster_id__delete: { parameters: { query?: never; header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateQuerySetRequest']; + path: { + cluster_id: string; }; + cookie?: never; }; + requestBody?: never; responses: { /** @description Successful Response */ - 201: { + 204: { headers: { [name: string]: unknown; }; - content: { - 'application/json': components['schemas']['QuerySetDetail']; - }; + content?: never; }; /** @description Validation Error */ 422: { @@ -4452,24 +4306,24 @@ export interface operations { }; }; }; - get_query_set_detail_api_v1_query_sets__query_set_id__get: { + reprobe_cluster_api_v1_clusters__cluster_id__reprobe_post: { parameters: { query?: never; header?: never; path: { - query_set_id: string; + cluster_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Successful Response */ - 200: { + 202: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['QuerySetDetail']; + 'application/json': components['schemas']['ClusterDetail']; }; }; /** @description Validation Error */ @@ -4483,20 +4337,22 @@ export interface operations { }; }; }; - list_queries_in_set_api_v1_query_sets__query_set_id__queries_get: { + run_query_api_v1_clusters__cluster_id__run_query_post: { parameters: { query?: { - cursor?: string | null; - limit?: number; - since?: string | null; + timeout_s?: number; }; header?: never; path: { - query_set_id: string; + cluster_id: string; }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + 'application/json': components['schemas']['RunQueryRequest']; + }; + }; responses: { /** @description Successful Response */ 200: { @@ -4504,7 +4360,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['QueryListResponse']; + 'application/json': components['schemas']['RunQueryResponse']; }; }; /** @description Validation Error */ @@ -4518,24 +4374,26 @@ export interface operations { }; }; }; - bulk_add_queries_api_v1_query_sets__query_set_id__queries_post: { + get_cluster_schema_api_v1_clusters__cluster_id__schema_get: { parameters: { - query?: never; + query: { + target: string; + }; header?: never; path: { - query_set_id: string; + cluster_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Successful Response */ - 201: { + 200: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['BulkQueriesResponse']; + 'application/json': components['schemas']['Schema']; }; }; /** @description Validation Error */ @@ -4549,32 +4407,60 @@ export interface operations { }; }; }; - delete_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__delete: { + list_cluster_targets_api_v1_clusters__cluster_id__targets_get: { parameters: { query?: never; header?: never; path: { - query_set_id: string; - query_id: string; + cluster_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Successful Response */ - 204: { + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + 'application/json': components['schemas']['TargetListResponse']; + }; }; - /** @description Conflict */ - 409: { + /** @description Validation Error */ + 422: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['QueryHasJudgmentsEnvelope']; + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + list_target_documents_api_v1_clusters__cluster_id__targets__target__documents_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + fields?: string | null; + }; + header?: never; + path: { + cluster_id: string; + target: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['DocumentListResponse']; }; }; /** @description Validation Error */ @@ -4588,21 +4474,52 @@ export interface operations { }; }; }; - update_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__patch: { + get_target_document_api_v1_clusters__cluster_id__targets__target__documents__doc_id__get: { parameters: { query?: never; header?: never; path: { - query_set_id: string; - query_id: string; + cluster_id: string; + target: string; + doc_id: string; }; cookie?: never; }; - requestBody: { - content: { - 'application/json': components['schemas']['UpdateQueryRequest']; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Document']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_cluster_ubi_readiness_api_v1_clusters__cluster_id__ubi_readiness_get: { + parameters: { + query: { + query_set_id: string; + target: string; + }; + header?: never; + path: { + cluster_id: string; }; + cookie?: never; }; + requestBody?: never; responses: { /** @description Successful Response */ 200: { @@ -4610,7 +4527,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['QueryRow']; + 'application/json': components['schemas']['UbiReadinessResponse']; }; }; /** @description Validation Error */ @@ -4624,30 +4541,11 @@ export interface operations { }; }; }; - list_studies_api_v1_studies_get: { + list_config_repos_endpoint_api_v1_config_repos_get: { parameters: { query?: { cursor?: string | null; limit?: number; - since?: string | null; - status?: ('queued' | 'running' | 'completed' | 'cancelled' | 'failed') | null; - cluster_id?: string | null; - target?: string | null; - q?: string | null; - sort?: - | ( - | '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' - ) - | null; }; header?: never; path?: never; @@ -4661,7 +4559,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['StudyListResponse']; + 'application/json': components['schemas']['ConfigReposListResponse']; }; }; /** @description Validation Error */ @@ -4675,7 +4573,7 @@ export interface operations { }; }; }; - create_study_api_v1_studies_post: { + create_config_repo_endpoint_api_v1_config_repos_post: { parameters: { query?: never; header?: never; @@ -4684,7 +4582,7 @@ export interface operations { }; requestBody: { content: { - 'application/json': components['schemas']['CreateStudyRequest']; + 'application/json': components['schemas']['CreateConfigRepoRequest']; }; }; responses: { @@ -4694,7 +4592,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['StudyDetail']; + 'application/json': components['schemas']['ConfigRepoDetail']; }; }; /** @description Validation Error */ @@ -4708,12 +4606,12 @@ export interface operations { }; }; }; - get_study_detail_api_v1_studies__study_id__get: { + get_config_repo_endpoint_api_v1_config_repos__config_repo_id__get: { parameters: { query?: never; header?: never; path: { - study_id: string; + config_repo_id: string; }; cookie?: never; }; @@ -4725,7 +4623,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['StudyDetail']; + 'application/json': components['schemas']['ConfigRepoDetail']; }; }; /** @description Validation Error */ @@ -4739,15 +4637,16 @@ export interface operations { }; }; }; - cancel_study_api_v1_studies__study_id__cancel_post: { + list_conversations_endpoint_api_v1_conversations_get: { parameters: { query?: { - cascade?: string; + cursor?: string | null; + limit?: number; + since?: string | null; + q?: string | null; }; header?: never; - path: { - study_id: string; - }; + path?: never; cookie?: never; }; requestBody?: never; @@ -4758,7 +4657,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['StudyDetail']; + 'application/json': components['schemas']['ConversationsListResponse']; }; }; /** @description Validation Error */ @@ -4772,24 +4671,26 @@ export interface operations { }; }; }; - list_study_children_api_v1_studies__study_id__children_get: { + create_conversation_endpoint_api_v1_conversations_post: { parameters: { query?: never; header?: never; - path: { - study_id: string; - }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + 'application/json': components['schemas']['CreateConversationRequest']; + }; + }; responses: { /** @description Successful Response */ - 200: { + 201: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['StudyListResponse']; + 'application/json': components['schemas']['ConversationSummary']; }; }; /** @description Validation Error */ @@ -4803,17 +4704,12 @@ export interface operations { }; }; }; - list_study_trials_api_v1_studies__study_id__trials_get: { + get_conversation_endpoint_api_v1_conversations__conversation_id__get: { parameters: { - query?: { - cursor?: string | null; - limit?: number; - since?: string | null; - sort?: string; - }; + query?: never; header?: never; path: { - study_id: string; + conversation_id: string; }; cookie?: never; }; @@ -4825,7 +4721,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['TrialListResponse']; + 'application/json': components['schemas']['ConversationDetail']; }; }; /** @description Validation Error */ @@ -4839,25 +4735,23 @@ export interface operations { }; }; }; - get_study_chain_api_v1_studies__study_id__chain_get: { + delete_conversation_endpoint_api_v1_conversations__conversation_id__delete: { parameters: { query?: never; header?: never; path: { - study_id: string; + conversation_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Successful Response */ - 200: { + 204: { headers: { [name: string]: unknown; }; - content: { - 'application/json': components['schemas']['StudyChainResponse']; - }; + content?: never; }; /** @description Validation Error */ 422: { @@ -4870,26 +4764,28 @@ export interface operations { }; }; }; - generate_judgments_api_v1_judgments_generate_post: { + post_message_endpoint_api_v1_conversations__conversation_id__messages_post: { parameters: { query?: never; header?: never; - path?: never; + path: { + conversation_id: string; + }; cookie?: never; }; requestBody: { content: { - 'application/json': components['schemas']['CreateJudgmentListGenerateRequest']; + 'application/json': components['schemas']['SendMessageRequest']; }; }; responses: { /** @description Successful Response */ - 202: { + 200: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['GenerateJudgmentsResponse']; + 'application/json': unknown; }; }; /** @description Validation Error */ @@ -4903,26 +4799,40 @@ export interface operations { }; }; }; - generate_judgments_from_ubi_api_v1_judgments_generate_from_ubi_post: { + list_judgment_lists_endpoint_api_v1_judgment_lists_get: { parameters: { - query?: never; + query?: { + cursor?: string | null; + limit?: number; + since?: string | null; + q?: string | null; + sort?: + | ( + | 'name:asc' + | 'name:desc' + | 'created_at:asc' + | 'created_at:desc' + | 'status:asc' + | 'status:desc' + ) + | null; + query_set_id?: string | null; + cluster_id?: string | null; + target?: string | null; + }; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateJudgmentListFromUbiRequest']; - }; - }; + requestBody?: never; responses: { /** @description Successful Response */ - 202: { + 200: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['GenerateJudgmentsResponse']; + 'application/json': components['schemas']['JudgmentListListResponse']; }; }; /** @description Validation Error */ @@ -4969,29 +4879,13 @@ export interface operations { }; }; }; - list_judgment_lists_endpoint_api_v1_judgment_lists_get: { + get_judgment_list_endpoint_api_v1_judgment_lists__judgment_list_id__get: { parameters: { - query?: { - cursor?: string | null; - limit?: number; - since?: string | null; - q?: string | null; - sort?: - | ( - | 'name:asc' - | 'name:desc' - | 'created_at:asc' - | 'created_at:desc' - | 'status:asc' - | 'status:desc' - ) - | null; - query_set_id?: string | null; - cluster_id?: string | null; - target?: string | null; - }; + query?: never; header?: never; - path?: never; + path: { + judgment_list_id: string; + }; cookie?: never; }; requestBody?: never; @@ -5002,7 +4896,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['JudgmentListListResponse']; + 'application/json': components['schemas']['JudgmentListDetail']; }; }; /** @description Validation Error */ @@ -5016,7 +4910,7 @@ export interface operations { }; }; }; - get_judgment_list_endpoint_api_v1_judgment_lists__judgment_list_id__get: { + calibrate_judgment_list_api_v1_judgment_lists__judgment_list_id__calibration_post: { parameters: { query?: never; header?: never; @@ -5025,7 +4919,11 @@ export interface operations { }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + 'application/json': components['schemas']['CalibrationSamplesRequest']; + }; + }; responses: { /** @description Successful Response */ 200: { @@ -5033,7 +4931,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['JudgmentListDetail']; + 'application/json': components['schemas']['CalibrationResponse']; }; }; /** @description Validation Error */ @@ -5128,28 +5026,26 @@ export interface operations { }; }; }; - calibrate_judgment_list_api_v1_judgment_lists__judgment_list_id__calibration_post: { + generate_judgments_api_v1_judgments_generate_post: { parameters: { query?: never; header?: never; - path: { - judgment_list_id: string; - }; + path?: never; cookie?: never; }; requestBody: { content: { - 'application/json': components['schemas']['CalibrationSamplesRequest']; + 'application/json': components['schemas']['CreateJudgmentListGenerateRequest']; }; }; responses: { /** @description Successful Response */ - 200: { + 202: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['CalibrationResponse']; + 'application/json': components['schemas']['GenerateJudgmentsResponse']; }; }; /** @description Validation Error */ @@ -5163,24 +5059,26 @@ export interface operations { }; }; }; - get_study_digest_api_v1_studies__study_id__digest_get: { + generate_judgments_from_ubi_api_v1_judgments_generate_from_ubi_post: { parameters: { query?: never; header?: never; - path: { - study_id: string; - }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + 'application/json': components['schemas']['CreateJudgmentListFromUbiRequest']; + }; + }; responses: { /** @description Successful Response */ - 200: { + 202: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['DigestResponse']; + 'application/json': components['schemas']['GenerateJudgmentsResponse']; }; }; /** @description Validation Error */ @@ -5306,7 +5204,7 @@ export interface operations { }; }; }; - reject_proposal_endpoint_api_v1_proposals__proposal_id__reject_post: { + open_pr_endpoint_api_v1_proposals__proposal_id__open_pr_post: { parameters: { query?: never; header?: never; @@ -5315,19 +5213,15 @@ export interface operations { }; cookie?: never; }; - requestBody: { - content: { - 'application/json': components['schemas']['RejectProposalRequest']; - }; - }; + requestBody?: never; responses: { /** @description Successful Response */ - 200: { + 202: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['ProposalDetail']; + 'application/json': components['schemas']['OpenPrResponse']; }; }; /** @description Validation Error */ @@ -5341,7 +5235,7 @@ export interface operations { }; }; }; - open_pr_endpoint_api_v1_proposals__proposal_id__open_pr_post: { + reject_proposal_endpoint_api_v1_proposals__proposal_id__reject_post: { parameters: { query?: never; header?: never; @@ -5350,15 +5244,19 @@ export interface operations { }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + 'application/json': components['schemas']['RejectProposalRequest']; + }; + }; responses: { /** @description Successful Response */ - 202: { + 200: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['OpenPrResponse']; + 'application/json': components['schemas']['ProposalDetail']; }; }; /** @description Validation Error */ @@ -5372,11 +5270,14 @@ export interface operations { }; }; }; - list_config_repos_endpoint_api_v1_config_repos_get: { + list_query_sets_api_v1_query_sets_get: { parameters: { query?: { cursor?: string | null; limit?: number; + since?: string | null; + q?: string | null; + sort?: ('name:asc' | 'name:desc' | 'created_at:asc' | 'created_at:desc') | null; }; header?: never; path?: never; @@ -5390,7 +5291,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['ConfigReposListResponse']; + 'application/json': components['schemas']['QuerySetListResponse']; }; }; /** @description Validation Error */ @@ -5404,26 +5305,92 @@ export interface operations { }; }; }; - create_config_repo_endpoint_api_v1_config_repos_post: { + create_query_set_api_v1_query_sets_post: { parameters: { query?: never; header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateConfigRepoRequest']; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['CreateQuerySetRequest']; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['QuerySetDetail']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_query_set_detail_api_v1_query_sets__query_set_id__get: { + parameters: { + query?: never; + header?: never; + path: { + query_set_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['QuerySetDetail']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + list_queries_in_set_api_v1_query_sets__query_set_id__queries_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + since?: string | null; + }; + header?: never; + path: { + query_set_id: string; }; + cookie?: never; }; + requestBody?: never; responses: { /** @description Successful Response */ - 201: { + 200: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['ConfigRepoDetail']; + 'application/json': components['schemas']['QueryListResponse']; }; }; /** @description Validation Error */ @@ -5437,24 +5404,24 @@ export interface operations { }; }; }; - get_config_repo_endpoint_api_v1_config_repos__config_repo_id__get: { + bulk_add_queries_api_v1_query_sets__query_set_id__queries_post: { parameters: { query?: never; header?: never; path: { - config_repo_id: string; + query_set_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Successful Response */ - 200: { + 201: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['ConfigRepoDetail']; + 'application/json': components['schemas']['BulkQueriesResponse']; }; }; /** @description Validation Error */ @@ -5468,27 +5435,32 @@ export interface operations { }; }; }; - list_conversations_endpoint_api_v1_conversations_get: { + delete_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__delete: { parameters: { - query?: { - cursor?: string | null; - limit?: number; - since?: string | null; - q?: string | null; - }; + query?: never; header?: never; - path?: never; + path: { + query_set_id: string; + query_id: string; + }; cookie?: never; }; requestBody?: never; responses: { /** @description Successful Response */ - 200: { + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Conflict */ + 409: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['ConversationsListResponse']; + 'application/json': components['schemas']['QueryHasJudgmentsEnvelope']; }; }; /** @description Validation Error */ @@ -5502,26 +5474,29 @@ export interface operations { }; }; }; - create_conversation_endpoint_api_v1_conversations_post: { + update_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__patch: { parameters: { query?: never; header?: never; - path?: never; + path: { + query_set_id: string; + query_id: string; + }; cookie?: never; }; requestBody: { content: { - 'application/json': components['schemas']['CreateConversationRequest']; + 'application/json': components['schemas']['UpdateQueryRequest']; }; }; responses: { /** @description Successful Response */ - 201: { + 200: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['ConversationSummary']; + 'application/json': components['schemas']['QueryRow']; }; }; /** @description Validation Error */ @@ -5535,13 +5510,29 @@ export interface operations { }; }; }; - get_conversation_endpoint_api_v1_conversations__conversation_id__get: { + list_query_templates_api_v1_query_templates_get: { parameters: { - query?: never; - header?: never; - path: { - conversation_id: string; + query?: { + cursor?: string | null; + limit?: number; + since?: string | null; + q?: string | null; + sort?: + | ( + | 'name:asc' + | 'name:desc' + | 'created_at:asc' + | 'created_at:desc' + | 'engine_type:asc' + | 'engine_type:desc' + | 'version:asc' + | 'version:desc' + ) + | null; + engine_type?: ('elasticsearch' | 'opensearch' | 'solr') | null; }; + header?: never; + path?: never; cookie?: never; }; requestBody?: never; @@ -5552,7 +5543,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['ConversationDetail']; + 'application/json': components['schemas']['QueryTemplateListResponse']; }; }; /** @description Validation Error */ @@ -5566,23 +5557,27 @@ export interface operations { }; }; }; - delete_conversation_endpoint_api_v1_conversations__conversation_id__delete: { + create_query_template_api_v1_query_templates_post: { parameters: { query?: never; header?: never; - path: { - conversation_id: string; - }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + 'application/json': components['schemas']['CreateQueryTemplateRequest']; + }; + }; responses: { /** @description Successful Response */ - 204: { + 201: { headers: { [name: string]: unknown; }; - content?: never; + content: { + 'application/json': components['schemas']['QueryTemplateDetail']; + }; }; /** @description Validation Error */ 422: { @@ -5595,20 +5590,16 @@ export interface operations { }; }; }; - post_message_endpoint_api_v1_conversations__conversation_id__messages_post: { + get_query_template_detail_api_v1_query_templates__template_id__get: { parameters: { query?: never; header?: never; path: { - conversation_id: string; + template_id: string; }; cookie?: never; }; - requestBody: { - content: { - 'application/json': components['schemas']['SendMessageRequest']; - }; - }; + requestBody?: never; responses: { /** @description Successful Response */ 200: { @@ -5616,7 +5607,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': unknown; + 'application/json': components['schemas']['QueryTemplateDetail']; }; }; /** @description Validation Error */ @@ -5630,26 +5621,44 @@ export interface operations { }; }; }; - seed_completed_study_api_v1__test_studies_seed_completed_post: { + list_studies_api_v1_studies_get: { parameters: { - query?: never; + query?: { + cursor?: string | null; + limit?: number; + since?: string | null; + status?: ('queued' | 'running' | 'completed' | 'cancelled' | 'failed') | null; + cluster_id?: string | null; + target?: string | null; + q?: string | null; + sort?: + | ( + | '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' + ) + | null; + }; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - 'application/json': components['schemas']['SeedCompletedStudyRequest']; - }; - }; + requestBody?: never; responses: { /** @description Successful Response */ - 201: { + 200: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['SeedCompletedStudyResponse']; + 'application/json': components['schemas']['StudyListResponse']; }; }; /** @description Validation Error */ @@ -5663,7 +5672,7 @@ export interface operations { }; }; }; - seed_auto_followup_chain_endpoint_api_v1__test_auto_followup_seed_chain_post: { + create_study_api_v1_studies_post: { parameters: { query?: never; header?: never; @@ -5672,7 +5681,7 @@ export interface operations { }; requestBody: { content: { - 'application/json': components['schemas']['SeedAutoFollowupChainRequest']; + 'application/json': components['schemas']['CreateStudyRequest']; }; }; responses: { @@ -5682,7 +5691,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['SeedAutoFollowupChainResponse']; + 'application/json': components['schemas']['StudyDetail']; }; }; /** @description Validation Error */ @@ -5696,23 +5705,25 @@ export interface operations { }; }; }; - delete_test_proposal_api_v1__test_proposals__proposal_id__delete: { + get_study_detail_api_v1_studies__study_id__get: { parameters: { query?: never; header?: never; path: { - proposal_id: string; + study_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Successful Response */ - 204: { + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + 'application/json': components['schemas']['StudyDetail']; + }; }; /** @description Validation Error */ 422: { @@ -5725,23 +5736,27 @@ export interface operations { }; }; }; - delete_test_digest_api_v1__test_digests__digest_id__delete: { + cancel_study_api_v1_studies__study_id__cancel_post: { parameters: { - query?: never; + query?: { + cascade?: string; + }; header?: never; path: { - digest_id: string; + study_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Successful Response */ - 204: { + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + 'application/json': components['schemas']['StudyDetail']; + }; }; /** @description Validation Error */ 422: { @@ -5754,7 +5769,7 @@ export interface operations { }; }; }; - delete_test_study_api_v1__test_studies__study_id__delete: { + get_study_chain_api_v1_studies__study_id__chain_get: { parameters: { query?: never; header?: never; @@ -5766,11 +5781,13 @@ export interface operations { requestBody?: never; responses: { /** @description Successful Response */ - 204: { + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + 'application/json': components['schemas']['StudyChainResponse']; + }; }; /** @description Validation Error */ 422: { @@ -5783,23 +5800,25 @@ export interface operations { }; }; }; - delete_test_judgment_list_api_v1__test_judgment_lists__judgment_list_id__delete: { + list_study_children_api_v1_studies__study_id__children_get: { parameters: { query?: never; header?: never; path: { - judgment_list_id: string; + study_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Successful Response */ - 204: { + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + 'application/json': components['schemas']['StudyListResponse']; + }; }; /** @description Validation Error */ 422: { @@ -5812,23 +5831,25 @@ export interface operations { }; }; }; - delete_test_query_set_api_v1__test_query_sets__query_set_id__delete: { + get_study_digest_api_v1_studies__study_id__digest_get: { parameters: { query?: never; header?: never; path: { - query_set_id: string; + study_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Successful Response */ - 204: { + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + 'application/json': components['schemas']['DigestResponse']; + }; }; /** @description Validation Error */ 422: { @@ -5841,23 +5862,30 @@ export interface operations { }; }; }; - delete_test_query_template_api_v1__test_query_templates__template_id__delete: { + list_study_trials_api_v1_studies__study_id__trials_get: { parameters: { - query?: never; + query?: { + cursor?: string | null; + limit?: number; + since?: string | null; + sort?: string; + }; header?: never; path: { - template_id: string; + study_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Successful Response */ - 204: { + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + 'application/json': components['schemas']['TrialListResponse']; + }; }; /** @description Validation Error */ 422: { @@ -5870,7 +5898,7 @@ export interface operations { }; }; }; - reseed_demo_api_v1__test_demo_reseed_post: { + healthz_healthz_get: { parameters: { query?: never; header?: never; @@ -5880,32 +5908,21 @@ export interface operations { requestBody?: never; responses: { /** @description Successful Response */ - 202: { + 200: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['ReseedStatusResponse']; + 'application/json': components['schemas']['HealthResponse']; }; }; - }; - }; - reseed_demo_status_api_v1__test_demo_reseed_status_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { + /** @description One or more required subsystems is down */ + 503: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['ReseedStatusResponse']; + 'application/json': components['schemas']['HealthResponse']; }; }; }; From 3da3896e6aec1c54bd17be892de54ab9351159c8 Mon Sep 17 00:00:00 2001 From: SoundMindsAI Date: Tue, 2 Jun 2026 23:28:33 -0400 Subject: [PATCH 08/10] infra(regen): canonical chained fix command + determinism wrap-up (Story 2.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Story 2.4 of infra_generated_artifact_freshness_gate (FR-8 chained + FR-6 determinism + AC-7). - scripts/regen-generated-artifacts.sh (new) — one-paste chained regen for all three CI-freshness-gated artifacts: 1. ui/openapi.json (uv run python -m backend.app.openapi_export) 2. ui/src/lib/types.ts (pnpm types:gen, reading the snapshot at 1) 3. ui/public/docs/ (node ui/scripts/copy-docs.mjs) Step ordering matters — types.ts derives from the snapshot, so the snapshot must regenerate first. After regen, all three are `git add`ed. REGEN_NO_STAGE=1 skips the staging step (used by CI's AC-7 determinism assertion so it inspects the working tree directly). - ui/.prettierignore (new) — generated files are NOT prettier-formatted. `ui/src/lib/types.ts` (openapi-typescript output) and `ui/public/docs/*.md` (copy-docs.mjs output) are listed; the generator is the source of truth. Without this, prettier would reformat the openapi-typescript output and the freshness gate would flap between local-prettier-formatted and CI-canonical bytes. - ui/src/lib/types.ts — regenerated via the canonical wrapper, NOT prettier-formatted. This is what every future regen produces and what the gate now expects. Two consecutive `bash scripts/regen- generated-artifacts.sh` invocations against this commit's tree produce byte-identical types.ts — FR-6 verified. - scripts/ci/verify_*.sh — all three guards now point their fix- command output at the canonical chained wrapper as the primary, with the per-gate one-liner shown as a fallback. Self-tests still green (7+7+7 = 21 cases) because the existing per-gate substrings remain in the output. - .github/workflows/pr.yml — appends an AC-7 clean-tree determinism step to the generated-artifacts-fresh job. After both per-gate guards have run, the step does a fresh canonical regen + asserts the working tree is clean. Catches a regenerator that is itself non-deterministic across runs, distinct from drift against the committed snapshot. - docs/05_quality/testing.md — promotes the chained wrapper as the single canonical fix command, demotes per-gate fixes to a fallback section, names the AC-7 determinism assertion, documents the `.prettierignore` rationale. - CLAUDE.md — adds a "Generated artifacts" subsection under Key Conventions naming the chained regen + the prettier-ignore rule. Verification: 21/21 self-test cases green (7 per guard); canonical regen output is byte-identical across consecutive runs (FR-6); a fresh regen against the committed tree leaves git status clean (AC-7); pr.yml parses cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: SoundMindsAI --- .github/workflows/pr.yml | 17 + CLAUDE.md | 18 + docs/05_quality/testing.md | 34 +- scripts/ci/verify_copy_docs_fresh.sh | 6 +- scripts/ci/verify_openapi_snapshot_fresh.sh | 6 +- scripts/ci/verify_types_fresh.sh | 5 +- scripts/regen-generated-artifacts.sh | 70 + ui/.prettierignore | 21 + ui/src/lib/types.ts | 11755 +++++++++--------- 9 files changed, 6000 insertions(+), 5932 deletions(-) create mode 100755 scripts/regen-generated-artifacts.sh create mode 100644 ui/.prettierignore diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a922a4d1..bf903e94 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -261,6 +261,23 @@ jobs: run: bash scripts/ci/test_verify_types_fresh.sh - name: Verify ui/src/lib/types.ts is fresh run: bash scripts/ci/verify_types_fresh.sh + - name: Clean-tree determinism assertion (AC-7) + # After both guards have run their regenerators, the working + # tree must be clean across all three artifacts. Catches a + # regenerator that is itself non-deterministic across runs + # (the failure mode `infra_generated_artifact_freshness_gate` + # FR-6 names) — distinct from drift against the committed + # snapshot, which the two guards above catch. + run: | + REGEN_NO_STAGE=1 bash scripts/regen-generated-artifacts.sh + DRIFT="$(git status --porcelain -- ui/openapi.json ui/src/lib/types.ts ui/public/docs)" + if [[ -n "${DRIFT}" ]]; then + echo "ERROR: regenerator output is non-deterministic across runs." >&2 + echo "Drift after a fresh canonical regen:" >&2 + printf '%s\n' "${DRIFT}" >&2 + exit 1 + fi + echo "OK: clean-tree determinism assertion passed." # ------------------------------------------------------------------------- # Static checks — ALWAYS run (not gated by SKIP_HEAVY_CI). Split into two diff --git a/CLAUDE.md b/CLAUDE.md index 89207f85..5be0b0d6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -258,6 +258,24 @@ Additional workflows (deploy-staging, release, image-publish) ship at MVP3 + GA - DB revision guard at API startup is **MVP2+** — MVP1 doesn't fail-fast on pending migrations (would crash the dev stack on first boot before `make migrate` runs). - Revision IDs are ≤32 chars (Alembic's `version_num` column is `VARCHAR(32)`); the `0001_baseline` convention stays well under. +### Generated artifacts + +`ui/openapi.json` (offline OpenAPI snapshot), `ui/src/lib/types.ts` +(generated by `openapi-typescript` from the snapshot), and +`ui/public/docs/*.md` (copied from `docs/08_guides/`) are +**CI-freshness-gated**. The `generated-artifacts-fresh` job in +`pr.yml` + the standalone `copy-docs-freshness` workflow regenerate +them on every PR and fail on `git status --porcelain` drift. The +single canonical fix: + +```bash +bash scripts/regen-generated-artifacts.sh +``` + +Generated files are listed in `ui/.prettierignore` — the generator +is the source of truth, do not run prettier on them. See +[`docs/05_quality/testing.md` §"Generated-artifact freshness gates"](docs/05_quality/testing.md). + ## Testing Conventions | Layer | Location | DB? | Notes | diff --git a/docs/05_quality/testing.md b/docs/05_quality/testing.md index d3d46472..fd1e841f 100644 --- a/docs/05_quality/testing.md +++ b/docs/05_quality/testing.md @@ -259,7 +259,25 @@ markers) — every drift mode the gate exists to catch. | 2 | `generated-artifacts-fresh` (snapshot step) | `pr.yml` job — backend (`**/*.py`) + ui (`**/*.ts`) changes can both invalidate the snapshot, so the gate runs on every code-bearing PR | backend FastAPI route table → `ui/openapi.json` | `uv run python -m backend.app.openapi_export --out ui/openapi.json` (offline, no live services per Story 2.1) | `scripts/ci/test_verify_openapi_snapshot_fresh.sh` uses an `OPENAPI_SNAPSHOT_REGEN_SCRIPT` path-override + a disposable `mktemp` fixture to test the guard's diff-detection without needing `uv` in the fixture (the exporter has its own Story-2.1 unit test) | | 3 | `generated-artifacts-fresh` (types step) | same `pr.yml` job — chained after the snapshot step so they share the toolchain install | `ui/openapi.json` → `ui/src/lib/types.ts` | `OPENAPI_URL="$PWD/ui/openapi.json" pnpm --dir ui types:gen` (wraps the lockfile-pinned `openapi-typescript@7.x` binary; Story 2.3 ditched the `npx` fallback per FR-5) | `scripts/ci/test_verify_types_fresh.sh` uses a `TYPES_FRESH_REGEN_SCRIPT` path-override against a disposable fixture; `ui/src/__tests__/scripts/gen-types-banner.test.ts` covers banner source-invariance (FR-5 / AC-8) | -The fix commands printed on failure: +The single canonical fix command for ALL three gates: + +```bash +bash scripts/regen-generated-artifacts.sh +``` + +`scripts/regen-generated-artifacts.sh` chains the three regenerators in +the order their outputs depend on each other — exporter writes +`ui/openapi.json`; `pnpm types:gen` reads that snapshot to produce +`ui/src/lib/types.ts`; `copy-docs.mjs` is independent — then `git add`s +all three. Running it on an up-to-date tree is a clean no-op. CI runs a +**clean-tree determinism assertion** (AC-7) after the two per-gate +guards: a fresh regen against a clean tree must leave `git status +--porcelain -- ui/openapi.json ui/src/lib/types.ts ui/public/docs` +empty. That catches a regenerator that is itself non-deterministic +across runs (the FR-6 failure mode), distinct from drift against the +committed snapshots that the per-gate guards above catch. + +Per-gate fix commands (if you'd rather refresh just one artifact): ```bash # Gate 1 (copy-docs) @@ -268,11 +286,19 @@ cd ui && node scripts/copy-docs.mjs && git add public/docs # Gate 2 (openapi.json snapshot) uv run python -m backend.app.openapi_export --out ui/openapi.json && git add ui/openapi.json -# Gate 3 (types.ts) — refreshes the snapshot too, so use the chained -# fix landing in Story 2.4 (`scripts/regen-generated-artifacts.sh`) -bash scripts/regen-generated-artifacts.sh && git add ui/openapi.json ui/src/lib/types.ts +# Gate 3 (types.ts — depends on the snapshot at gate 2, so the chained +# regen above is usually the right call) +( cd ui && OPENAPI_URL="$PWD/openapi.json" pnpm types:gen ) && git add ui/src/lib/types.ts ``` +**Generated files are NOT prettier-formatted.** `ui/src/lib/types.ts` +(emitted by `openapi-typescript`) and `ui/public/docs/*.md` (copied +from `docs/08_guides/`) are listed in `ui/.prettierignore` — the +generator is the source of truth, and reformatting their output would +make the gates flap. If you edit a guide or change a backend route, +run the canonical regen above; do not run prettier on the generated +artifacts. + The freshness-gate scripts (`scripts/ci/verify_copy_docs_fresh.sh` + its self-test) follow the canonical `scripts/ci/` shape: `set -euo pipefail`, SPDX header, `git status --porcelain` (never bare `git diff`), and a diff --git a/scripts/ci/verify_copy_docs_fresh.sh b/scripts/ci/verify_copy_docs_fresh.sh index 851967d1..f9f5e584 100755 --- a/scripts/ci/verify_copy_docs_fresh.sh +++ b/scripts/ci/verify_copy_docs_fresh.sh @@ -46,8 +46,10 @@ DRIFT="$(git status --porcelain -- ui/public/docs/)" if [[ -n "${DRIFT}" ]]; then echo "ERROR: ui/public/docs/ is stale." >&2 - echo "Fix with:" >&2 - echo " cd ui && node scripts/copy-docs.mjs && git add public/docs" >&2 + echo "Fix with the canonical chained regen (Story 2.4):" >&2 + echo " bash scripts/regen-generated-artifacts.sh" >&2 + echo "(or this gate alone:" >&2 + echo " cd ui && node scripts/copy-docs.mjs && git add public/docs)" >&2 echo >&2 echo "Drift detected (diagnostic):" >&2 printf '%s\n' "${DRIFT}" >&2 diff --git a/scripts/ci/verify_openapi_snapshot_fresh.sh b/scripts/ci/verify_openapi_snapshot_fresh.sh index c8481722..e4f57617 100755 --- a/scripts/ci/verify_openapi_snapshot_fresh.sh +++ b/scripts/ci/verify_openapi_snapshot_fresh.sh @@ -61,8 +61,10 @@ DRIFT="$(git status --porcelain -- ui/openapi.json)" if [[ -n "${DRIFT}" ]]; then echo "ERROR: ui/openapi.json is stale." >&2 - echo "Fix with:" >&2 - echo " uv run python -m backend.app.openapi_export --out ui/openapi.json && git add ui/openapi.json" >&2 + echo "Fix with the canonical chained regen (Story 2.4):" >&2 + echo " bash scripts/regen-generated-artifacts.sh" >&2 + echo "(or this gate alone:" >&2 + echo " uv run python -m backend.app.openapi_export --out ui/openapi.json && git add ui/openapi.json)" >&2 echo >&2 echo "Drift detected (diagnostic):" >&2 printf '%s\n' "${DRIFT}" >&2 diff --git a/scripts/ci/verify_types_fresh.sh b/scripts/ci/verify_types_fresh.sh index 03931d18..841c37bb 100755 --- a/scripts/ci/verify_types_fresh.sh +++ b/scripts/ci/verify_types_fresh.sh @@ -64,8 +64,9 @@ DRIFT="$(git status --porcelain -- ui/src/lib/types.ts)" if [[ -n "${DRIFT}" ]]; then echo "ERROR: ui/src/lib/types.ts is stale." >&2 - echo "Fix with:" >&2 - echo " bash scripts/regen-generated-artifacts.sh && git add ui/openapi.json ui/src/lib/types.ts" >&2 + echo "Fix with the canonical chained regen (Story 2.4):" >&2 + echo " bash scripts/regen-generated-artifacts.sh" >&2 + echo "(this also refreshes ui/openapi.json + ui/public/docs/ in lockstep.)" >&2 echo >&2 echo "Drift detected (diagnostic):" >&2 printf '%s\n' "${DRIFT}" >&2 diff --git a/scripts/regen-generated-artifacts.sh b/scripts/regen-generated-artifacts.sh new file mode 100755 index 00000000..8684cd3a --- /dev/null +++ b/scripts/regen-generated-artifacts.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2026 soundminds.ai +# +# SPDX-License-Identifier: Apache-2.0 + +# Story 2.4 of infra_generated_artifact_freshness_gate (FR-8 chained +# fix + FR-6 determinism). +# +# One-paste fix command that regenerates ALL three CI-freshness-gated +# generated artifacts in lockstep, then stages them for commit. When a +# freshness gate fails on a PR, the gate's diagnostic output points +# operators here so they don't have to chain four separate commands +# themselves. +# +# What it regenerates: +# +# 1. ui/openapi.json +# via `uv run python -m backend.app.openapi_export --out ui/openapi.json` +# (offline; no live services; canonical sort_keys=True JSON form per +# Story 2.1 / FR-4). +# +# 2. ui/src/lib/types.ts +# via `OPENAPI_URL="$PWD/ui/openapi.json" pnpm --dir ui types:gen` +# (uses the lockfile-pinned `openapi-typescript` binary; reads +# from the snapshot at step 1; source-invariant banner per +# Story 2.3 / FR-5). +# +# 3. ui/public/docs/*.md +# via `(cd ui && node scripts/copy-docs.mjs)` +# (copies guides from docs/08_guides/ + prunes to exact set per +# Story 1.1 / FR-9). +# +# Step ordering matters: types.ts is generated FROM the snapshot, so +# the snapshot must be regenerated first. copy-docs is independent and +# could go anywhere, but is run last so its diagnostic output appears +# at the bottom — easier to spot a missing source file. +# +# After running, the three artifacts are `git add`ed so a subsequent +# `git commit` picks them up. Re-running on an up-to-date tree is a +# clean no-op. +# +# Usage (from anywhere in the repo): +# +# bash scripts/regen-generated-artifacts.sh +# +# Honors REGEN_NO_STAGE=1 to skip the final `git add` (used by CI's +# clean-tree determinism assertion — it wants to inspect the working +# tree directly, not the index). + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${REPO_ROOT}" + +echo "[regen] 1/3: ui/openapi.json (offline exporter)" +uv run python -m backend.app.openapi_export --out ui/openapi.json + +echo "[regen] 2/3: ui/src/lib/types.ts (from committed snapshot)" +( cd ui && OPENAPI_URL="${REPO_ROOT}/ui/openapi.json" pnpm types:gen ) + +echo "[regen] 3/3: ui/public/docs/ (from docs/08_guides/)" +( cd ui && node scripts/copy-docs.mjs ) + +if [[ "${REGEN_NO_STAGE:-}" != "1" ]]; then + git add ui/openapi.json ui/src/lib/types.ts ui/public/docs + echo "[regen] done — three artifacts regenerated and staged." +else + echo "[regen] done — three artifacts regenerated (REGEN_NO_STAGE=1, not staged)." +fi diff --git a/ui/.prettierignore b/ui/.prettierignore new file mode 100644 index 00000000..2512ec24 --- /dev/null +++ b/ui/.prettierignore @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2026 soundminds.ai +# +# SPDX-License-Identifier: Apache-2.0 + +# Generated files where the generator IS the source of truth. +# +# `src/lib/types.ts` is emitted by `openapi-typescript` (via +# `pnpm types:gen` / `node scripts/gen-types.mjs`) from the committed +# `ui/openapi.json` snapshot. The `generated-artifacts-fresh` CI job +# (`infra_generated_artifact_freshness_gate`) regenerates the file +# and fails on `git status --porcelain` drift, so running prettier +# on it would produce bytes that differ from the canonical generator +# output and the gate would flap. Leave it in openapi-typescript's +# native format. +# +# `public/docs/*.md` is copied verbatim from `docs/08_guides/*.md` +# by `scripts/copy-docs.mjs`. Letting prettier reformat the public +# copy would cause the same drift between the source and the +# committed output. +src/lib/types.ts +public/docs/*.md diff --git a/ui/src/lib/types.ts b/ui/src/lib/types.ts index c63715d2..4fe9649a 100644 --- a/ui/src/lib/types.ts +++ b/ui/src/lib/types.ts @@ -18,5935 +18,5846 @@ */ export interface paths { - '/api/v1/_test/auto-followup/seed-chain': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/api/v1/_test/auto-followup/seed-chain": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Seed an auto-followup chain of N+1 linked studies + * @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. + */ + post: operations["seed_auto_followup_chain_endpoint_api_v1__test_auto_followup_seed_chain_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/_test/demo/reseed": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Enqueue a demo-state reseed (dev-only, async) + * @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. + * + * Per ``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. + */ + post: operations["reseed_demo_api_v1__test_demo_reseed_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/_test/demo/reseed/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Poll the current demo-reseed progress (dev-only) + * @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. + */ + get: operations["reseed_demo_status_api_v1__test_demo_reseed_status_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/_test/digests/{digest_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Hard-delete a digest (test-only) + * @description FR-2: Hard-delete the digest row. No FK children — no preflight needed. + */ + delete: operations["delete_test_digest_api_v1__test_digests__digest_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/_test/judgment-lists/{judgment_list_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Hard-delete a judgment_list (test-only) + * @description FR-4 — hard-delete the judgment_list row. + * + * Judgments cascade-delete via existing FK. Preflight-checks ``studies`` + * (non-cascade); 409 if any study references the judgment_list. + */ + delete: operations["delete_test_judgment_list_api_v1__test_judgment_lists__judgment_list_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/_test/proposals/{proposal_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Hard-delete a proposal (test-only) + * @description FR-1: Hard-delete the proposal row. No FK children — no preflight needed. + */ + delete: operations["delete_test_proposal_api_v1__test_proposals__proposal_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/_test/query-sets/{query_set_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Hard-delete a query_set (test-only) + * @description FR-5 — hard-delete the query_set row. + * + * Queries cascade-delete via existing FK. Preflight-checks ``studies`` + * + ``judgment_lists`` (both non-cascade); 409 with resource-specific + * code if either references. + */ + delete: operations["delete_test_query_set_api_v1__test_query_sets__query_set_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/_test/query-templates/{template_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Hard-delete a query_template (test-only) + * @description FR-6 — hard-delete the query_template row. + * + * No FK children cascade with template. Preflight-checks ``studies``, + * ``proposals``, and ``judgment_lists.current_template_id`` in + * **fixed priority order: STUDY > PROPOSAL > JUDGMENT_LIST** (per + * spec §FR-6) — first match wins. + */ + delete: operations["delete_test_query_template_api_v1__test_query_templates__template_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/_test/studies/seed-completed": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Seed a completed study + digest + (optional) pending proposal + * @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. + */ + post: operations["seed_completed_study_api_v1__test_studies_seed_completed_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/_test/studies/{study_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Hard-delete a study (test-only) + * @description FR-3 — hard-delete the study row. + * + * Trials cascade-delete via existing FK. Preflight-checks ``proposals`` + * + ``digests`` (both non-cascade); 409 if any dependent rows reference + * the study. + */ + delete: operations["delete_test_study_api_v1__test_studies__study_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/clusters": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Clusters + * @description List clusters with cursor pagination + ``X-Total-Count`` header. + * + * ``?q=`` is a Postgres FTS match against the cluster's ``search_vector`` + * (name + base_url); 2–200 chars. Filter-only — ordering unchanged per + * spec FR-1. ``?sort=`` is one of the values in + * :data:`~backend.app.api.v1.schemas.ClusterSortKey`; the cursor is + * sort-aware so the keyset predicate matches the active ORDER BY + * (feat_data_table_primitive Stories 1.2 + 1.3). + */ + get: operations["list_clusters_api_v1_clusters_get"]; + put?: never; + /** + * Create Cluster + * @description Register a cluster (FR-5 / AC-1). + */ + post: operations["create_cluster_api_v1_clusters_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/clusters/test-connection": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Test Connection + * @description Probe a cluster config WITHOUT persisting (infra_adapter_solr Story A9). + * + * Powers the registration modal's "Test connection" button. Always 200 — + * transport failures surface as ``reachable=false`` with ``error`` set. + * Invalid engine×auth pairings 400 BEFORE the network call. + */ + post: operations["test_connection_api_v1_clusters_test_connection_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/clusters/{cluster_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Cluster Detail + * @description Return cluster row + cached/fresh health probe. + */ + get: operations["get_cluster_detail_api_v1_clusters__cluster_id__get"]; + put?: never; + post?: never; + /** + * Delete Cluster + * @description Soft-delete a cluster (AC-8). Returns 204 with no body. + */ + delete: operations["delete_cluster_api_v1_clusters__cluster_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/clusters/{cluster_id}/reprobe": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Reprobe Cluster + * @description Re-run cluster capability probe (Story A9 / spec FR-2 + AC-14). + * + * Concurrent calls serialize on ``SELECT … FOR UPDATE``. On probe failure + * the row's engine_config is NOT updated (the transaction rolls back). + */ + post: operations["reprobe_cluster_api_v1_clusters__cluster_id__reprobe_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/clusters/{cluster_id}/run_query": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Run Query + * @description Execute one query DSL fragment against the cluster (FR-6 / AC-3). + */ + post: operations["run_query_api_v1_clusters__cluster_id__run_query_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/clusters/{cluster_id}/schema": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Cluster Schema + * @description Return the field schema for ``target`` (FR-4 / AC-2). + */ + get: operations["get_cluster_schema_api_v1_clusters__cluster_id__schema_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/clusters/{cluster_id}/targets": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Cluster Targets + * @description List targets (indices/collections) on the cluster (FR-1 / AC-1). + * + * Thin passthrough to ``ElasticAdapter.list_targets()`` (which filters out + * system indices whose names start with ``.``). Mirrors the ``get_cluster_schema`` + * pattern: ``get_cluster`` → ``acquire_adapter`` async context → adapter call + * → translate exceptions via the ``_err()`` helper to the spec §7.5 envelope. + * + * Error mapping: + * * cluster missing or soft-deleted → 404 ``CLUSTER_NOT_FOUND`` (retryable=false) + * * adapter raises ``TargetsForbiddenError`` (ACL 401/403) → 403 + * ``TARGETS_FORBIDDEN`` (retryable=false) — frontend auto-engages manual mode + * * adapter raises ``ClusterUnreachableError`` (5xx / connection failure) → 503 + * ``CLUSTER_UNREACHABLE`` (retryable=true) + */ + get: operations["list_cluster_targets_api_v1_clusters__cluster_id__targets_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/clusters/{cluster_id}/targets/{target}/documents": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Target Documents + * @description Paginated _id + truncated _source preview for a target (FR-3). + * + * The endpoint asks the adapter for ``limit + 1`` rows so it can detect + * end-of-data exactly (no extra round-trip). Only the first ``limit`` rows + * are returned; ``next_cursor`` encodes the ES ``hits[i].sort`` of the + * last visible row when ``has_more`` is True. ``X-Total-Count`` header + * carries the engine's ``hits.total.value``. + */ + get: operations["list_target_documents_api_v1_clusters__cluster_id__targets__target__documents_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/clusters/{cluster_id}/targets/{target}/documents/{doc_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Target Document + * @description Fetch one document by ``_id`` (FR-4). + * + * FastAPI's ``{doc_id:path}`` converter round-trips slashes verbatim, so + * operator IDs containing ``/`` are supported (D-17 / AC-16). Returns the + * adapter ``Document`` shape directly; on ``found: false`` returns 404 + * ``DOCUMENT_NOT_FOUND`` (distinct from ``TARGET_NOT_FOUND``). + */ + get: operations["get_target_document_api_v1_clusters__cluster_id__targets__target__documents__doc_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/clusters/{cluster_id}/ubi-readiness": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Cluster Ubi Readiness + * @description Classify ``(cluster, query_set, target)`` on the UBI rung ladder. + * + * feat_ubi_judgments FR-7. + * + * Required query params: ``query_set_id`` + ``target`` (Spec FR-7 + + * cycle-3 D-10c: the endpoint MUST 422 without them — the classifier + * can't compute a per-target rung without an application filter). + * + * Error envelopes (all per spec §7.5): + * * ``404 CLUSTER_NOT_FOUND`` — cluster row missing or soft-deleted. + * * ``404 QUERY_SET_NOT_FOUND`` — query set row missing. + * * ``422 VALIDATION_ERROR`` — missing required query params (FastAPI's + * built-in handler, surfaces via ``api/errors.py``). + * * ``503 CLUSTER_UNREACHABLE`` — adapter cannot reach the cluster. + * + * The result is cached for 60 s in Redis per + * ``(cluster_id, query_set_id, target)`` so back-to-back dialog-open + * and dialog-submit calls don't re-probe. + */ + get: operations["get_cluster_ubi_readiness_api_v1_clusters__cluster_id__ubi_readiness_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/config-repos": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Config Repos Endpoint + * @description Cursor-paginated config-repo list, newest first. + */ + get: operations["list_config_repos_endpoint_api_v1_config_repos_get"]; + put?: never; + /** + * Create Config Repo Endpoint + * @description Register a new config repo. ``provider`` is server-derived from ``repo_url``. + * + * Preflight order matches spec FR-3: + * + * 1. ``validate_repo_url(repo_url)`` → 400 ``UNSUPPORTED_PROVIDER`` for + * non-GitHub URLs (AC-8). GitLab + Bitbucket arrive at MVP3. + * 2. ``./secrets/{auth_ref}`` must exist → else 400 ``AUTH_REF_NOT_FOUND`` + * (AC-9). The contents check defers to the worker — operators may + * populate the file between registration and first PR-open. + * 3. ``name`` uniqueness check → 409 ``CONFIG_REPO_NAME_TAKEN`` on collision. + * 4. Insert with server-derived ``provider="github"``. + * 5. **feat_github_webhook Story 4.2** — when ``webhook_secret_ref`` is + * populated, best-effort enqueue ``register_webhook`` against the + * newly created config_repo id. Enqueue failure (Redis down, pool + * absent, transient blip) does NOT break the 201 — it logs WARN + * and the operator drives recovery via the runbook. + */ + post: operations["create_config_repo_endpoint_api_v1_config_repos_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/config-repos/{config_repo_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Config Repo Endpoint + * @description Detail by id; 404 ``CONFIG_REPO_NOT_FOUND`` if missing. + * + * feat_config_repo_baseline_tracking FR-4 — when + * ``last_merged_proposal_id`` is set, embed the pointed-at proposal as a + * :class:`ProposalSummary` with ``is_currently_live=True``. The embed-side + * derivation uses the pointer context directly (NOT the generic + * ``proposals → clusters → config_repos`` JOIN used elsewhere) so the + * badge renders correctly even when the proposal's cluster was later + * unwired from this config_repo (spec §19 "Cluster-with-config_repo- + * rotated" decision-log entry). + */ + get: operations["get_config_repo_endpoint_api_v1_config_repos__config_repo_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/conversations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Conversations Endpoint + * @description List conversations newest-first with per-row message_count + X-Total-Count header. + * + * ``?since=`` (Story 1.5 — closes api-conventions.md drift) filters by + * ``created_at >= since``. ``?q=`` (Story 1.2) is a Postgres FTS match + * against ``search_vector`` (coalesce(title, '')); 2-200 chars. + */ + get: operations["list_conversations_endpoint_api_v1_conversations_get"]; + put?: never; + /** + * Create Conversation Endpoint + * @description Create a new conversation. Title is optional (FR-1 auto-generates from first message). + */ + post: operations["create_conversation_endpoint_api_v1_conversations_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/conversations/{conversation_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Conversation Endpoint + * @description Return the conversation's full message history. + */ + get: operations["get_conversation_endpoint_api_v1_conversations__conversation_id__get"]; + put?: never; + post?: never; + /** + * Delete Conversation Endpoint + * @description Soft-delete the conversation; subsequent reads return 404. + */ + delete: operations["delete_conversation_endpoint_api_v1_conversations__conversation_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/conversations/{conversation_id}/messages": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Post Message Endpoint + * @description Send a user message and stream the assistant turn as SSE. + * + * Preflight (in order; returns plain JSON envelope, NOT a partial stream): + * A. Conversation exists → else 404 ``CONVERSATION_NOT_FOUND``. + * B. ``Settings.openai_api_key`` populated → else 503 ``OPENAI_NOT_CONFIGURED``. + * C. Daily budget peek under cap → else 503 ``OPENAI_BUDGET_EXCEEDED``. + * + * Successful preflight returns a ``StreamingResponse(text/event-stream)`` + * driven by :func:`agent_chat.send_user_message`. + */ + post: operations["post_message_endpoint_api_v1_conversations__conversation_id__messages_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/judgment-lists": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Judgment Lists Endpoint + * @description List judgment lists, newest-first with cursor pagination. + * + * ``?since=`` filters by ``created_at >= since`` (Story 1.5). ``?q=`` FTS + * match against ``search_vector`` (name + target). ``?sort=`` is a + * :data:`JudgmentListSortKey` value with sort-aware cursor (Story 1.3). + * ``?query_set_id`` / ``?cluster_id`` filter to lists belonging to the + * supplied parent (``bug_judgment_lists_listing_ignores_query_set_filter`` + * — required by the create-study modal's Step-2 dropdown so the user + * can only pick judgment-lists valid for the chosen query-set + cluster; + * without these filters the modal returns all rows and the user can + * pick a mismatched pair, which the ``POST /api/v1/studies`` cross- + * entity integrity check then rejects at create time with a confusing + * 422 ``VALIDATION_ERROR: "judgment_list query_set_id does not match + * study query_set_id"``). + * + * ``?target=`` filters by exact target index/collection name + * (``feat_study_target_judgment_mismatch_guard`` FR-2 — pairs with the + * ``POST /studies`` ``JUDGMENT_TARGET_MISMATCH`` 422 so the create-study + * modal can pre-filter the dropdown to only lists matching the chosen + * study target). Bounded by the ES/OpenSearch index-name ceiling + * (255 bytes). + */ + get: operations["list_judgment_lists_endpoint_api_v1_judgment_lists_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/judgment-lists/import": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Import Judgment List + * @description Create a judgment_lists row with status='complete' + bulk-insert judgments. + * + * Tutorial path; no OpenAI involvement. Every supplied judgment must + * reference a ``query_id`` that exists in ``body.query_set_id`` — + * mismatches → 400 ``QUERY_NOT_IN_SET``. + */ + post: operations["import_judgment_list_api_v1_judgment_lists_import_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/judgment-lists/{judgment_list_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Judgment List Endpoint */ + get: operations["get_judgment_list_endpoint_api_v1_judgment_lists__judgment_list_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/judgment-lists/{judgment_list_id}/calibration": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Calibrate Judgment List + * @description Compute Cohen's + weighted kappa from supplied human samples. + * + * Pairs are built by joining each sample with the existing + * ``source='llm'`` judgment at ``(query_id, doc_id)`` — overridden rows + * (``source='human'``) are excluded (per spec FR-5 + GPT-5.5 cycle 1 F12). + */ + post: operations["calibrate_judgment_list_api_v1_judgment_lists__judgment_list_id__calibration_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/judgment-lists/{judgment_list_id}/judgments": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Judgments Endpoint + * @description List per-list judgments with cursor pagination. + * + * ``?sort=`` is :data:`JudgmentRowSortKey` with sort-aware cursor + * (feat_data_table_primitive Story 1.3). + */ + get: operations["list_judgments_endpoint_api_v1_judgment_lists__judgment_list_id__judgments_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/judgment-lists/{judgment_list_id}/judgments/{judgment_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Override Judgment + * @description Replace an LLM rating with a human override (UPSERT-replace). + */ + patch: operations["override_judgment_api_v1_judgment_lists__judgment_list_id__judgments__judgment_id__patch"]; + trace?: never; + }; + "/api/v1/judgments/generate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Generate Judgments + * @description Create a judgment_lists row + enqueue the worker. + * + * Delegates the full preflight + INSERT + Arq enqueue to + * :func:`backend.app.services.agent_judgments_dispatch.start_judgment_generation` + * so the chat-agent ``generate_judgments_llm`` tool reuses the exact same + * checks (no duplicated preflight). Wire behavior is identical — same error + * codes, same status codes, same response shape. + */ + post: operations["generate_judgments_api_v1_judgments_generate_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/judgments/generate-from-ubi": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Generate Judgments From Ubi + * @description Start a UBI-derived judgment generation job. + * + * Delegates to + * :func:`backend.app.services.agent_judgments_dispatch.start_ubi_judgment_generation` + * which runs the full FR-4 preflight (U-A..U-H) before INSERT + Arq + * enqueue. The Pydantic ``model_validator`` on + * :class:`CreateJudgmentListFromUbiRequest` already enforces the + * hybrid conditional (``current_template_id`` + ``rubric`` required + * iff ``converter == 'hybrid_ubi_llm'``); the dispatcher trusts the + * validated request. + */ + post: operations["generate_judgments_from_ubi_api_v1_judgments_generate_from_ubi_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/proposals": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Proposals Endpoint + * @description List proposals with cursor pagination + filters. + * + * ``?template_id=`` (Story 1.5) filters by ``proposals.template_id`` FK; + * ``?study_id=`` filters by ``proposals.study_id`` FK (used by the + * study-detail page's pending-proposal lookup). Both reject invalid + * UUIDs with 422 via FastAPI's UUID parsing. ``?sort=`` (Story 1.3) is + * a :data:`ProposalSortKey` value with sort-aware cursor. + */ + get: operations["list_proposals_endpoint_api_v1_proposals_get"]; + put?: never; + /** + * Create Manual Proposal + * @description Manually create a proposal (chat-agent hand-crafted tweaks). + * + * ``study_id`` and ``study_trial_id`` are NULL for manual proposals. + * Validates FK targets (cluster + template exist) before insert. + */ + post: operations["create_manual_proposal_api_v1_proposals_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/proposals/{proposal_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Proposal Endpoint */ + get: operations["get_proposal_endpoint_api_v1_proposals__proposal_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/proposals/{proposal_id}/open_pr": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Open Pr Endpoint + * @description Enqueue the ``open_pr`` worker for an operator-approved proposal. + * + * Delegates the full preflight + Arq enqueue to + * :func:`backend.app.services.agent_proposals_dispatch.open_pr` so the + * chat-agent ``open_pr`` tool reuses the same checks. Wire behavior is + * identical — same error codes, status codes, response shape. + */ + post: operations["open_pr_endpoint_api_v1_proposals__proposal_id__open_pr_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/proposals/{proposal_id}/reject": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Reject Proposal Endpoint + * @description AC-5: ``pending → rejected`` transition; 409 INVALID_STATE_TRANSITION otherwise. + */ + post: operations["reject_proposal_endpoint_api_v1_proposals__proposal_id__reject_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/query-sets": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Query Sets + * @description List query sets with cursor pagination + X-Total-Count. + * + * ``?q=`` is FTS match against ``search_vector`` (name). ``?sort=`` is a + * :data:`QuerySetSortKey` value; cursor is sort-aware. + */ + get: operations["list_query_sets_api_v1_query_sets_get"]; + put?: never; + /** + * Create Query Set + * @description Register a query set under a cluster (FR-3). + */ + post: operations["create_query_set_api_v1_query_sets_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/query-sets/{query_set_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Query Set Detail + * @description Return a query set by id (includes ``query_count``). + */ + get: operations["get_query_set_detail_api_v1_query_sets__query_set_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/query-sets/{query_set_id}/queries": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Queries In Set + * @description List per-query rows under a query set, with derived ``judgment_count``. + */ + get: operations["list_queries_in_set_api_v1_query_sets__query_set_id__queries_get"]; + put?: never; + /** + * Bulk Add Queries + * @description Bulk-add queries to a set (FR-3 + AC-8). + * + * Dispatches on Content-Type: + * + * * ``application/json`` → :class:`BulkQueriesJsonRequest` Pydantic-parse. + * * ``text/csv`` → :func:`parse_queries_csv` (AC-8). + * + * Other content types → 415-equivalent surfaced as 400 ``INVALID_CSV`` + * (the documented error code for content-type-mismatch in spec §7.5). + */ + post: operations["bulk_add_queries_api_v1_query_sets__query_set_id__queries_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/query-sets/{query_set_id}/queries/{query_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Delete Query Endpoint + * @description Hard-delete a query. FK-guarded — 409 if any judgment references it. + */ + delete: operations["delete_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__delete"]; + options?: never; + head?: never; + /** + * Update Query Endpoint + * @description Partial-update a query. Whole-object replace on ``query_metadata``. + */ + patch: operations["update_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__patch"]; + trace?: never; + }; + "/api/v1/query-templates": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Query Templates + * @description List query templates with cursor pagination + X-Total-Count header. + * + * ``?q=`` FTS match (name). ``?sort=`` sort-aware cursor (Story 1.3). + * ``?engine_type=`` filters by engine (Story 1.4). + */ + get: operations["list_query_templates_api_v1_query_templates_get"]; + put?: never; + /** + * Create Query Template + * @description Register a query template (FR-2 + AC-7). + * + * AC-7: a body containing ``{{ os.system('rm -rf /') }}`` surfaces as + * 400 ``INVALID_TEMPLATE_SYNTAX`` (the AST walk catches the ``Call`` + * node before reaching the meta-vars cross-check that would otherwise + * classify ``os`` as ``UndeclaredParamUsed``). + */ + post: operations["create_query_template_api_v1_query_templates_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/query-templates/{template_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Query Template Detail + * @description Return a query template by id. + */ + get: operations["get_query_template_detail_api_v1_query_templates__template_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/studies": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Studies + * @description List studies with cursor pagination + X-Total-Count. + * + * ``?status=`` is typed as :data:`StudyStatusWire` so FastAPI returns + * 422 ``VALIDATION_ERROR`` for unsupported values. ``?q=`` is a Postgres + * FTS match against ``search_vector`` (name + target). ``?sort=`` is a + * :data:`StudySortKey` value (``:``); the cursor is + * sort-aware (feat_data_table_primitive Stories 1.2 + 1.3). + * + * ``?target=`` (feat_index_document_browser FR-5) scopes the list to + * studies targeting a single index/collection. Composes with all other + * filters via AND. + */ + get: operations["list_studies_api_v1_studies_get"]; + put?: never; + /** + * Create Study + * @description Create a study (FR-1 + AC-1) and enqueue the orchestrator job. + */ + post: operations["create_study_api_v1_studies_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/studies/{study_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Study Detail + * @description Return a study by id (includes ``trials_summary``). + */ + get: operations["get_study_detail_api_v1_studies__study_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/studies/{study_id}/cancel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Cancel Study + * @description Cancel a study (Story 2.3, FR-8 + AC-8/AC-9). + * + * Optionally cascades to in-flight chain children. + * + * ``?cascade=true`` (default): routes through + * :func:`services.study_state.cancel_study_with_chain_cascade` — + * cancels the parent (if in-flight) AND recursively cancels in-flight + * descendants. Tolerates terminal parents (recurses through completed + * intermediates to reach an in-flight grandchild). + * + * ``?cascade=false``: routes through the original + * :func:`services.study_state.cancel_study` — single-study cancel, + * preserves the existing 409 error contract on terminal parents + * (AC-9 wire contract). + */ + post: operations["cancel_study_api_v1_studies__study_id__cancel_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/studies/{study_id}/chain": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Study Chain + * @description Return the rolled-up chain summary for the study and its lineage (FR-3). + * + * Walks to the chain anchor, aggregates the completed-link subset into a + * best link + cumulative lift + derived stop reason, and emits per-link + * deltas. The anchor's ``delta_from_prev`` is always ``None`` (spec §8.3). + * Returns ``404 STUDY_NOT_FOUND`` when the study does not exist. + */ + get: operations["get_study_chain_api_v1_studies__study_id__chain_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/studies/{study_id}/children": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Study Children + * @description List direct child studies of a parent (FR-10 + D-13). + * + * Returns ``{"data": [], "next_cursor": null}`` for a study with no + * children — empty data array, NOT 404. 404 only fires when the parent + * study itself is missing. + * + * Per D-13 (direct-children-only): does NOT return transitive + * descendants. The chain panel renders parent ↑ + direct children ↓; + * operators walk lineage one hop per page navigation. + */ + get: operations["list_study_children_api_v1_studies__study_id__children_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/studies/{study_id}/digest": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Study Digest + * @description Fetch the digest for a completed study. + * + * Returns 404 ``DIGEST_NOT_READY`` (``retryable=true``) when: + * - the study is not in ``status='completed'``, OR + * - the study is completed but the worker hasn't written the digest yet + * (worker lag, or a worker-side terminal failure like + * ``OPENAI_NOT_CONFIGURED`` deferred the run). + */ + get: operations["get_study_digest_api_v1_studies__study_id__digest_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/studies/{study_id}/trials": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Study Trials + * @description List trials in a study (FR-6). + * + * Sort variants per spec §7.4: ``primary_metric_desc`` (default), + * ``primary_metric_asc``, ``ended_at_desc``, ``ended_at_asc``, + * ``optuna_trial_number_asc``. + */ + get: operations["list_study_trials_api_v1_studies__study_id__trials_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/healthz": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Healthz + * @description Probe each subsystem in parallel and return the documented JSON shape. + * + * Args: + * settings: Application settings (DB URL, ES/OS URLs, OpenAI base URL, etc.) + * redis_client: Redis client for ping probe + capability-cache read + * es_client: shared httpx client for ES + OpenSearch HTTP probes + * db: Async DB session for the registered-clusters aggregate (Story 3.5) + * + * Returns: + * JSONResponse with the HealthResponse body and HTTP 200 (healthy) or 503 (degraded). + */ + get: operations["healthz_healthz_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/webhooks/github": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Github Webhook + * @description Receive a single GitHub webhook delivery. + * + * Returns ``{"status": "ok", "action": }`` where + * ``wire_action`` is one of the four values in + * :data:`WEBHOOK_ACTION_VALUES`. + * + * Raises: + * HTTPException(403, INVALID_SIGNATURE): bad signature or unknown + * repository. Both share one error code so the receiver does + * not reveal repo enumeration. + */ + post: operations["github_webhook_webhooks_github_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - get?: never; - put?: never; - /** - * Seed an auto-followup chain of N+1 linked studies - * @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. - */ - post: operations['seed_auto_followup_chain_endpoint_api_v1__test_auto_followup_seed_chain_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/_test/demo/reseed': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Enqueue a demo-state reseed (dev-only, async) - * @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. - * - * Per ``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. - */ - post: operations['reseed_demo_api_v1__test_demo_reseed_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/_test/demo/reseed/status': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Poll the current demo-reseed progress (dev-only) - * @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. - */ - get: operations['reseed_demo_status_api_v1__test_demo_reseed_status_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/_test/digests/{digest_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** - * Hard-delete a digest (test-only) - * @description FR-2: Hard-delete the digest row. No FK children — no preflight needed. - */ - delete: operations['delete_test_digest_api_v1__test_digests__digest_id__delete']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/_test/judgment-lists/{judgment_list_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** - * Hard-delete a judgment_list (test-only) - * @description FR-4 — hard-delete the judgment_list row. - * - * Judgments cascade-delete via existing FK. Preflight-checks ``studies`` - * (non-cascade); 409 if any study references the judgment_list. - */ - delete: operations['delete_test_judgment_list_api_v1__test_judgment_lists__judgment_list_id__delete']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/_test/proposals/{proposal_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** - * Hard-delete a proposal (test-only) - * @description FR-1: Hard-delete the proposal row. No FK children — no preflight needed. - */ - delete: operations['delete_test_proposal_api_v1__test_proposals__proposal_id__delete']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/_test/query-sets/{query_set_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** - * Hard-delete a query_set (test-only) - * @description FR-5 — hard-delete the query_set row. - * - * Queries cascade-delete via existing FK. Preflight-checks ``studies`` - * + ``judgment_lists`` (both non-cascade); 409 with resource-specific - * code if either references. - */ - delete: operations['delete_test_query_set_api_v1__test_query_sets__query_set_id__delete']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/_test/query-templates/{template_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** - * Hard-delete a query_template (test-only) - * @description FR-6 — hard-delete the query_template row. - * - * No FK children cascade with template. Preflight-checks ``studies``, - * ``proposals``, and ``judgment_lists.current_template_id`` in - * **fixed priority order: STUDY > PROPOSAL > JUDGMENT_LIST** (per - * spec §FR-6) — first match wins. - */ - delete: operations['delete_test_query_template_api_v1__test_query_templates__template_id__delete']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/_test/studies/seed-completed': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Seed a completed study + digest + (optional) pending proposal - * @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. - */ - post: operations['seed_completed_study_api_v1__test_studies_seed_completed_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/_test/studies/{study_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** - * Hard-delete a study (test-only) - * @description FR-3 — hard-delete the study row. - * - * Trials cascade-delete via existing FK. Preflight-checks ``proposals`` - * + ``digests`` (both non-cascade); 409 if any dependent rows reference - * the study. - */ - delete: operations['delete_test_study_api_v1__test_studies__study_id__delete']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/clusters': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List Clusters - * @description List clusters with cursor pagination + ``X-Total-Count`` header. - * - * ``?q=`` is a Postgres FTS match against the cluster's ``search_vector`` - * (name + base_url); 2–200 chars. Filter-only — ordering unchanged per - * spec FR-1. ``?sort=`` is one of the values in - * :data:`~backend.app.api.v1.schemas.ClusterSortKey`; the cursor is - * sort-aware so the keyset predicate matches the active ORDER BY - * (feat_data_table_primitive Stories 1.2 + 1.3). - */ - get: operations['list_clusters_api_v1_clusters_get']; - put?: never; - /** - * Create Cluster - * @description Register a cluster (FR-5 / AC-1). - */ - post: operations['create_cluster_api_v1_clusters_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/clusters/test-connection': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Test Connection - * @description Probe a cluster config WITHOUT persisting (infra_adapter_solr Story A9). - * - * Powers the registration modal's "Test connection" button. Always 200 — - * transport failures surface as ``reachable=false`` with ``error`` set. - * Invalid engine×auth pairings 400 BEFORE the network call. - */ - post: operations['test_connection_api_v1_clusters_test_connection_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/clusters/{cluster_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Cluster Detail - * @description Return cluster row + cached/fresh health probe. - */ - get: operations['get_cluster_detail_api_v1_clusters__cluster_id__get']; - put?: never; - post?: never; - /** - * Delete Cluster - * @description Soft-delete a cluster (AC-8). Returns 204 with no body. - */ - delete: operations['delete_cluster_api_v1_clusters__cluster_id__delete']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/clusters/{cluster_id}/reprobe': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Reprobe Cluster - * @description Re-run cluster capability probe (Story A9 / spec FR-2 + AC-14). - * - * Concurrent calls serialize on ``SELECT … FOR UPDATE``. On probe failure - * the row's engine_config is NOT updated (the transaction rolls back). - */ - post: operations['reprobe_cluster_api_v1_clusters__cluster_id__reprobe_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/clusters/{cluster_id}/run_query': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Run Query - * @description Execute one query DSL fragment against the cluster (FR-6 / AC-3). - */ - post: operations['run_query_api_v1_clusters__cluster_id__run_query_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/clusters/{cluster_id}/schema': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Cluster Schema - * @description Return the field schema for ``target`` (FR-4 / AC-2). - */ - get: operations['get_cluster_schema_api_v1_clusters__cluster_id__schema_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/clusters/{cluster_id}/targets': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List Cluster Targets - * @description List targets (indices/collections) on the cluster (FR-1 / AC-1). - * - * Thin passthrough to ``ElasticAdapter.list_targets()`` (which filters out - * system indices whose names start with ``.``). Mirrors the ``get_cluster_schema`` - * pattern: ``get_cluster`` → ``acquire_adapter`` async context → adapter call - * → translate exceptions via the ``_err()`` helper to the spec §7.5 envelope. - * - * Error mapping: - * * cluster missing or soft-deleted → 404 ``CLUSTER_NOT_FOUND`` (retryable=false) - * * adapter raises ``TargetsForbiddenError`` (ACL 401/403) → 403 - * ``TARGETS_FORBIDDEN`` (retryable=false) — frontend auto-engages manual mode - * * adapter raises ``ClusterUnreachableError`` (5xx / connection failure) → 503 - * ``CLUSTER_UNREACHABLE`` (retryable=true) - */ - get: operations['list_cluster_targets_api_v1_clusters__cluster_id__targets_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/clusters/{cluster_id}/targets/{target}/documents': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List Target Documents - * @description Paginated _id + truncated _source preview for a target (FR-3). - * - * The endpoint asks the adapter for ``limit + 1`` rows so it can detect - * end-of-data exactly (no extra round-trip). Only the first ``limit`` rows - * are returned; ``next_cursor`` encodes the ES ``hits[i].sort`` of the - * last visible row when ``has_more`` is True. ``X-Total-Count`` header - * carries the engine's ``hits.total.value``. - */ - get: operations['list_target_documents_api_v1_clusters__cluster_id__targets__target__documents_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/clusters/{cluster_id}/targets/{target}/documents/{doc_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Target Document - * @description Fetch one document by ``_id`` (FR-4). - * - * FastAPI's ``{doc_id:path}`` converter round-trips slashes verbatim, so - * operator IDs containing ``/`` are supported (D-17 / AC-16). Returns the - * adapter ``Document`` shape directly; on ``found: false`` returns 404 - * ``DOCUMENT_NOT_FOUND`` (distinct from ``TARGET_NOT_FOUND``). - */ - get: operations['get_target_document_api_v1_clusters__cluster_id__targets__target__documents__doc_id__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/clusters/{cluster_id}/ubi-readiness': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Cluster Ubi Readiness - * @description Classify ``(cluster, query_set, target)`` on the UBI rung ladder. - * - * feat_ubi_judgments FR-7. - * - * Required query params: ``query_set_id`` + ``target`` (Spec FR-7 + - * cycle-3 D-10c: the endpoint MUST 422 without them — the classifier - * can't compute a per-target rung without an application filter). - * - * Error envelopes (all per spec §7.5): - * * ``404 CLUSTER_NOT_FOUND`` — cluster row missing or soft-deleted. - * * ``404 QUERY_SET_NOT_FOUND`` — query set row missing. - * * ``422 VALIDATION_ERROR`` — missing required query params (FastAPI's - * built-in handler, surfaces via ``api/errors.py``). - * * ``503 CLUSTER_UNREACHABLE`` — adapter cannot reach the cluster. - * - * The result is cached for 60 s in Redis per - * ``(cluster_id, query_set_id, target)`` so back-to-back dialog-open - * and dialog-submit calls don't re-probe. - */ - get: operations['get_cluster_ubi_readiness_api_v1_clusters__cluster_id__ubi_readiness_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/config-repos': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List Config Repos Endpoint - * @description Cursor-paginated config-repo list, newest first. - */ - get: operations['list_config_repos_endpoint_api_v1_config_repos_get']; - put?: never; - /** - * Create Config Repo Endpoint - * @description Register a new config repo. ``provider`` is server-derived from ``repo_url``. - * - * Preflight order matches spec FR-3: - * - * 1. ``validate_repo_url(repo_url)`` → 400 ``UNSUPPORTED_PROVIDER`` for - * non-GitHub URLs (AC-8). GitLab + Bitbucket arrive at MVP3. - * 2. ``./secrets/{auth_ref}`` must exist → else 400 ``AUTH_REF_NOT_FOUND`` - * (AC-9). The contents check defers to the worker — operators may - * populate the file between registration and first PR-open. - * 3. ``name`` uniqueness check → 409 ``CONFIG_REPO_NAME_TAKEN`` on collision. - * 4. Insert with server-derived ``provider="github"``. - * 5. **feat_github_webhook Story 4.2** — when ``webhook_secret_ref`` is - * populated, best-effort enqueue ``register_webhook`` against the - * newly created config_repo id. Enqueue failure (Redis down, pool - * absent, transient blip) does NOT break the 201 — it logs WARN - * and the operator drives recovery via the runbook. - */ - post: operations['create_config_repo_endpoint_api_v1_config_repos_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/config-repos/{config_repo_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Config Repo Endpoint - * @description Detail by id; 404 ``CONFIG_REPO_NOT_FOUND`` if missing. - * - * feat_config_repo_baseline_tracking FR-4 — when - * ``last_merged_proposal_id`` is set, embed the pointed-at proposal as a - * :class:`ProposalSummary` with ``is_currently_live=True``. The embed-side - * derivation uses the pointer context directly (NOT the generic - * ``proposals → clusters → config_repos`` JOIN used elsewhere) so the - * badge renders correctly even when the proposal's cluster was later - * unwired from this config_repo (spec §19 "Cluster-with-config_repo- - * rotated" decision-log entry). - */ - get: operations['get_config_repo_endpoint_api_v1_config_repos__config_repo_id__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/conversations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List Conversations Endpoint - * @description List conversations newest-first with per-row message_count + X-Total-Count header. - * - * ``?since=`` (Story 1.5 — closes api-conventions.md drift) filters by - * ``created_at >= since``. ``?q=`` (Story 1.2) is a Postgres FTS match - * against ``search_vector`` (coalesce(title, '')); 2-200 chars. - */ - get: operations['list_conversations_endpoint_api_v1_conversations_get']; - put?: never; - /** - * Create Conversation Endpoint - * @description Create a new conversation. Title is optional (FR-1 auto-generates from first message). - */ - post: operations['create_conversation_endpoint_api_v1_conversations_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/conversations/{conversation_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Conversation Endpoint - * @description Return the conversation's full message history. - */ - get: operations['get_conversation_endpoint_api_v1_conversations__conversation_id__get']; - put?: never; - post?: never; - /** - * Delete Conversation Endpoint - * @description Soft-delete the conversation; subsequent reads return 404. - */ - delete: operations['delete_conversation_endpoint_api_v1_conversations__conversation_id__delete']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/conversations/{conversation_id}/messages': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Post Message Endpoint - * @description Send a user message and stream the assistant turn as SSE. - * - * Preflight (in order; returns plain JSON envelope, NOT a partial stream): - * A. Conversation exists → else 404 ``CONVERSATION_NOT_FOUND``. - * B. ``Settings.openai_api_key`` populated → else 503 ``OPENAI_NOT_CONFIGURED``. - * C. Daily budget peek under cap → else 503 ``OPENAI_BUDGET_EXCEEDED``. - * - * Successful preflight returns a ``StreamingResponse(text/event-stream)`` - * driven by :func:`agent_chat.send_user_message`. - */ - post: operations['post_message_endpoint_api_v1_conversations__conversation_id__messages_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/judgment-lists': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List Judgment Lists Endpoint - * @description List judgment lists, newest-first with cursor pagination. - * - * ``?since=`` filters by ``created_at >= since`` (Story 1.5). ``?q=`` FTS - * match against ``search_vector`` (name + target). ``?sort=`` is a - * :data:`JudgmentListSortKey` value with sort-aware cursor (Story 1.3). - * ``?query_set_id`` / ``?cluster_id`` filter to lists belonging to the - * supplied parent (``bug_judgment_lists_listing_ignores_query_set_filter`` - * — required by the create-study modal's Step-2 dropdown so the user - * can only pick judgment-lists valid for the chosen query-set + cluster; - * without these filters the modal returns all rows and the user can - * pick a mismatched pair, which the ``POST /api/v1/studies`` cross- - * entity integrity check then rejects at create time with a confusing - * 422 ``VALIDATION_ERROR: "judgment_list query_set_id does not match - * study query_set_id"``). - * - * ``?target=`` filters by exact target index/collection name - * (``feat_study_target_judgment_mismatch_guard`` FR-2 — pairs with the - * ``POST /studies`` ``JUDGMENT_TARGET_MISMATCH`` 422 so the create-study - * modal can pre-filter the dropdown to only lists matching the chosen - * study target). Bounded by the ES/OpenSearch index-name ceiling - * (255 bytes). - */ - get: operations['list_judgment_lists_endpoint_api_v1_judgment_lists_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/judgment-lists/import': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Import Judgment List - * @description Create a judgment_lists row with status='complete' + bulk-insert judgments. - * - * Tutorial path; no OpenAI involvement. Every supplied judgment must - * reference a ``query_id`` that exists in ``body.query_set_id`` — - * mismatches → 400 ``QUERY_NOT_IN_SET``. - */ - post: operations['import_judgment_list_api_v1_judgment_lists_import_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/judgment-lists/{judgment_list_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get Judgment List Endpoint */ - get: operations['get_judgment_list_endpoint_api_v1_judgment_lists__judgment_list_id__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/judgment-lists/{judgment_list_id}/calibration': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Calibrate Judgment List - * @description Compute Cohen's + weighted kappa from supplied human samples. - * - * Pairs are built by joining each sample with the existing - * ``source='llm'`` judgment at ``(query_id, doc_id)`` — overridden rows - * (``source='human'``) are excluded (per spec FR-5 + GPT-5.5 cycle 1 F12). - */ - post: operations['calibrate_judgment_list_api_v1_judgment_lists__judgment_list_id__calibration_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/judgment-lists/{judgment_list_id}/judgments': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List Judgments Endpoint - * @description List per-list judgments with cursor pagination. - * - * ``?sort=`` is :data:`JudgmentRowSortKey` with sort-aware cursor - * (feat_data_table_primitive Story 1.3). - */ - get: operations['list_judgments_endpoint_api_v1_judgment_lists__judgment_list_id__judgments_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/judgment-lists/{judgment_list_id}/judgments/{judgment_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Override Judgment - * @description Replace an LLM rating with a human override (UPSERT-replace). - */ - patch: operations['override_judgment_api_v1_judgment_lists__judgment_list_id__judgments__judgment_id__patch']; - trace?: never; - }; - '/api/v1/judgments/generate': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Generate Judgments - * @description Create a judgment_lists row + enqueue the worker. - * - * Delegates the full preflight + INSERT + Arq enqueue to - * :func:`backend.app.services.agent_judgments_dispatch.start_judgment_generation` - * so the chat-agent ``generate_judgments_llm`` tool reuses the exact same - * checks (no duplicated preflight). Wire behavior is identical — same error - * codes, same status codes, same response shape. - */ - post: operations['generate_judgments_api_v1_judgments_generate_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/judgments/generate-from-ubi': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Generate Judgments From Ubi - * @description Start a UBI-derived judgment generation job. - * - * Delegates to - * :func:`backend.app.services.agent_judgments_dispatch.start_ubi_judgment_generation` - * which runs the full FR-4 preflight (U-A..U-H) before INSERT + Arq - * enqueue. The Pydantic ``model_validator`` on - * :class:`CreateJudgmentListFromUbiRequest` already enforces the - * hybrid conditional (``current_template_id`` + ``rubric`` required - * iff ``converter == 'hybrid_ubi_llm'``); the dispatcher trusts the - * validated request. - */ - post: operations['generate_judgments_from_ubi_api_v1_judgments_generate_from_ubi_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/proposals': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List Proposals Endpoint - * @description List proposals with cursor pagination + filters. - * - * ``?template_id=`` (Story 1.5) filters by ``proposals.template_id`` FK; - * ``?study_id=`` filters by ``proposals.study_id`` FK (used by the - * study-detail page's pending-proposal lookup). Both reject invalid - * UUIDs with 422 via FastAPI's UUID parsing. ``?sort=`` (Story 1.3) is - * a :data:`ProposalSortKey` value with sort-aware cursor. - */ - get: operations['list_proposals_endpoint_api_v1_proposals_get']; - put?: never; - /** - * Create Manual Proposal - * @description Manually create a proposal (chat-agent hand-crafted tweaks). - * - * ``study_id`` and ``study_trial_id`` are NULL for manual proposals. - * Validates FK targets (cluster + template exist) before insert. - */ - post: operations['create_manual_proposal_api_v1_proposals_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/proposals/{proposal_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get Proposal Endpoint */ - get: operations['get_proposal_endpoint_api_v1_proposals__proposal_id__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/proposals/{proposal_id}/open_pr': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Open Pr Endpoint - * @description Enqueue the ``open_pr`` worker for an operator-approved proposal. - * - * Delegates the full preflight + Arq enqueue to - * :func:`backend.app.services.agent_proposals_dispatch.open_pr` so the - * chat-agent ``open_pr`` tool reuses the same checks. Wire behavior is - * identical — same error codes, status codes, response shape. - */ - post: operations['open_pr_endpoint_api_v1_proposals__proposal_id__open_pr_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/proposals/{proposal_id}/reject': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Reject Proposal Endpoint - * @description AC-5: ``pending → rejected`` transition; 409 INVALID_STATE_TRANSITION otherwise. - */ - post: operations['reject_proposal_endpoint_api_v1_proposals__proposal_id__reject_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/query-sets': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List Query Sets - * @description List query sets with cursor pagination + X-Total-Count. - * - * ``?q=`` is FTS match against ``search_vector`` (name). ``?sort=`` is a - * :data:`QuerySetSortKey` value; cursor is sort-aware. - */ - get: operations['list_query_sets_api_v1_query_sets_get']; - put?: never; - /** - * Create Query Set - * @description Register a query set under a cluster (FR-3). - */ - post: operations['create_query_set_api_v1_query_sets_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/query-sets/{query_set_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Query Set Detail - * @description Return a query set by id (includes ``query_count``). - */ - get: operations['get_query_set_detail_api_v1_query_sets__query_set_id__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/query-sets/{query_set_id}/queries': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List Queries In Set - * @description List per-query rows under a query set, with derived ``judgment_count``. - */ - get: operations['list_queries_in_set_api_v1_query_sets__query_set_id__queries_get']; - put?: never; - /** - * Bulk Add Queries - * @description Bulk-add queries to a set (FR-3 + AC-8). - * - * Dispatches on Content-Type: - * - * * ``application/json`` → :class:`BulkQueriesJsonRequest` Pydantic-parse. - * * ``text/csv`` → :func:`parse_queries_csv` (AC-8). - * - * Other content types → 415-equivalent surfaced as 400 ``INVALID_CSV`` - * (the documented error code for content-type-mismatch in spec §7.5). - */ - post: operations['bulk_add_queries_api_v1_query_sets__query_set_id__queries_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/query-sets/{query_set_id}/queries/{query_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** - * Delete Query Endpoint - * @description Hard-delete a query. FK-guarded — 409 if any judgment references it. - */ - delete: operations['delete_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__delete']; - options?: never; - head?: never; - /** - * Update Query Endpoint - * @description Partial-update a query. Whole-object replace on ``query_metadata``. - */ - patch: operations['update_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__patch']; - trace?: never; - }; - '/api/v1/query-templates': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List Query Templates - * @description List query templates with cursor pagination + X-Total-Count header. - * - * ``?q=`` FTS match (name). ``?sort=`` sort-aware cursor (Story 1.3). - * ``?engine_type=`` filters by engine (Story 1.4). - */ - get: operations['list_query_templates_api_v1_query_templates_get']; - put?: never; - /** - * Create Query Template - * @description Register a query template (FR-2 + AC-7). - * - * AC-7: a body containing ``{{ os.system('rm -rf /') }}`` surfaces as - * 400 ``INVALID_TEMPLATE_SYNTAX`` (the AST walk catches the ``Call`` - * node before reaching the meta-vars cross-check that would otherwise - * classify ``os`` as ``UndeclaredParamUsed``). - */ - post: operations['create_query_template_api_v1_query_templates_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/query-templates/{template_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Query Template Detail - * @description Return a query template by id. - */ - get: operations['get_query_template_detail_api_v1_query_templates__template_id__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/studies': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List Studies - * @description List studies with cursor pagination + X-Total-Count. - * - * ``?status=`` is typed as :data:`StudyStatusWire` so FastAPI returns - * 422 ``VALIDATION_ERROR`` for unsupported values. ``?q=`` is a Postgres - * FTS match against ``search_vector`` (name + target). ``?sort=`` is a - * :data:`StudySortKey` value (``:``); the cursor is - * sort-aware (feat_data_table_primitive Stories 1.2 + 1.3). - * - * ``?target=`` (feat_index_document_browser FR-5) scopes the list to - * studies targeting a single index/collection. Composes with all other - * filters via AND. - */ - get: operations['list_studies_api_v1_studies_get']; - put?: never; - /** - * Create Study - * @description Create a study (FR-1 + AC-1) and enqueue the orchestrator job. - */ - post: operations['create_study_api_v1_studies_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/studies/{study_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Study Detail - * @description Return a study by id (includes ``trials_summary``). - */ - get: operations['get_study_detail_api_v1_studies__study_id__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/studies/{study_id}/cancel': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Cancel Study - * @description Cancel a study (Story 2.3, FR-8 + AC-8/AC-9). - * - * Optionally cascades to in-flight chain children. - * - * ``?cascade=true`` (default): routes through - * :func:`services.study_state.cancel_study_with_chain_cascade` — - * cancels the parent (if in-flight) AND recursively cancels in-flight - * descendants. Tolerates terminal parents (recurses through completed - * intermediates to reach an in-flight grandchild). - * - * ``?cascade=false``: routes through the original - * :func:`services.study_state.cancel_study` — single-study cancel, - * preserves the existing 409 error contract on terminal parents - * (AC-9 wire contract). - */ - post: operations['cancel_study_api_v1_studies__study_id__cancel_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/studies/{study_id}/chain': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Study Chain - * @description Return the rolled-up chain summary for the study and its lineage (FR-3). - * - * Walks to the chain anchor, aggregates the completed-link subset into a - * best link + cumulative lift + derived stop reason, and emits per-link - * deltas. The anchor's ``delta_from_prev`` is always ``None`` (spec §8.3). - * Returns ``404 STUDY_NOT_FOUND`` when the study does not exist. - */ - get: operations['get_study_chain_api_v1_studies__study_id__chain_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/studies/{study_id}/children': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List Study Children - * @description List direct child studies of a parent (FR-10 + D-13). - * - * Returns ``{"data": [], "next_cursor": null}`` for a study with no - * children — empty data array, NOT 404. 404 only fires when the parent - * study itself is missing. - * - * Per D-13 (direct-children-only): does NOT return transitive - * descendants. The chain panel renders parent ↑ + direct children ↓; - * operators walk lineage one hop per page navigation. - */ - get: operations['list_study_children_api_v1_studies__study_id__children_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/studies/{study_id}/digest': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Study Digest - * @description Fetch the digest for a completed study. - * - * Returns 404 ``DIGEST_NOT_READY`` (``retryable=true``) when: - * - the study is not in ``status='completed'``, OR - * - the study is completed but the worker hasn't written the digest yet - * (worker lag, or a worker-side terminal failure like - * ``OPENAI_NOT_CONFIGURED`` deferred the run). - */ - get: operations['get_study_digest_api_v1_studies__study_id__digest_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/studies/{study_id}/trials': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List Study Trials - * @description List trials in a study (FR-6). - * - * Sort variants per spec §7.4: ``primary_metric_desc`` (default), - * ``primary_metric_asc``, ``ended_at_desc``, ``ended_at_asc``, - * ``optuna_trial_number_asc``. - */ - get: operations['list_study_trials_api_v1_studies__study_id__trials_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/healthz': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Healthz - * @description Probe each subsystem in parallel and return the documented JSON shape. - * - * Args: - * settings: Application settings (DB URL, ES/OS URLs, OpenAI base URL, etc.) - * redis_client: Redis client for ping probe + capability-cache read - * es_client: shared httpx client for ES + OpenSearch HTTP probes - * db: Async DB session for the registered-clusters aggregate (Story 3.5) - * - * Returns: - * JSONResponse with the HealthResponse body and HTTP 200 (healthy) or 503 (degraded). - */ - get: operations['healthz_healthz_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/webhooks/github': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Github Webhook - * @description Receive a single GitHub webhook delivery. - * - * Returns ``{"status": "ok", "action": }`` where - * ``wire_action`` is one of the four values in - * :data:`WEBHOOK_ACTION_VALUES`. - * - * Raises: - * HTTPException(403, INVALID_SIGNATURE): bad signature or unknown - * repository. Both share one error code so the receiver does - * not reveal repo enumeration. - */ - post: operations['github_webhook_webhooks_github_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; } export type webhooks = Record; export interface components { - schemas: { - /** - * BulkQueriesResponse - * @description ``POST /api/v1/query-sets/{id}/queries`` response. - */ - BulkQueriesResponse: { - /** Added */ - added: number; - }; - /** - * CIShape - * @description Bootstrap percentile CI on the winner's per-query metric values. - */ - CIShape: { - /** High */ - high: number; - /** Low */ - low: number; - /** - * Method - * @constant - */ - method: 'bootstrap_n1000'; - /** N Samples */ - n_samples: number; - }; - /** - * CalibrationResponse - * @description Calibration endpoint response. - * - * Mirrors :class:`backend.app.eval.calibration.CalibrationResult` — - * persisted as ``judgment_lists.calibration`` JSONB. - */ - CalibrationResponse: { - /** Cohens Kappa */ - cohens_kappa: number | null; - /** N Samples */ - n_samples: number; - /** Per Class */ - per_class: { - [key: string]: number; - }; - /** Warning */ - warning: string | null; - /** Weighted Kappa */ - weighted_kappa: number | null; - }; - /** - * CalibrationSample - * @description One row in :class:`CalibrationSamplesRequest`. - */ - CalibrationSample: { - /** Doc Id */ - doc_id: string; - /** Query Id */ - query_id: string; - /** - * Rating - * @enum {integer} - */ - rating: 0 | 1 | 2 | 3; - }; - /** - * CalibrationSamplesRequest - * @description Body for ``POST /api/v1/judgment-lists/{id}/calibration`` (Story 3.5). - */ - CalibrationSamplesRequest: { - /** Human Samples */ - human_samples: components['schemas']['CalibrationSample'][]; - }; - /** - * CategoricalParam - * @description Discrete choice parameter. - * - * Optuna ``suggest_categorical`` handles strings, ints, floats, and bools - * as choices. - */ - CategoricalParam: { - /** Choices */ - choices: (string | number | boolean)[]; - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: 'categorical'; - }; - /** - * ClusterAggregateHealth - * @description Aggregate counts for the ``elasticsearch_clusters`` /healthz field (Story 3.5). - * - * Per spec §2: probes only the *registered* user clusters (from the DB), - * NOT the local Compose ES/OpenSearch — those have their own subsystem - * fields. ``status`` is a count derived from the cached ``cluster:health:*`` - * entries; missing-cache or red/unreachable clusters are counted as - * ``unreachable``. - */ - ClusterAggregateHealth: { - /** Healthy */ - healthy: number; - /** Registered */ - registered: number; - /** Unreachable */ - unreachable: number; - }; - /** - * ClusterDetail - * @description ``GET /api/v1/clusters/{id}`` response. - */ - ClusterDetail: { - /** - * Auth Kind - * @enum {string} - */ - auth_kind: - | 'es_apikey' - | 'es_basic' - | 'opensearch_basic' - | 'opensearch_sigv4' - | 'solr_basic' - | 'solr_apikey'; - /** Base Url */ - base_url: string; - /** - * Created At - * Format: date-time - */ - created_at: string; - /** Engine Config */ - engine_config?: { - [key: string]: unknown; - } | null; - /** - * Engine Type - * @enum {string} - */ - engine_type: 'elasticsearch' | 'opensearch' | 'solr'; - /** - * Environment - * @enum {string} - */ - environment: 'prod' | 'staging' | 'dev'; - health_check: components['schemas']['HealthCheckResult']; - /** Id */ - id: string; - /** Name */ - name: string; - /** Notes */ - notes?: string | null; - /** Target Filter */ - target_filter?: string | null; - }; - /** - * ClusterListResponse - * @description Paginated list response. - */ - ClusterListResponse: { - /** Data */ - data: components['schemas']['ClusterSummary'][]; - /** Has More */ - has_more: boolean; - /** Next Cursor */ - next_cursor: string | null; - }; - /** - * ClusterSummary - * @description List-view; drops engine_config + notes for brevity. - */ - ClusterSummary: { - /** - * Auth Kind - * @enum {string} - */ - auth_kind: - | 'es_apikey' - | 'es_basic' - | 'opensearch_basic' - | 'opensearch_sigv4' - | 'solr_basic' - | 'solr_apikey'; - /** Base Url */ - base_url: string; - /** - * Created At - * Format: date-time - */ - created_at: string; - /** - * Engine Type - * @enum {string} - */ - engine_type: 'elasticsearch' | 'opensearch' | 'solr'; - /** - * Environment - * @enum {string} - */ - environment: 'prod' | 'staging' | 'dev'; - health_check: components['schemas']['HealthCheckResult']; - /** Id */ - id: string; - /** Name */ - name: string; - /** Target Filter */ - target_filter?: string | null; - }; - /** - * ConfidenceShape - * @description The top-level shape exposed via ``StudyDetail.confidence``. - * - * Every sub-field is independently nullable per FR-7 — degraded paths - * suppress only the sub-fields they affect, never the whole shape (the - * orchestrator returns whole-object ``None`` only when the winner trial - * row itself is missing). - */ - ConfidenceShape: { - ci_95: components['schemas']['CIShape'] | null; - convergence: components['schemas']['ConvergenceShape'] | null; - headline: components['schemas']['HeadlineShape']; - late_trial_stddev: components['schemas']['LateTrialStddevShape'] | null; - per_query_outcomes: components['schemas']['PerQueryOutcomesShape'] | null; - runner_up_gap: components['schemas']['RunnerUpGapShape'] | null; - }; - /** - * ConfigRepoDetail - * @description ``GET /api/v1/config-repos/{id}`` response + ``POST`` 201 body. - */ - ConfigRepoDetail: { - /** Auth Ref */ - auth_ref: string; - /** - * Created At - * Format: date-time - */ - created_at: string; - /** Default Branch */ - default_branch: string; - /** Id */ - id: string; - last_merged_proposal?: components['schemas']['ProposalSummary'] | null; - /** Name */ - name: string; - /** Pr Base Branch */ - pr_base_branch: string; - /** - * Provider - * @constant - */ - provider: 'github'; - /** Repo Url */ - repo_url: string; - /** Webhook Registration Error */ - webhook_registration_error: string | null; - /** Webhook Secret Ref */ - webhook_secret_ref: string | null; - }; - /** - * ConfigReposListResponse - * @description ``GET /api/v1/config-repos`` response. - */ - ConfigReposListResponse: { - /** Data */ - data: components['schemas']['ConfigRepoDetail'][]; - /** Has More */ - has_more: boolean; - /** Next Cursor */ - next_cursor: string | null; - }; - /** - * ConnectionTestRequest - * @description Body for ``POST /api/v1/clusters/test-connection`` (infra_adapter_solr Story A9). - * - * Same shape as ``CreateClusterRequest`` minus the persisted-only fields - * (``name``, ``environment``, ``notes``, ``target_filter``). ``engine_type`` - * + ``auth_kind`` are typed as ``str`` (not Literal) so a bad value yields - * the project-standard 400 envelope rather than a raw 422 — same convention - * as ``CreateClusterRequest``. - */ - ConnectionTestRequest: { - /** Auth Kind */ - auth_kind: string; - /** Base Url */ - base_url: string; - /** Credentials Ref */ - credentials_ref: string; - /** Engine Config */ - engine_config?: { - [key: string]: unknown; - } | null; - /** Engine Type */ - engine_type: string; - }; - /** - * ConnectionTestResult - * @description Response for ``POST /api/v1/clusters/test-connection``. - * - * Always 200 — reachable vs unreachable surfaces via ``reachable`` + - * ``status`` fields. The endpoint is a diagnostic, never a mutation, - * so it never returns 503; invalid engine×auth pairings 400 BEFORE the - * network call. (Cycle-delta F1.) - */ - ConnectionTestResult: { - /** Engine Capabilities */ - engine_capabilities?: { - [key: string]: unknown; - } | null; - /** Error */ - error?: string | null; - /** Reachable */ - reachable: boolean; - /** - * Status - * @enum {string} - */ - status: 'green' | 'yellow' | 'red' | 'unreachable'; - /** Version */ - version?: string | null; - }; - /** - * ConvergenceShape - * @description Where the winner sits in the Optuna trial sequence + the classified regime. - */ - ConvergenceShape: { - /** Best At Trial */ - best_at_trial: number; - /** - * Regime - * @enum {string} - */ - regime: 'early_held' | 'late_rising' | 'noisy'; - /** Total Trials */ - total_trials: number; - }; - /** - * ConversationDetail - * @description ``GET /api/v1/conversations/{id}`` response. - */ - ConversationDetail: { - /** - * Created At - * Format: date-time - */ - created_at: string; - /** Id */ - id: string; - /** Messages */ - messages: components['schemas']['MessageWire'][]; - /** Title */ - title: string | null; - }; - /** - * ConversationSummary - * @description ``GET /api/v1/conversations`` row + ``POST`` 201 body. - * - * ``last_message_preview`` is the most recent user / assistant message's - * ``content.text``, truncated at the repo layer to 120 chars (with ``…`` - * suffix when cut). Tool-role rows and assistant rows whose ``content.kind`` - * is ``system_notice`` are skipped. ``None`` for brand-new conversations - * with no qualifying messages — see ``chore_chat_last_message_preview``. - * - * ``last_message_at`` is the ``created_at`` of that same row, or ``None`` - * for empty conversations. The list page uses it to render "when did - * anyone last touch this thread" instead of the conversation's - * ``created_at``. - */ - ConversationSummary: { - /** - * Created At - * Format: date-time - */ - created_at: string; - /** Id */ - id: string; - /** Last Message At */ - last_message_at?: string | null; - /** Last Message Preview */ - last_message_preview?: string | null; - /** Message Count */ - message_count: number; - /** Title */ - title: string | null; - }; - /** - * ConversationsListResponse - * @description ``GET /api/v1/conversations`` response. - */ - ConversationsListResponse: { - /** Data */ - data: components['schemas']['ConversationSummary'][]; - /** Has More */ - has_more: boolean; - /** Next Cursor */ - next_cursor: string | null; - }; - /** - * CreateClusterRequest - * @description Request body for ``POST /api/v1/clusters``. - * - * See module docstring for the deliberate ``str`` vs ``Literal`` split. - */ - CreateClusterRequest: { - /** Auth Kind */ - auth_kind: string; - /** Base Url */ - base_url: string; - /** Credentials Ref */ - credentials_ref: string; - /** Engine Config */ - engine_config?: { - [key: string]: unknown; - } | null; - /** Engine Type */ - engine_type: string; - /** - * Environment - * @enum {string} - */ - environment: 'prod' | 'staging' | 'dev'; - /** Name */ - name: string; - /** Notes */ - notes?: string | null; - /** - * Target Filter - * @description Optional glob pattern (fnmatch.fnmatchcase: *, ?, [seq], [!seq]; no brace expansion). Scopes GET /clusters/{id}/targets to matching index names. Null = no filter. - */ - target_filter?: string | null; - }; - /** - * CreateConfigRepoRequest - * @description Body of ``POST /api/v1/config-repos`` (FR-3). - * - * ``provider`` is server-derived from ``repo_url`` (cycle-2 F4 from - * spec review) — NOT in the payload. The validator enforces a strict - * GitHub URL pattern; non-GitHub URLs surface as 400 - * ``UNSUPPORTED_PROVIDER`` at the router layer. - */ - CreateConfigRepoRequest: { - /** Auth Ref */ - auth_ref: string; - /** - * Default Branch - * @default main - */ - default_branch: string; - /** Name */ - name: string; - /** - * Pr Base Branch - * @default main - */ - pr_base_branch: string; - /** Repo Url */ - repo_url: string; - /** Webhook Secret Ref */ - webhook_secret_ref?: string | null; - }; - /** - * CreateConversationRequest - * @description ``POST /api/v1/conversations`` body. - */ - CreateConversationRequest: { - /** Title */ - title?: string | null; - }; - /** - * CreateJudgmentListFromUbiRequest - * @description Body for ``POST /api/v1/judgments/generate-from-ubi`` (Story 3.2 / FR-3). - * - * Mirrors :class:`backend.app.services.agent_judgments_dispatch.UbiJudgmentGenerationRequest`. - * The ``@model_validator(mode="after")`` enforces the conditional - * requiredness of ``current_template_id`` + ``rubric`` per the hybrid - * converter: REQUIRED when ``converter == 'hybrid_ubi_llm'`` (the LLM- - * fill path needs both); FORBIDDEN otherwise (pure UBI never calls - * the LLM so accepting them silently would mask operator error). - */ - CreateJudgmentListFromUbiRequest: { - /** Cluster Id */ - cluster_id: string; - /** - * Converter - * @enum {string} - */ - converter: 'ctr_threshold' | 'dwell_time' | 'hybrid_ubi_llm'; - /** Converter Config */ - converter_config?: { - [key: string]: unknown; - } | null; - /** Current Template Id */ - current_template_id?: string | null; - /** Description */ - description?: string | null; - /** - * Llm Fill Threshold - * @default 20 - */ - llm_fill_threshold: number | null; - /** - * Mapping Strategy - * @default reject - * @enum {string} - */ - mapping_strategy: 'reject' | 'first_match' | 'most_recent'; - /** - * Min Impressions Threshold - * @default 100 - */ - min_impressions_threshold: number | null; - /** Name */ - name: string; - /** Query Set Id */ - query_set_id: string; - /** Rubric */ - rubric?: string | null; - /** - * Since - * Format: date-time - */ - since: string; - /** Target */ - target: string; - /** Until */ - until?: string | null; - }; - /** - * CreateJudgmentListGenerateRequest - * @description Body for ``POST /api/v1/judgments/generate`` (Story 3.1). - */ - CreateJudgmentListGenerateRequest: { - /** Cluster Id */ - cluster_id: string; - /** Current Template Id */ - current_template_id: string; - /** Description */ - description?: string | null; - /** Name */ - name: string; - /** Query Set Id */ - query_set_id: string; - /** Rubric */ - rubric: string; - /** Target */ - target: string; - }; - /** - * CreateProposalRequest - * @description Body of ``POST /api/v1/proposals`` (manual proposal creation, FR-4 / AC-6). - */ - CreateProposalRequest: { - /** Cluster Id */ - cluster_id: string; - /** Config Diff */ - config_diff: { - [key: string]: unknown; - }; - /** Metric Delta */ - metric_delta?: { - [key: string]: unknown; - } | null; - /** Template Id */ - template_id: string; - }; - /** - * CreateQuerySetRequest - * @description ``POST /api/v1/query-sets`` body. - * - * ``cluster_id`` is required because Phase 1's shipped schema has - * ``query_sets.cluster_id NOT NULL``. Spec FR-3 wording (``cluster_id?``) - * is documented drift tracked at - * ``docs/00_overview/planned_features/chore_spec_query_set_cluster_id_drift/idea.md``. - */ - CreateQuerySetRequest: { - /** Cluster Id */ - cluster_id: string; - /** Description */ - description?: string | null; - /** Name */ - name: string; - }; - /** - * CreateQueryTemplateRequest - * @description Request body for ``POST /api/v1/query-templates``. - */ - CreateQueryTemplateRequest: { - /** Body */ - body: string; - /** Declared Params */ - declared_params?: { - [key: string]: string; - }; - /** - * Engine Type - * @enum {string} - */ - engine_type: 'elasticsearch' | 'opensearch' | 'solr'; - /** Name */ - name: string; - /** Parent Id */ - parent_id?: string | null; - }; - /** - * CreateStudyRequest - * @description ``POST /api/v1/studies`` body. - * - * ``search_space`` is validated post-Pydantic-parse via - * :class:`backend.app.domain.study.search_space.SearchSpace` so - * :exc:`pydantic.ValidationError` produces the spec's 400 - * ``INVALID_SEARCH_SPACE`` (per Story 3.3 task 2). - * - * feat_digest_executable_followups Story 4.2 — optional ``parent`` field - * records the parent proposal + followup-index lineage when the study - * was spawned from a digest "Run this followup" action (FR-11). - */ - CreateStudyRequest: { - /** Cluster Id */ - cluster_id: string; - config: components['schemas']['StudyConfigSpec']; - /** Judgment List Id */ - judgment_list_id: string; - /** Name */ - name: string; - objective: components['schemas']['ObjectiveSpec']; - parent?: components['schemas']['ParentFollowupRef'] | null; - /** - * Parent Study Id - * @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. - */ - parent_study_id?: string | null; - /** Query Set Id */ - query_set_id: string; - /** Search Space */ - search_space: { - [key: string]: unknown; - }; - /** Target */ - target: string; - /** Template Id */ - template_id: string; - }; - /** - * CurvePoint - * @description One point on the best-so-far curve. - * - * ``trial_number`` is the trial's ``optuna_trial_number`` (the canonical - * "trial order within the study" field — see ``auto_followup.py`` module - * docstring for why we sort by this rather than ``started_at``). - * ``best_so_far`` is the running extremum of ``primary_metric`` over all - * earlier trials, sign-corrected to the study's optimization direction. - */ - CurvePoint: { - /** Best So Far */ - best_so_far: number; - /** Trial Number */ - trial_number: number; - }; - /** - * DigestResponse - * @description Body of ``GET /api/v1/studies/{id}/digest`` (FR-3 / AC-3). - * - * feat_digest_executable_followups Story 4.1 — ``suggested_followups`` is - * now a discriminated-union list (NarrowFollowup | WidenFollowup | - * TextFollowup), populated by the digest handler via - * ``parse_followup_list(digest.suggested_followups, ...)`` so legacy or - * malformed JSONB payloads never crash the response. - */ - DigestResponse: { - /** - * Generated At - * Format: date-time - */ - generated_at: string; - /** Generated By */ - generated_by: string; - /** Id */ - id: string; - /** Narrative */ - narrative: string; - /** Parameter Importance */ - parameter_importance: { - [key: string]: number; - }; - /** Recommended Config */ - recommended_config: { - [key: string]: unknown; - }; - /** Study Id */ - study_id: string; - /** Suggested Followups */ - suggested_followups: components['schemas']['FollowupItem'][]; - }; - /** - * Document - * @description A single document by ID — return shape of ``SearchAdapter.get_document``. - * - * Mirrors :class:`ScoredHit` minus ``score`` (browsing doesn't need scoring). - * ``source`` is ``None`` when the engine's index has ``_source: false`` mapping. - */ - Document: { - /** Doc Id */ - doc_id: string; - /** Source */ - source?: { - [key: string]: unknown; - } | null; - }; - /** - * DocumentListResponse - * @description ``GET /api/v1/clusters/{cluster_id}/targets/{target}/documents`` response. - * - * ``next_cursor`` opaque-encodes the ES ``hits[-1].sort`` array of the - * last visible row when ``has_more`` is True (see - * ``backend.app.api.v1._documents_cursor``). The ``X-Total-Count`` header - * on the response carries the engine's ``hits.total.value``. - */ - DocumentListResponse: { - /** Data */ - data: components['schemas']['DocumentSummary'][]; - /** Has More */ - has_more: boolean; - /** Next Cursor */ - next_cursor: string | null; - }; - /** - * DocumentSummary - * @description One row in the documents list (per FR-3 / FR-8). - * - * ``source`` is the *truncated* preview emitted by - * ``backend.app.services.documents.truncate_source_for_list``. The detail - * endpoint returns the untruncated ``Document.source``. - */ - DocumentSummary: { - /** Doc Id */ - doc_id: string; - /** Source */ - source: { - [key: string]: unknown; - } | null; - }; - /** - * FieldSpec - * @description One field returned by ``get_schema``. - */ - FieldSpec: { - /** Analyzer */ - analyzer?: string | null; - /** Doc Count */ - doc_count?: number | null; - /** Name */ - name: string; - /** Type */ - type: string; - }; - /** - * FloatParam - * @description Continuous float parameter. - * - * ``log=True`` enables log-uniform sampling - * (Optuna's ``suggest_float(..., log=True)``); requires ``low > 0``. - */ - FloatParam: { - /** High */ - high: number; - /** - * Log - * @default false - */ - log: boolean; - /** Low */ - low: number; - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: 'float'; - }; - FollowupItem: - | components['schemas']['NarrowFollowup'] - | components['schemas']['WidenFollowup'] - | components['schemas']['TextFollowup'] - | components['schemas']['SwapTemplateFollowup']; - /** - * GenerateJudgmentsResponse - * @description Response of ``POST /api/v1/judgments/generate``. - * - * Per GPT-5.5 cycle 1 F5 — the endpoint registers a typed - * ``response_model`` so OpenAPI introspection + contract tests can verify - * the wire shape. - */ - GenerateJudgmentsResponse: { - /** Judgment List Id */ - judgment_list_id: string; - /** - * Status - * @constant - */ - status: 'generating'; - }; - /** HTTPValidationError */ - HTTPValidationError: { - /** Detail */ - detail?: components['schemas']['ValidationError'][]; - }; - /** - * HeadlineShape - * @description Top-line metric value + N(queries) used in the CI. - * - * ``metric`` uses ``str`` (not ``ObjectiveMetric``) to avoid a circular - * import: ``schemas.py`` imports ``ConfidenceShape`` from here, so this - * module cannot import back from ``schemas.py``. The upstream value is - * already validated by the existing ``ObjectiveMetric`` Literal at the - * create-study endpoint (``schemas.py:214``). - */ - HeadlineShape: { - /** K */ - k: number | null; - /** Metric */ - metric: string; - /** N Queries */ - n_queries: number | null; - /** Value */ - value: number; - }; - /** - * HealthCheckResult - * @description Wire shape of the per-cluster health probe (mirrors ``HealthStatus``). - */ - HealthCheckResult: { - /** Checked At */ - checked_at: string; - /** Error */ - error?: string | null; - /** - * Status - * @enum {string} - */ - status: 'green' | 'yellow' | 'red' | 'unreachable'; - /** Version */ - version?: string | null; - }; - /** - * HealthResponse - * @description The /healthz response body. Same shape for HTTP 200 and 503. - */ - HealthResponse: { - openai_capabilities: components['schemas']['OpenAICapabilities']; - /** - * Openai Endpoint - * @description Configured OPENAI_BASE_URL - */ - openai_endpoint: string; - /** - * Status - * @enum {string} - */ - status: 'ok' | 'degraded'; - subsystems: components['schemas']['Subsystems']; - /** - * Uptime Seconds - * @description Seconds since the API process started - */ - uptime_seconds: number; - /** - * Version - * @description Application version (relyloop_git_sha) - */ - version: string; - }; - /** - * ImportJudgmentItem - * @description One row in :class:`ImportJudgmentListRequest`. - */ - ImportJudgmentItem: { - /** Doc Id */ - doc_id: string; - /** Notes */ - notes?: string | null; - /** Query Id */ - query_id: string; - /** - * Rating - * @enum {integer} - */ - rating: 0 | 1 | 2 | 3; - }; - /** - * ImportJudgmentListRequest - * @description Body for ``POST /api/v1/judgment-lists/import`` (Story 3.2). - */ - ImportJudgmentListRequest: { - /** Cluster Id */ - cluster_id: string; - /** Description */ - description?: string | null; - /** Judgments */ - judgments: components['schemas']['ImportJudgmentItem'][]; - /** Name */ - name: string; - /** Query Set Id */ - query_set_id: string; - /** Rubric */ - rubric: string; - /** Target */ - target: string; - }; - /** - * IntParam - * @description Integer parameter inclusive of both bounds. - */ - IntParam: { - /** High */ - high: number; - /** Low */ - low: number; - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: 'int'; - }; - /** - * JudgmentListDetail - * @description ``GET /api/v1/judgment-lists/{id}`` response. - * - * Note: ``generation_params`` is populated for UBI lists (feat_ubi_judgments - * Story 1.1's JSONB column) and NULL for LLM lists. The Story 4.3 UI - * (```` + ````) reads the - * payload to discriminate UBI/hybrid lists and to reconstruct the - * original request for the ambiguous-skip "Re-run with most_recent" - * affordance. - */ - JudgmentListDetail: { - /** Calibration */ - calibration: { - [key: string]: unknown; - } | null; - /** Cluster Id */ - cluster_id: string; - /** - * Created At - * Format: date-time - */ - created_at: string; - /** Current Template Id */ - current_template_id: string | null; - /** Description */ - description: string | null; - /** Failed Reason */ - failed_reason: string | null; - /** Generation Params */ - generation_params: { - [key: string]: unknown; - } | null; - /** Id */ - id: string; - /** Judgment Count */ - judgment_count: number; - /** Name */ - name: string; - /** Query Set Id */ - query_set_id: string; - /** Rubric */ - rubric: string; - source_breakdown: components['schemas']['_SourceBreakdown']; - /** - * Status - * @enum {string} - */ - status: 'generating' | 'complete' | 'failed'; - /** Target */ - target: string; - }; - /** - * JudgmentListJudgmentsResponse - * @description ``GET /api/v1/judgment-lists/{id}/judgments`` response. - */ - JudgmentListJudgmentsResponse: { - /** Data */ - data: components['schemas']['JudgmentRow'][]; - /** Has More */ - has_more: boolean; - /** Next Cursor */ - next_cursor: string | null; - }; - /** - * JudgmentListListResponse - * @description ``GET /api/v1/judgment-lists`` response. - */ - JudgmentListListResponse: { - /** Data */ - data: components['schemas']['JudgmentListSummary'][]; - /** Has More */ - has_more: boolean; - /** Next Cursor */ - next_cursor: string | null; - }; - /** - * JudgmentListRef - * @description One entry in the ``QUERY_HAS_JUDGMENTS`` 409 envelope. - * - * Lives in ``detail.judgment_lists``. Maps from the repo-layer - * :class:`backend.app.db.repo.judgment.JudgmentListRefRow` at the - * router boundary. - */ - JudgmentListRef: { - /** Id */ - id: string; - /** Name */ - name: string; - }; - /** - * JudgmentListSummary - * @description List-view row on ``GET /api/v1/judgment-lists``. - */ - JudgmentListSummary: { - /** Cluster Id */ - cluster_id: string; - /** - * Created At - * Format: date-time - */ - created_at: string; - /** Description */ - description: string | null; - /** Id */ - id: string; - /** Name */ - name: string; - /** Query Set Id */ - query_set_id: string; - /** - * Status - * @enum {string} - */ - status: 'generating' | 'complete' | 'failed'; - /** Target */ - target: string; - }; - /** - * JudgmentRow - * @description ``GET /api/v1/judgment-lists/{id}/judgments`` row + PATCH response. - */ - JudgmentRow: { - /** Confidence */ - confidence: number | null; - /** - * Created At - * Format: date-time - */ - created_at: string; - /** Doc Id */ - doc_id: string; - /** Id */ - id: string; - /** Judgment List Id */ - judgment_list_id: string; - /** Notes */ - notes: string | null; - /** Query Id */ - query_id: string; - /** Rater Ref */ - rater_ref: string | null; - /** - * Rating - * @enum {integer} - */ - rating: 0 | 1 | 2 | 3; - /** - * Source - * @enum {string} - */ - source: 'llm' | 'human' | 'click'; - }; - /** - * LateTrialStddevShape - * @description Sample stddev of ``primary_metric`` over the late-trial window. - */ - LateTrialStddevShape: { - /** Min Window Required */ - min_window_required: number; - /** Value */ - value: number; - /** Window Size */ - window_size: number; - }; - /** - * MessageWire - * @description One row of ``GET /api/v1/conversations/{id}.messages``. - */ - MessageWire: { - /** Content */ - content: { - [key: string]: unknown; - }; - /** - * Created At - * Format: date-time - */ - created_at: string; - /** Id */ - id: string; - /** - * Role - * @enum {string} - */ - role: 'user' | 'assistant' | 'tool'; - /** Tool Calls */ - tool_calls?: - | { - [key: string]: unknown; - }[] - | null; - }; - /** - * NarrowFollowup - * @description A 'narrow' followup — re-run with a tighter range than the parent. - */ - NarrowFollowup: { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - kind: 'narrow'; - /** Rationale */ - rationale: string; - search_space: components['schemas']['SearchSpace']; - }; - /** - * ObjectiveSpec - * @description Wire shape of ``studies.objective`` (write-side validated at create). - * - * ``k`` is required for ``ndcg`` / ``precision`` / ``recall`` (per - * standard IR-evaluation conventions: those metrics are computed at a - * cutoff rank). ``map`` accepts ``k`` optionally; ``mrr`` / ``err`` ignore - * it. The model_validator enforces this so a malformed objective - * surfaces as 400 ``INVALID_SEARCH_SPACE`` / 422 ``VALIDATION_ERROR`` - * at study-create time rather than failing later inside ``run_trial`` - * when the worker computes the metric. - */ - ObjectiveSpec: { - /** - * Direction - * @default maximize - * @enum {string} - */ - direction: 'maximize' | 'minimize'; - /** K */ - k?: (1 | 3 | 5 | 10 | 20 | 50 | 100) | null; - /** - * Metric - * @enum {string} - */ - metric: 'ndcg' | 'map' | 'precision' | 'recall' | 'mrr'; - }; - /** - * OpenAICapabilities - * @description Cached results of the OpenAI capability check (Story 3.3 populates Redis). - * - * Step 1 (``models_endpoint``) is reported first because it gates the rest: - * when it fails, the other three are reported as ``"untested"``. The - * ``models_endpoint_status_code`` field is required-but-nullable - * (per ``bug_openai_capability_check_incapable_on_valid_key`` spec §19 D-3/D-8) - * — always present in the JSON, ``null`` when not applicable. This lets - * operators distinguish ``401 -> bad key``, ``429 -> quota``, - * ``5xx -> upstream outage``, ``null -> network unreachable / cache miss``. - */ - OpenAICapabilities: { - /** - * Chat - * @description Chat completion probe result - * @enum {string} - */ - chat: 'ok' | 'fail' | 'untested'; - /** - * Function Calling - * @description Function-calling probe result (tool_choice=required) - * @enum {string} - */ - function_calling: 'ok' | 'fail' | 'untested'; - /** - * 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 {string} - */ - models_endpoint: 'ok' | 'fail' | 'untested'; - /** - * Models Endpoint Status Code - * @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. - */ - models_endpoint_status_code: number | null; - /** - * Structured Output - * @description JSON-schema response_format probe result - * @enum {string} - */ - structured_output: 'ok' | 'fail' | 'untested'; - }; - /** - * OpenPrResponse - * @description Body of ``POST /api/v1/proposals/{id}/open_pr`` (FR-1). - * - * Returned with HTTP 202 on successful enqueue. Status is always - * ``'pending'`` at enqueue time; the worker flips it to ``'pr_opened'`` - * after the PR is open. - */ - OpenPrResponse: { - /** Message */ - message: string; - /** Proposal Id */ - proposal_id: string; - /** - * Status - * @constant - */ - status: 'pending'; - }; - /** - * OverrideJudgmentRequest - * @description Body for ``PATCH /api/v1/judgment-lists/{id}/judgments/{judgment_id}``. - * - * ``rating`` is INTENTIONALLY unbounded at the Pydantic layer — spec §8.5 - * requires out-of-range failures to surface as 400 ``INVALID_RATING`` (not - * Pydantic's default 422 ``VALIDATION_ERROR``). The handler validates the - * value manually and raises the domain code (per GPT-5.5 cycle 1 F4). - */ - OverrideJudgmentRequest: { - /** Notes */ - notes?: string | null; - /** Rating */ - rating: number; - }; - /** - * ParentFollowupRef - * @description Optional lineage payload on ``POST /api/v1/studies``. - * - * feat_digest_executable_followups FR-11 — when the operator clicks - * "Run this followup" on a proposal's digest card, the create-study - * payload carries the parent proposal's id + the 0-based index into - * the digest's ``suggested_followups`` array so the spawned study - * remembers where it came from. - * - * ``proposal_id`` is a UUIDv7 (36-char hex). The exact-length bound - * forces malformed strings to surface as 422 ``VALIDATION_ERROR`` - * rather than reach the DB FK check and emerge as a 404 - * ``PROPOSAL_NOT_FOUND``. - */ - ParentFollowupRef: { - /** Followup Index */ - followup_index: number; - /** Proposal Id */ - proposal_id: string; - }; - /** - * PerQueryOutcomesShape - * @description Per-query outcome counts + the top-5 named regressors and improvers. - */ - PerQueryOutcomesShape: { - /** - * Comparison Against - * @enum {string} - */ - comparison_against: 'runner_up' | 'baseline'; - /** Improved */ - improved: number; - /** Regressed */ - regressed: number; - /** - * Top Improvers - * @default [] - */ - top_improvers: components['schemas']['RegressorRowShape'][]; - /** Top Regressors */ - top_regressors: components['schemas']['RegressorRowShape'][]; - /** Unchanged */ - unchanged: number; - }; - /** - * ProposalDetail - * @description Body of the proposal detail endpoints. - * - * Used by ``GET /api/v1/proposals/{id}``, ``POST /api/v1/proposals``, - * and ``POST /api/v1/proposals/{id}/reject``. - */ - ProposalDetail: { - cluster: components['schemas']['_ClusterEmbed']; - /** Config Diff */ - config_diff: { - [key: string]: unknown; - }; - /** - * Created At - * Format: date-time - */ - created_at: string; - digest: components['schemas']['_DigestEmbed'] | null; - /** Id */ - id: string; - /** - * Is Currently Live - * @default false - */ - is_currently_live: boolean; - /** Metric Delta */ - metric_delta: { - [key: string]: unknown; - } | null; - /** Pr Merged At */ - pr_merged_at: string | null; - /** Pr Open Error */ - pr_open_error: string | null; - /** Pr State */ - pr_state: ('open' | 'closed' | 'merged') | null; - /** Pr Url */ - pr_url: string | null; - /** Rejected Reason */ - rejected_reason: string | null; - /** - * Status - * @enum {string} - */ - status: 'pending' | 'pr_opened' | 'pr_merged' | 'rejected'; - /** Study Id */ - study_id: string | null; - study_summary: components['schemas']['_StudySummary'] | null; - /** Study Trial Id */ - study_trial_id: string | null; - template: components['schemas']['_TemplateEmbed']; - }; - /** - * ProposalSummary - * @description Row in the ``GET /api/v1/proposals`` list response. - */ - ProposalSummary: { - cluster: components['schemas']['_ClusterEmbed']; - /** - * Created At - * Format: date-time - */ - created_at: string; - /** Id */ - id: string; - /** - * Is Currently Live - * @default false - */ - is_currently_live: boolean; - /** Metric Delta */ - metric_delta: { - [key: string]: unknown; - } | null; - /** Pr State */ - pr_state: ('open' | 'closed' | 'merged') | null; - /** Pr Url */ - pr_url: string | null; - /** - * Status - * @enum {string} - */ - status: 'pending' | 'pr_opened' | 'pr_merged' | 'rejected'; - /** Study Id */ - study_id: string | null; - template: components['schemas']['_TemplateEmbed']; - }; - /** - * ProposalsListResponse - * @description Body of ``GET /api/v1/proposals``. - */ - ProposalsListResponse: { - /** Data */ - data: components['schemas']['ProposalSummary'][]; - /** Has More */ - has_more: boolean; - /** Next Cursor */ - next_cursor: string | null; - }; - /** - * QueryHasJudgmentsDetail - * @description The ``detail`` object of a 409 ``QUERY_HAS_JUDGMENTS`` response. - * - * Extends the canonical ``{error_code, message, retryable}`` envelope - * with two structured fields the frontend consumes directly - * (``judgment_lists`` + ``overflow_count``). Wired into the FastAPI - * route's ``responses={409: {"model": QueryHasJudgmentsEnvelope}}`` so - * the OpenAPI schema documents the contract. - */ - QueryHasJudgmentsDetail: { - /** - * Error Code - * @constant - */ - error_code: 'QUERY_HAS_JUDGMENTS'; - /** Judgment Lists */ - judgment_lists: components['schemas']['JudgmentListRef'][]; - /** Message */ - message: string; - /** Overflow Count */ - overflow_count: number; - /** - * Retryable - * @constant - */ - retryable: false; - }; - /** - * QueryHasJudgmentsEnvelope - * @description Top-level 409 wrapper (FastAPI nests under ``detail`` for HTTPException). - */ - QueryHasJudgmentsEnvelope: { - detail: components['schemas']['QueryHasJudgmentsDetail']; - }; - /** - * QueryListResponse - * @description ``GET /api/v1/query-sets/{set_id}/queries`` response. - */ - QueryListResponse: { - /** Data */ - data: components['schemas']['QueryRow'][]; - /** Has More */ - has_more: boolean; - /** Next Cursor */ - next_cursor: string | null; - }; - /** - * QueryRow - * @description Wire row returned by the per-query GET + PATCH endpoints. - * - * Used by both ``GET /api/v1/query-sets/{set_id}/queries`` and - * ``PATCH /api/v1/query-sets/{set_id}/queries/{query_id}``. - * ``judgment_count`` is a derived field — single batched GROUP BY in the - * router via :func:`backend.app.db.repo.judgment.count_judgments_per_query`. - */ - QueryRow: { - /** Id */ - id: string; - /** Judgment Count */ - judgment_count: number; - /** Query Metadata */ - query_metadata: { - [key: string]: unknown; - } | null; - /** Query Text */ - query_text: string; - /** Reference Answer */ - reference_answer: string | null; - }; - /** - * QuerySetDetail - * @description ``GET /api/v1/query-sets/{id}`` response. - */ - QuerySetDetail: { - /** Cluster Id */ - cluster_id: string; - /** - * Created At - * Format: date-time - */ - created_at: string; - /** Description */ - description: string | null; - /** Id */ - id: string; - /** Name */ - name: string; - /** Query Count */ - query_count: number; - }; - /** - * QuerySetListResponse - * @description ``GET /api/v1/query-sets`` response. - */ - QuerySetListResponse: { - /** Data */ - data: components['schemas']['QuerySetSummary'][]; - /** Has More */ - has_more: boolean; - /** Next Cursor */ - next_cursor: string | null; - }; - /** - * QuerySetSummary - * @description List-view shape; omits ``query_count`` to avoid N+1 counts at list time. - */ - QuerySetSummary: { - /** Cluster Id */ - cluster_id: string; - /** - * Created At - * Format: date-time - */ - created_at: string; - /** Id */ - id: string; - /** Name */ - name: string; - }; - /** - * QueryTemplateDetail - * @description ``GET /api/v1/query-templates/{id}`` response. - */ - QueryTemplateDetail: { - /** Body */ - body: string; - /** - * Created At - * Format: date-time - */ - created_at: string; - /** Declared Params */ - declared_params: { - [key: string]: string; - }; - /** - * Engine Type - * @enum {string} - */ - engine_type: 'elasticsearch' | 'opensearch' | 'solr'; - /** Id */ - id: string; - /** Name */ - name: string; - /** Parent Id */ - parent_id: string | null; - /** Version */ - version: number; - }; - /** - * QueryTemplateListResponse - * @description ``GET /api/v1/query-templates`` response. - */ - QueryTemplateListResponse: { - /** Data */ - data: components['schemas']['QueryTemplateSummary'][]; - /** Has More */ - has_more: boolean; - /** Next Cursor */ - next_cursor: string | null; - }; - /** - * QueryTemplateSummary - * @description List-view shape; drops ``body`` + ``declared_params`` for brevity. - */ - QueryTemplateSummary: { - /** - * Created At - * Format: date-time - */ - created_at: string; - /** - * Engine Type - * @enum {string} - */ - engine_type: 'elasticsearch' | 'opensearch' | 'solr'; - /** Id */ - id: string; - /** Name */ - name: string; - /** Version */ - version: number; - }; - /** - * RegressorRowShape - * @description One row in the named-regressors or named-improvers table. - * - * Used for BOTH the ``top_regressors`` and ``top_improvers`` lists. - * The wire shape is identical — ``delta = winner_score - comparison_score`` - * is negative on the regressor list, positive on the improver list. The - * class name is historical (regressors shipped first); reusing the same - * type keeps the schema and the per-row renderer compact. - */ - RegressorRowShape: { - /** Comparison Score */ - comparison_score: number; - /** Delta */ - delta: number; - /** Query Id */ - query_id: string; - /** Query Text */ - query_text: string; - /** Winner Score */ - winner_score: number; - }; - /** - * RejectProposalRequest - * @description Body of ``POST /api/v1/proposals/{id}/reject`` (FR-4 / AC-5). - */ - RejectProposalRequest: { - /** Reason */ - reason?: string | null; - }; - /** - * ReseedStatusResponse - * @description Polling-endpoint response for ``GET /api/v1/_test/demo/reseed/status``. - * - * Per ``bug_demo_reseed_fake_metric_regression`` D-2. Lives in Redis as a - * single JSON blob keyed by :data:`DEMO_RESEED_STATUS_KEY` so the - * handler reads it in one round-trip. - */ - ReseedStatusResponse: { - /** Current Step */ - current_step?: string | null; - /** Failed Reason */ - failed_reason?: string | null; - /** Finished At */ - finished_at?: string | null; - /** - * Scenarios Completed - * @default 0 - */ - scenarios_completed: number; - /** Scenarios Skipped */ - scenarios_skipped?: string[]; - /** - * Scenarios Total - * @default 0 - */ - scenarios_total: number; - /** Started At */ - started_at?: string | null; - /** - * Status - * @enum {string} - */ - status: 'idle' | 'running' | 'complete' | 'failed'; - /** Steps */ - steps?: string[]; - summary?: components['schemas']['ReseedSummary'] | null; - }; - /** - * ReseedSummary - * @description Returned by :func:`reseed_demo_state` on success. - * - * Per spec §9 Required invariants, every counter is exactly 4 on the - * happy path; ``duration_ms`` is wall-clock from orchestration start - * to the rename commit. - */ - ReseedSummary: { - /** Clusters Created */ - clusters_created: number; - /** Duration Ms */ - duration_ms: number; - /** Proposals Created */ - proposals_created: number; - /** Query Sets Created */ - query_sets_created: number; - /** Studies Completed */ - studies_completed: number; - }; - /** - * RunQueryHit - * @description One hit in the ``run_query`` response. - */ - RunQueryHit: { - /** Doc Id */ - doc_id: string; - /** Score */ - score: number; - /** Source */ - source?: { - [key: string]: unknown; - } | null; - }; - /** - * RunQueryRequest - * @description ``POST /api/v1/clusters/{id}/run_query`` body. - */ - RunQueryRequest: { - /** Query Dsl */ - query_dsl: { - [key: string]: unknown; - }; - /** Target */ - target: string; - /** - * Top K - * @default 10 - */ - top_k: number; - }; - /** - * RunQueryResponse - * @description ``POST /api/v1/clusters/{id}/run_query`` response. - */ - RunQueryResponse: { - /** Hits */ - hits: components['schemas']['RunQueryHit'][]; - }; - /** - * RunnerUpGapShape - * @description Runner-up trial's metric vs the winner. - * - * The whole shape is suppressed to ``None`` when there are <2 complete - * trials (FR-2 + FR-7); ``classification`` is non-null whenever this shape - * is present. - */ - RunnerUpGapShape: { - /** - * Classification - * @enum {string} - */ - classification: 'robust_plateau' | 'sharp_peak'; - /** Runner Up Metric */ - runner_up_metric: number; - /** Top10 Within */ - top10_within: number; - /** Value */ - value: number; - }; - /** - * Schema - * @description An index / collection's field schema. - */ - Schema: { - /** Fields */ - fields: components['schemas']['FieldSpec'][]; - /** Name */ - name: string; - }; - /** - * SearchSpace - * @description Pydantic model for the ``studies.search_space`` JSONB column. - * - * Wire format:: - * - * { - * "params": { - * "boost_title": {"type": "float", "low": 0.1, "high": 10.0, "log": true}, - * "min_should_match": {"type": "int", "low": 1, "high": 5}, - * "operator": {"type": "categorical", "choices": ["and", "or"]}, - * } - * } - */ - SearchSpace: { - /** Params */ - params: { - [key: string]: - | components['schemas']['FloatParam'] - | components['schemas']['IntParam'] - | components['schemas']['CategoricalParam']; - }; - }; - /** - * SeedAutoFollowupChainRequest - * @description Payload for ``POST /api/v1/_test/auto-followup/seed-chain``. - * - * Seeds ``depth + 1`` linked studies (root → … → leaf) so E2E tests can - * cover the chain-panel parent-link / children-table / cascade-radio paths - * that the public ``POST /api/v1/studies`` endpoint can't drive - * (``parent_study_id`` is set only by the auto-followup worker). - * - * Closes ``chore_auto_followup_e2e_chain_seed_helper`` (idea #2). - */ - SeedAutoFollowupChainRequest: { - /** Cluster Id */ - cluster_id: string; - /** - * Depth - * @description Number of chain hops to seed. depth=1 → root + leaf (2 nodes). depth=2 → root + 1 middle + leaf (3 nodes). - */ - depth: number; - /** - * In Flight Leaf - * @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. - * @default true - */ - in_flight_leaf: boolean; - /** - * In Flight Middle - * @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). - * @default true - */ - in_flight_middle: boolean; - /** Judgment List Id */ - judgment_list_id: string; - /** Query Set Id */ - query_set_id: string; - /** Template Id */ - template_id: string; - }; - /** - * SeedAutoFollowupChainResponse - * @description IDs of every node in the seeded chain, in parent→child order. - */ - SeedAutoFollowupChainResponse: { - /** Leaf Id */ - leaf_id: string; - /** Middle Ids */ - middle_ids: string[]; - /** Root Id */ - root_id: string; - }; - /** - * SeedCompletedStudyRequest - * @description Payload for ``POST /api/v1/_test/studies/seed-completed``. - * - * All four FK fields are required; the caller is responsible for - * seeding the parent rows first (typically via the public - * ``seedFullChain`` E2E helper). - */ - SeedCompletedStudyRequest: { - /** Cluster Id */ - cluster_id: string; - /** - * Extra Trial Metrics - * @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. - */ - extra_trial_metrics?: number[] | null; - /** Judgment List Id */ - judgment_list_id: string; - /** Query Set Id */ - query_set_id: string; - /** - * Runner Up Per Query - * @description Optional per-query metrics for the runner-up trial; pairs with `winner_per_query`. - */ - runner_up_per_query?: { - [key: string]: { - [key: string]: unknown; - }; - } | null; - /** - * Suggested Followups - * @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. - */ - suggested_followups?: - | { - [key: string]: unknown; - }[] - | null; - /** Template Id */ - template_id: string; - /** - * Winner Per Query - * @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). - */ - winner_per_query?: { - [key: string]: { - [key: string]: unknown; - }; - } | null; - /** - * With Pending Proposal - * @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. - * @default true - */ - with_pending_proposal: boolean; - }; - /** - * SeedCompletedStudyResponse - * @description IDs of the inserted rows; mirrors :class:`SeededStudyTriple`. - */ - SeedCompletedStudyResponse: { - /** Digest Id */ - digest_id: string; - /** Proposal Id */ - proposal_id: string | null; - /** Study Id */ - study_id: string; - }; - /** - * SendMessageRequest - * @description ``POST /api/v1/conversations/{id}/messages`` body (Story 3.2). - */ - SendMessageRequest: { - content: components['schemas']['SendMessageRequestContent']; - /** - * Role - * @default user - * @constant - */ - role: 'user'; - }; - /** - * SendMessageRequestContent - * @description Sub-shape inside :class:`SendMessageRequest`. - */ - SendMessageRequestContent: { - /** Text */ - text: string; - }; - /** - * StudyChainLink - * @description One link in the rolled-up overnight-chain summary (feat_overnight_autopilot §8.3). - */ - StudyChainLink: { - /** Auto Followup Depth Remaining */ - auto_followup_depth_remaining: number | null; - /** Baseline Metric */ - baseline_metric: number | null; - /** Best Metric */ - best_metric: number | null; - /** Completed At */ - completed_at: string | null; - /** - * Created At - * Format: date-time - */ - created_at: string; - /** Delta From Prev */ - delta_from_prev: number | null; - /** - * Direction - * @enum {string} - */ - direction: 'maximize' | 'minimize'; - /** Failed Reason */ - failed_reason: string | null; - /** Id */ - id: string; - /** Name */ - name: string; - /** Proposal Id */ - proposal_id: string | null; - /** - * Status - * @enum {string} - */ - status: 'queued' | 'running' | 'completed' | 'cancelled' | 'failed'; - }; - /** - * StudyChainResponse - * @description ``GET /api/v1/studies/{id}/chain`` response (feat_overnight_autopilot §8.3). - */ - StudyChainResponse: { - /** Anchor Study Id */ - anchor_study_id: string; - /** Best Link Id */ - best_link_id: string | null; - /** Best Metric */ - best_metric: number | null; - /** Cumulative Lift */ - cumulative_lift: number | null; - /** - * Direction - * @enum {string} - */ - direction: 'maximize' | 'minimize'; - /** Links */ - links: components['schemas']['StudyChainLink'][]; - /** Proposal Id For Best Link */ - proposal_id_for_best_link: string | null; - /** - * Stop Reason - * @enum {string} - */ - stop_reason: - | 'depth_exhausted' - | 'no_lift' - | 'budget' - | 'parent_failed' - | 'cancelled' - | 'in_flight'; - }; - /** - * StudyConfigSpec - * @description Wire shape of ``studies.config`` (write-side). - * - * The model_validator below enforces that at least one stop condition is - * set — otherwise the study has no terminating condition (FR-4). - * ``parallelism`` / ``trial_timeout_s`` are optional; when absent the - * worker reads ``Settings.studies_default_parallelism`` / - * ``studies_default_timeout_s`` at job time. The API layer does NOT - * materialize these fields into the stored row — see Story 1.5 + - * Story 3.3's ``config.model_dump(exclude_none=True, exclude_unset=True)`` - * contract. - */ - StudyConfigSpec: { - /** Auto Followup Depth */ - auto_followup_depth?: number | null; - /** Baseline Params */ - baseline_params?: { - [key: string]: string | number | boolean | null; - } | null; - /** Max Trials */ - max_trials?: number | null; - /** Parallelism */ - parallelism?: number | null; - /** Pruner */ - pruner?: ('median' | 'none') | null; - /** Sampler */ - sampler?: ('tpe' | 'random') | null; - /** Secondary Metrics */ - secondary_metrics?: string[] | null; - /** Seed */ - seed?: number | null; - /** Time Budget Min */ - time_budget_min?: number | null; - /** Trial Timeout S */ - trial_timeout_s?: number | null; - }; - /** - * StudyConvergenceShape - * @description Verdict + supporting numerics for the UI panel and the digest narrative. - * - * Mirrors the ``ConfidenceShape`` pattern from ``confidence.py``: the - * domain module owns the Pydantic model, and ``backend.app.api.v1.schemas`` - * re-exports it for the ``StudyDetail.convergence`` field. The - * ``best_so_far_curve`` is the chart's data series; ``verdict`` is the - * badge label. - * - * **Name discipline (plan §0).** The bare class name ``ConvergenceShape`` - * is already taken by :class:`backend.app.domain.study.confidence.ConvergenceShape` - * (a different concept — winner-trial *timing*, not metric plateau). - * ``StudyConvergenceShape`` is the study-level analogue; the confidence - * sub-shape stays on its inner module. The two coexist on ``StudyDetail`` - * (``confidence.convergence`` is the inner one; ``convergence`` is this - * one), and FastAPI emits both under their bare class names in the - * OpenAPI schema — no fully-qualified disambiguation noise leaks to the - * frontend. - */ - StudyConvergenceShape: { - /** Best So Far Curve */ - best_so_far_curve: components['schemas']['CurvePoint'][]; - /** - * Direction - * @enum {string} - */ - direction: 'maximize' | 'minimize'; - /** Epsilon */ - epsilon: number; - /** Improvement In Window */ - improvement_in_window: number; - /** Total Complete Trials */ - total_complete_trials: number; - /** - * Verdict - * @enum {string} - */ - verdict: 'converged' | 'still_improving' | 'too_few_trials'; - /** Warmup Floor */ - warmup_floor: number; - /** Window Size */ - window_size: number; - }; - /** - * StudyDetail - * @description ``GET /api/v1/studies/{id}`` response + ``POST/cancel`` response. - */ - StudyDetail: { - /** Baseline Metric */ - baseline_metric: number | null; - /** Baseline Trial Id */ - baseline_trial_id: string | null; - /** Best Metric */ - best_metric: number | null; - /** Best Trial Id */ - best_trial_id: string | null; - /** Cluster Id */ - cluster_id: string; - /** Completed At */ - completed_at: string | null; - confidence?: components['schemas']['ConfidenceShape'] | null; - /** Config */ - config: { - [key: string]: unknown; - }; - convergence?: components['schemas']['StudyConvergenceShape'] | null; - /** - * Created At - * Format: date-time - */ - created_at: string; - /** Failed Reason */ - failed_reason: string | null; - /** Id */ - id: string; - /** Judgment List Id */ - judgment_list_id: string; - /** Name */ - name: string; - /** Objective */ - objective: { - [key: string]: unknown; - }; - /** Optuna Study Name */ - optuna_study_name: string; - /** Parent Study Id */ - parent_study_id: string | null; - /** Query Set Id */ - query_set_id: string; - /** Search Space */ - search_space: { - [key: string]: unknown; - }; - /** Started At */ - started_at: string | null; - /** - * Status - * @enum {string} - */ - status: 'queued' | 'running' | 'completed' | 'cancelled' | 'failed'; - /** Target */ - target: string; - /** Template Id */ - template_id: string; - trials_summary: components['schemas']['TrialsSummaryShape']; - }; - /** - * StudyListResponse - * @description ``GET /api/v1/studies`` response. - */ - StudyListResponse: { - /** Data */ - data: components['schemas']['StudySummary'][]; - /** Has More */ - has_more: boolean; - /** Next Cursor */ - next_cursor: string | null; - }; - /** - * StudySummary - * @description List-view shape. - */ - StudySummary: { - /** Best Metric */ - best_metric: number | null; - /** Cluster Id */ - cluster_id: string; - /** Completed At */ - completed_at: string | null; - /** Convergence Verdict */ - convergence_verdict?: ('converged' | 'still_improving' | 'too_few_trials') | null; - /** - * Created At - * Format: date-time - */ - created_at: string; - /** - * Direction - * @default maximize - * @enum {string} - */ - direction: 'maximize' | 'minimize'; - /** Id */ - id: string; - /** Name */ - name: string; - /** - * Status - * @enum {string} - */ - status: 'queued' | 'running' | 'completed' | 'cancelled' | 'failed'; - /** - * Trial Count - * @default 0 - */ - trial_count: number; - }; - /** - * Subsystems - * @description Per-subsystem reachability/configuration state. Wire values per spec §7.4. - */ - Subsystems: { - /** - * Db - * @description Postgres reachability - * @enum {string} - */ - db: 'ok' | 'down'; - /** - * Elasticsearch - * @description Local Elasticsearch container reachability - * @enum {string} - */ - elasticsearch: 'reachable' | 'unreachable'; - /** @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`. */ - elasticsearch_clusters: components['schemas']['ClusterAggregateHealth']; - /** - * 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 {string} - */ - openai: 'configured' | 'missing_key' | 'incapable'; - /** - * Opensearch - * @description Local OpenSearch container reachability - * @enum {string} - */ - opensearch: 'reachable' | 'unreachable'; - /** - * Redis - * @description Redis reachability - * @enum {string} - */ - redis: 'ok' | 'down'; - /** - * Solr - * @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. - * @default not_configured - * @enum {string} - */ - solr: 'reachable' | 'unreachable' | 'not_configured'; - }; - /** - * SwapTemplateFollowup - * @description A 'swap_template' followup — re-run against a different query template. - * - * Carries the LLM-proposed bounds for params shared with the parent template - * in ``search_space``. The digest worker calls - * :func:`backend.app.domain.study.template_swap.remap_search_space_for_swap_target` - * after parsing to merge these bounds with heuristic defaults for any - * swap-target params not shared with the parent. - * - * Owner: ``feat_digest_executable_followups_swap_template`` (Tier B). - */ - SwapTemplateFollowup: { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - kind: 'swap_template'; - /** Rationale */ - rationale: string; - search_space: components['schemas']['SearchSpace']; - /** Template Id */ - template_id: string; - }; - /** - * TargetInfo - * @description One target (index / collection) on a cluster. - */ - TargetInfo: { - /** Doc Count */ - doc_count?: number | null; - /** Name */ - name: string; - }; - /** - * TargetListResponse - * @description Response for ``GET /api/v1/clusters/{cluster_id}/targets`` (FR-1). - * - * Unpaginated by design — see feature_spec.md §7.1 "pagination shape - * rationale". The single-resource lookup pattern matches - * ``/clusters/{id}/schema`` rather than the queryable ``/clusters`` list. - * ``EntitySelectListPage``'s ``next_cursor`` and ``has_more`` fields - * are optional, so this bare ``data``-only shape consumes correctly on - * the frontend without pretending to be a cursor endpoint. - */ - TargetListResponse: { - /** Data */ - data: components['schemas']['TargetInfo'][]; - }; - /** - * TextFollowup - * @description A free-form textual suggestion — no auto-prefill, operator interprets. - */ - TextFollowup: { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - kind: 'text'; - /** Rationale */ - rationale: string; - /** Search Space */ - search_space?: null; - }; - /** - * TrialDetail - * @description ``GET /api/v1/studies/{id}/trials`` response row. - */ - TrialDetail: { - /** Duration Ms */ - duration_ms: number | null; - /** Ended At */ - ended_at: string | null; - /** Error */ - error: string | null; - /** Id */ - id: string; - /** - * Is Baseline - * @default false - */ - is_baseline: boolean; - /** Metrics */ - metrics: { - [key: string]: unknown; - }; - /** Optuna Trial Number */ - optuna_trial_number: number; - /** Params */ - params: { - [key: string]: unknown; - }; - /** Primary Metric */ - primary_metric: number | null; - /** Started At */ - started_at: string | null; - /** - * Status - * @enum {string} - */ - status: 'complete' | 'failed' | 'pruned'; - /** Study Id */ - study_id: string; - }; - /** - * TrialListResponse - * @description ``GET /api/v1/studies/{id}/trials`` response. - */ - TrialListResponse: { - /** Data */ - data: components['schemas']['TrialDetail'][]; - /** Has More */ - has_more: boolean; - /** Next Cursor */ - next_cursor: string | null; - }; - /** - * TrialsSummaryShape - * @description The ``trials_summary`` field embedded in :class:`StudyDetail`. - */ - TrialsSummaryShape: { - /** Best Primary Metric */ - best_primary_metric: number | null; - /** Complete */ - complete: number; - /** Failed */ - failed: number; - /** Pruned */ - pruned: number; - /** Total */ - total: number; - }; - /** - * UbiReadinessResponse - * @description ``GET /api/v1/clusters/{cluster_id}/ubi-readiness`` response (FR-7). - * - * ``covered_pairs_pct`` and ``head_covered`` are nullable — MVP2's - * rung classifier uses event-count thresholds (the SearchAdapter - * Protocol doesn't expose an exact ``_count`` endpoint). The fields - * are reserved on the wire so a future ``infra_adapter_count_method`` - * can fill them without breaking the contract. See - * :mod:`backend.app.services.ubi_readiness` for the rationale. - */ - UbiReadinessResponse: { - /** - * Checked At - * Format: date-time - */ - checked_at: string; - /** Covered Pairs Pct */ - covered_pairs_pct: number | null; - /** Head Covered */ - head_covered: boolean | null; - /** - * Rung - * @enum {string} - */ - rung: 'rung_0' | 'rung_1' | 'rung_2' | 'rung_3'; - }; - /** - * UpdateQueryRequest - * @description ``PATCH /api/v1/query-sets/{set_id}/queries/{query_id}`` body. - * - * Whole-object replace on ``query_metadata`` (NOT deep-merge); explicit - * ``null`` removes a nullable field; omitted key = no change. Empty - * body ``{}`` validates as a no-op (AC-28). - * - * ``query_text`` is NOT NULL on the underlying table, so explicit-null - * is rejected by the ``@model_validator`` below (a 422 surfaces sooner - * than the SQL ``NotNullViolation``). - */ - UpdateQueryRequest: { - /** Query Metadata */ - query_metadata?: { - [key: string]: unknown; - } | null; - /** Query Text */ - query_text?: string | null; - /** Reference Answer */ - reference_answer?: string | null; - }; - /** ValidationError */ - ValidationError: { - /** Context */ - ctx?: Record; - /** Input */ - input?: unknown; - /** Location */ - loc: (string | number)[]; - /** Message */ - msg: string; - /** Error Type */ - type: string; - }; - /** - * WidenFollowup - * @description A 'widen' followup — re-run with a broader range than the parent. - */ - WidenFollowup: { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - kind: 'widen'; - /** Rationale */ - rationale: string; - search_space: components['schemas']['SearchSpace']; - }; - /** - * _ClusterEmbed - * @description Inline cluster summary on proposal responses. - */ - _ClusterEmbed: { - /** Engine Type */ - engine_type: string; - /** Environment */ - environment?: string | null; - /** Id */ - id: string; - /** Name */ - name: string; - }; - /** - * _DigestEmbed - * @description Inline digest summary on the proposal-detail response. - * - * feat_digest_executable_followups Story 4.1 — ``suggested_followups`` is - * now a discriminated-union list (see ``DigestResponse``). - */ - _DigestEmbed: { - /** - * Generated At - * Format: date-time - */ - generated_at: string; - /** Id */ - id: string; - /** Narrative */ - narrative: string; - /** Parameter Importance */ - parameter_importance: { - [key: string]: number; - }; - /** Recommended Config */ - recommended_config: { - [key: string]: unknown; - }; - /** Suggested Followups */ - suggested_followups: components['schemas']['FollowupItem'][]; - }; - /** - * _SourceBreakdown - * @description Source-breakdown sub-shape on :class:`JudgmentListDetail`. - * - * Evolved 2026-05-29 by ``feat_ubi_judgments`` FR-10 — now three terms - * (``llm + human + click == judgment_count``). The cycle-2 F6 - * "click folds into human" contract is superseded the moment UBI ships - * click rows; the UI's source-breakdown card now renders all three - * buckets separately so operators see the mix at a glance. - */ - _SourceBreakdown: { - /** Click */ - click: number; - /** Human */ - human: number; - /** Llm */ - llm: number; - }; - /** - * _StudySummary - * @description Inline study summary on the proposal-detail response. - */ - _StudySummary: { - /** Best Metric */ - best_metric: number | null; - /** Best Trial Id */ - best_trial_id: string | null; - /** Id */ - id: string; - /** Judgment List */ - judgment_list: { - [key: string]: unknown; - }; - /** Name */ - name: string; - /** Query Set */ - query_set: { - [key: string]: unknown; - }; - /** Status */ - status: string; - }; - /** - * _TemplateEmbed - * @description Inline template summary on proposal responses. - */ - _TemplateEmbed: { - /** Engine Type */ - engine_type?: string | null; - /** Id */ - id: string; - /** Name */ - name: string; - /** Version */ - version: number; - }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; + schemas: { + /** + * BulkQueriesResponse + * @description ``POST /api/v1/query-sets/{id}/queries`` response. + */ + BulkQueriesResponse: { + /** Added */ + added: number; + }; + /** + * CIShape + * @description Bootstrap percentile CI on the winner's per-query metric values. + */ + CIShape: { + /** High */ + high: number; + /** Low */ + low: number; + /** + * Method + * @constant + */ + method: "bootstrap_n1000"; + /** N Samples */ + n_samples: number; + }; + /** + * CalibrationResponse + * @description Calibration endpoint response. + * + * Mirrors :class:`backend.app.eval.calibration.CalibrationResult` — + * persisted as ``judgment_lists.calibration`` JSONB. + */ + CalibrationResponse: { + /** Cohens Kappa */ + cohens_kappa: number | null; + /** N Samples */ + n_samples: number; + /** Per Class */ + per_class: { + [key: string]: number; + }; + /** Warning */ + warning: string | null; + /** Weighted Kappa */ + weighted_kappa: number | null; + }; + /** + * CalibrationSample + * @description One row in :class:`CalibrationSamplesRequest`. + */ + CalibrationSample: { + /** Doc Id */ + doc_id: string; + /** Query Id */ + query_id: string; + /** + * Rating + * @enum {integer} + */ + rating: 0 | 1 | 2 | 3; + }; + /** + * CalibrationSamplesRequest + * @description Body for ``POST /api/v1/judgment-lists/{id}/calibration`` (Story 3.5). + */ + CalibrationSamplesRequest: { + /** Human Samples */ + human_samples: components["schemas"]["CalibrationSample"][]; + }; + /** + * CategoricalParam + * @description Discrete choice parameter. + * + * Optuna ``suggest_categorical`` handles strings, ints, floats, and bools + * as choices. + */ + CategoricalParam: { + /** Choices */ + choices: (string | number | boolean)[]; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "categorical"; + }; + /** + * ClusterAggregateHealth + * @description Aggregate counts for the ``elasticsearch_clusters`` /healthz field (Story 3.5). + * + * Per spec §2: probes only the *registered* user clusters (from the DB), + * NOT the local Compose ES/OpenSearch — those have their own subsystem + * fields. ``status`` is a count derived from the cached ``cluster:health:*`` + * entries; missing-cache or red/unreachable clusters are counted as + * ``unreachable``. + */ + ClusterAggregateHealth: { + /** Healthy */ + healthy: number; + /** Registered */ + registered: number; + /** Unreachable */ + unreachable: number; + }; + /** + * ClusterDetail + * @description ``GET /api/v1/clusters/{id}`` response. + */ + ClusterDetail: { + /** + * Auth Kind + * @enum {string} + */ + auth_kind: "es_apikey" | "es_basic" | "opensearch_basic" | "opensearch_sigv4" | "solr_basic" | "solr_apikey"; + /** Base Url */ + base_url: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Engine Config */ + engine_config?: { + [key: string]: unknown; + } | null; + /** + * Engine Type + * @enum {string} + */ + engine_type: "elasticsearch" | "opensearch" | "solr"; + /** + * Environment + * @enum {string} + */ + environment: "prod" | "staging" | "dev"; + health_check: components["schemas"]["HealthCheckResult"]; + /** Id */ + id: string; + /** Name */ + name: string; + /** Notes */ + notes?: string | null; + /** Target Filter */ + target_filter?: string | null; + }; + /** + * ClusterListResponse + * @description Paginated list response. + */ + ClusterListResponse: { + /** Data */ + data: components["schemas"]["ClusterSummary"][]; + /** Has More */ + has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; + }; + /** + * ClusterSummary + * @description List-view; drops engine_config + notes for brevity. + */ + ClusterSummary: { + /** + * Auth Kind + * @enum {string} + */ + auth_kind: "es_apikey" | "es_basic" | "opensearch_basic" | "opensearch_sigv4" | "solr_basic" | "solr_apikey"; + /** Base Url */ + base_url: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** + * Engine Type + * @enum {string} + */ + engine_type: "elasticsearch" | "opensearch" | "solr"; + /** + * Environment + * @enum {string} + */ + environment: "prod" | "staging" | "dev"; + health_check: components["schemas"]["HealthCheckResult"]; + /** Id */ + id: string; + /** Name */ + name: string; + /** Target Filter */ + target_filter?: string | null; + }; + /** + * ConfidenceShape + * @description The top-level shape exposed via ``StudyDetail.confidence``. + * + * Every sub-field is independently nullable per FR-7 — degraded paths + * suppress only the sub-fields they affect, never the whole shape (the + * orchestrator returns whole-object ``None`` only when the winner trial + * row itself is missing). + */ + ConfidenceShape: { + ci_95: components["schemas"]["CIShape"] | null; + convergence: components["schemas"]["ConvergenceShape"] | null; + headline: components["schemas"]["HeadlineShape"]; + late_trial_stddev: components["schemas"]["LateTrialStddevShape"] | null; + per_query_outcomes: components["schemas"]["PerQueryOutcomesShape"] | null; + runner_up_gap: components["schemas"]["RunnerUpGapShape"] | null; + }; + /** + * ConfigRepoDetail + * @description ``GET /api/v1/config-repos/{id}`` response + ``POST`` 201 body. + */ + ConfigRepoDetail: { + /** Auth Ref */ + auth_ref: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Default Branch */ + default_branch: string; + /** Id */ + id: string; + last_merged_proposal?: components["schemas"]["ProposalSummary"] | null; + /** Name */ + name: string; + /** Pr Base Branch */ + pr_base_branch: string; + /** + * Provider + * @constant + */ + provider: "github"; + /** Repo Url */ + repo_url: string; + /** Webhook Registration Error */ + webhook_registration_error: string | null; + /** Webhook Secret Ref */ + webhook_secret_ref: string | null; + }; + /** + * ConfigReposListResponse + * @description ``GET /api/v1/config-repos`` response. + */ + ConfigReposListResponse: { + /** Data */ + data: components["schemas"]["ConfigRepoDetail"][]; + /** Has More */ + has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; + }; + /** + * ConnectionTestRequest + * @description Body for ``POST /api/v1/clusters/test-connection`` (infra_adapter_solr Story A9). + * + * Same shape as ``CreateClusterRequest`` minus the persisted-only fields + * (``name``, ``environment``, ``notes``, ``target_filter``). ``engine_type`` + * + ``auth_kind`` are typed as ``str`` (not Literal) so a bad value yields + * the project-standard 400 envelope rather than a raw 422 — same convention + * as ``CreateClusterRequest``. + */ + ConnectionTestRequest: { + /** Auth Kind */ + auth_kind: string; + /** Base Url */ + base_url: string; + /** Credentials Ref */ + credentials_ref: string; + /** Engine Config */ + engine_config?: { + [key: string]: unknown; + } | null; + /** Engine Type */ + engine_type: string; + }; + /** + * ConnectionTestResult + * @description Response for ``POST /api/v1/clusters/test-connection``. + * + * Always 200 — reachable vs unreachable surfaces via ``reachable`` + + * ``status`` fields. The endpoint is a diagnostic, never a mutation, + * so it never returns 503; invalid engine×auth pairings 400 BEFORE the + * network call. (Cycle-delta F1.) + */ + ConnectionTestResult: { + /** Engine Capabilities */ + engine_capabilities?: { + [key: string]: unknown; + } | null; + /** Error */ + error?: string | null; + /** Reachable */ + reachable: boolean; + /** + * Status + * @enum {string} + */ + status: "green" | "yellow" | "red" | "unreachable"; + /** Version */ + version?: string | null; + }; + /** + * ConvergenceShape + * @description Where the winner sits in the Optuna trial sequence + the classified regime. + */ + ConvergenceShape: { + /** Best At Trial */ + best_at_trial: number; + /** + * Regime + * @enum {string} + */ + regime: "early_held" | "late_rising" | "noisy"; + /** Total Trials */ + total_trials: number; + }; + /** + * ConversationDetail + * @description ``GET /api/v1/conversations/{id}`` response. + */ + ConversationDetail: { + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Id */ + id: string; + /** Messages */ + messages: components["schemas"]["MessageWire"][]; + /** Title */ + title: string | null; + }; + /** + * ConversationSummary + * @description ``GET /api/v1/conversations`` row + ``POST`` 201 body. + * + * ``last_message_preview`` is the most recent user / assistant message's + * ``content.text``, truncated at the repo layer to 120 chars (with ``…`` + * suffix when cut). Tool-role rows and assistant rows whose ``content.kind`` + * is ``system_notice`` are skipped. ``None`` for brand-new conversations + * with no qualifying messages — see ``chore_chat_last_message_preview``. + * + * ``last_message_at`` is the ``created_at`` of that same row, or ``None`` + * for empty conversations. The list page uses it to render "when did + * anyone last touch this thread" instead of the conversation's + * ``created_at``. + */ + ConversationSummary: { + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Id */ + id: string; + /** Last Message At */ + last_message_at?: string | null; + /** Last Message Preview */ + last_message_preview?: string | null; + /** Message Count */ + message_count: number; + /** Title */ + title: string | null; + }; + /** + * ConversationsListResponse + * @description ``GET /api/v1/conversations`` response. + */ + ConversationsListResponse: { + /** Data */ + data: components["schemas"]["ConversationSummary"][]; + /** Has More */ + has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; + }; + /** + * CreateClusterRequest + * @description Request body for ``POST /api/v1/clusters``. + * + * See module docstring for the deliberate ``str`` vs ``Literal`` split. + */ + CreateClusterRequest: { + /** Auth Kind */ + auth_kind: string; + /** Base Url */ + base_url: string; + /** Credentials Ref */ + credentials_ref: string; + /** Engine Config */ + engine_config?: { + [key: string]: unknown; + } | null; + /** Engine Type */ + engine_type: string; + /** + * Environment + * @enum {string} + */ + environment: "prod" | "staging" | "dev"; + /** Name */ + name: string; + /** Notes */ + notes?: string | null; + /** + * Target Filter + * @description Optional glob pattern (fnmatch.fnmatchcase: *, ?, [seq], [!seq]; no brace expansion). Scopes GET /clusters/{id}/targets to matching index names. Null = no filter. + */ + target_filter?: string | null; + }; + /** + * CreateConfigRepoRequest + * @description Body of ``POST /api/v1/config-repos`` (FR-3). + * + * ``provider`` is server-derived from ``repo_url`` (cycle-2 F4 from + * spec review) — NOT in the payload. The validator enforces a strict + * GitHub URL pattern; non-GitHub URLs surface as 400 + * ``UNSUPPORTED_PROVIDER`` at the router layer. + */ + CreateConfigRepoRequest: { + /** Auth Ref */ + auth_ref: string; + /** + * Default Branch + * @default main + */ + default_branch: string; + /** Name */ + name: string; + /** + * Pr Base Branch + * @default main + */ + pr_base_branch: string; + /** Repo Url */ + repo_url: string; + /** Webhook Secret Ref */ + webhook_secret_ref?: string | null; + }; + /** + * CreateConversationRequest + * @description ``POST /api/v1/conversations`` body. + */ + CreateConversationRequest: { + /** Title */ + title?: string | null; + }; + /** + * CreateJudgmentListFromUbiRequest + * @description Body for ``POST /api/v1/judgments/generate-from-ubi`` (Story 3.2 / FR-3). + * + * Mirrors :class:`backend.app.services.agent_judgments_dispatch.UbiJudgmentGenerationRequest`. + * The ``@model_validator(mode="after")`` enforces the conditional + * requiredness of ``current_template_id`` + ``rubric`` per the hybrid + * converter: REQUIRED when ``converter == 'hybrid_ubi_llm'`` (the LLM- + * fill path needs both); FORBIDDEN otherwise (pure UBI never calls + * the LLM so accepting them silently would mask operator error). + */ + CreateJudgmentListFromUbiRequest: { + /** Cluster Id */ + cluster_id: string; + /** + * Converter + * @enum {string} + */ + converter: "ctr_threshold" | "dwell_time" | "hybrid_ubi_llm"; + /** Converter Config */ + converter_config?: { + [key: string]: unknown; + } | null; + /** Current Template Id */ + current_template_id?: string | null; + /** Description */ + description?: string | null; + /** + * Llm Fill Threshold + * @default 20 + */ + llm_fill_threshold: number | null; + /** + * Mapping Strategy + * @default reject + * @enum {string} + */ + mapping_strategy: "reject" | "first_match" | "most_recent"; + /** + * Min Impressions Threshold + * @default 100 + */ + min_impressions_threshold: number | null; + /** Name */ + name: string; + /** Query Set Id */ + query_set_id: string; + /** Rubric */ + rubric?: string | null; + /** + * Since + * Format: date-time + */ + since: string; + /** Target */ + target: string; + /** Until */ + until?: string | null; + }; + /** + * CreateJudgmentListGenerateRequest + * @description Body for ``POST /api/v1/judgments/generate`` (Story 3.1). + */ + CreateJudgmentListGenerateRequest: { + /** Cluster Id */ + cluster_id: string; + /** Current Template Id */ + current_template_id: string; + /** Description */ + description?: string | null; + /** Name */ + name: string; + /** Query Set Id */ + query_set_id: string; + /** Rubric */ + rubric: string; + /** Target */ + target: string; + }; + /** + * CreateProposalRequest + * @description Body of ``POST /api/v1/proposals`` (manual proposal creation, FR-4 / AC-6). + */ + CreateProposalRequest: { + /** Cluster Id */ + cluster_id: string; + /** Config Diff */ + config_diff: { + [key: string]: unknown; + }; + /** Metric Delta */ + metric_delta?: { + [key: string]: unknown; + } | null; + /** Template Id */ + template_id: string; + }; + /** + * CreateQuerySetRequest + * @description ``POST /api/v1/query-sets`` body. + * + * ``cluster_id`` is required because Phase 1's shipped schema has + * ``query_sets.cluster_id NOT NULL``. Spec FR-3 wording (``cluster_id?``) + * is documented drift tracked at + * ``docs/00_overview/planned_features/chore_spec_query_set_cluster_id_drift/idea.md``. + */ + CreateQuerySetRequest: { + /** Cluster Id */ + cluster_id: string; + /** Description */ + description?: string | null; + /** Name */ + name: string; + }; + /** + * CreateQueryTemplateRequest + * @description Request body for ``POST /api/v1/query-templates``. + */ + CreateQueryTemplateRequest: { + /** Body */ + body: string; + /** Declared Params */ + declared_params?: { + [key: string]: string; + }; + /** + * Engine Type + * @enum {string} + */ + engine_type: "elasticsearch" | "opensearch" | "solr"; + /** Name */ + name: string; + /** Parent Id */ + parent_id?: string | null; + }; + /** + * CreateStudyRequest + * @description ``POST /api/v1/studies`` body. + * + * ``search_space`` is validated post-Pydantic-parse via + * :class:`backend.app.domain.study.search_space.SearchSpace` so + * :exc:`pydantic.ValidationError` produces the spec's 400 + * ``INVALID_SEARCH_SPACE`` (per Story 3.3 task 2). + * + * feat_digest_executable_followups Story 4.2 — optional ``parent`` field + * records the parent proposal + followup-index lineage when the study + * was spawned from a digest "Run this followup" action (FR-11). + */ + CreateStudyRequest: { + /** Cluster Id */ + cluster_id: string; + config: components["schemas"]["StudyConfigSpec"]; + /** Judgment List Id */ + judgment_list_id: string; + /** Name */ + name: string; + objective: components["schemas"]["ObjectiveSpec"]; + parent?: components["schemas"]["ParentFollowupRef"] | null; + /** + * Parent Study Id + * @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. + */ + parent_study_id?: string | null; + /** Query Set Id */ + query_set_id: string; + /** Search Space */ + search_space: { + [key: string]: unknown; + }; + /** Target */ + target: string; + /** Template Id */ + template_id: string; + }; + /** + * CurvePoint + * @description One point on the best-so-far curve. + * + * ``trial_number`` is the trial's ``optuna_trial_number`` (the canonical + * "trial order within the study" field — see ``auto_followup.py`` module + * docstring for why we sort by this rather than ``started_at``). + * ``best_so_far`` is the running extremum of ``primary_metric`` over all + * earlier trials, sign-corrected to the study's optimization direction. + */ + CurvePoint: { + /** Best So Far */ + best_so_far: number; + /** Trial Number */ + trial_number: number; + }; + /** + * DigestResponse + * @description Body of ``GET /api/v1/studies/{id}/digest`` (FR-3 / AC-3). + * + * feat_digest_executable_followups Story 4.1 — ``suggested_followups`` is + * now a discriminated-union list (NarrowFollowup | WidenFollowup | + * TextFollowup), populated by the digest handler via + * ``parse_followup_list(digest.suggested_followups, ...)`` so legacy or + * malformed JSONB payloads never crash the response. + */ + DigestResponse: { + /** + * Generated At + * Format: date-time + */ + generated_at: string; + /** Generated By */ + generated_by: string; + /** Id */ + id: string; + /** Narrative */ + narrative: string; + /** Parameter Importance */ + parameter_importance: { + [key: string]: number; + }; + /** Recommended Config */ + recommended_config: { + [key: string]: unknown; + }; + /** Study Id */ + study_id: string; + /** Suggested Followups */ + suggested_followups: components["schemas"]["FollowupItem"][]; + }; + /** + * Document + * @description A single document by ID — return shape of ``SearchAdapter.get_document``. + * + * Mirrors :class:`ScoredHit` minus ``score`` (browsing doesn't need scoring). + * ``source`` is ``None`` when the engine's index has ``_source: false`` mapping. + */ + Document: { + /** Doc Id */ + doc_id: string; + /** Source */ + source?: { + [key: string]: unknown; + } | null; + }; + /** + * DocumentListResponse + * @description ``GET /api/v1/clusters/{cluster_id}/targets/{target}/documents`` response. + * + * ``next_cursor`` opaque-encodes the ES ``hits[-1].sort`` array of the + * last visible row when ``has_more`` is True (see + * ``backend.app.api.v1._documents_cursor``). The ``X-Total-Count`` header + * on the response carries the engine's ``hits.total.value``. + */ + DocumentListResponse: { + /** Data */ + data: components["schemas"]["DocumentSummary"][]; + /** Has More */ + has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; + }; + /** + * DocumentSummary + * @description One row in the documents list (per FR-3 / FR-8). + * + * ``source`` is the *truncated* preview emitted by + * ``backend.app.services.documents.truncate_source_for_list``. The detail + * endpoint returns the untruncated ``Document.source``. + */ + DocumentSummary: { + /** Doc Id */ + doc_id: string; + /** Source */ + source: { + [key: string]: unknown; + } | null; + }; + /** + * FieldSpec + * @description One field returned by ``get_schema``. + */ + FieldSpec: { + /** Analyzer */ + analyzer?: string | null; + /** Doc Count */ + doc_count?: number | null; + /** Name */ + name: string; + /** Type */ + type: string; + }; + /** + * FloatParam + * @description Continuous float parameter. + * + * ``log=True`` enables log-uniform sampling + * (Optuna's ``suggest_float(..., log=True)``); requires ``low > 0``. + */ + FloatParam: { + /** High */ + high: number; + /** + * Log + * @default false + */ + log: boolean; + /** Low */ + low: number; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "float"; + }; + FollowupItem: components["schemas"]["NarrowFollowup"] | components["schemas"]["WidenFollowup"] | components["schemas"]["TextFollowup"] | components["schemas"]["SwapTemplateFollowup"]; + /** + * GenerateJudgmentsResponse + * @description Response of ``POST /api/v1/judgments/generate``. + * + * Per GPT-5.5 cycle 1 F5 — the endpoint registers a typed + * ``response_model`` so OpenAPI introspection + contract tests can verify + * the wire shape. + */ + GenerateJudgmentsResponse: { + /** Judgment List Id */ + judgment_list_id: string; + /** + * Status + * @constant + */ + status: "generating"; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components["schemas"]["ValidationError"][]; + }; + /** + * HeadlineShape + * @description Top-line metric value + N(queries) used in the CI. + * + * ``metric`` uses ``str`` (not ``ObjectiveMetric``) to avoid a circular + * import: ``schemas.py`` imports ``ConfidenceShape`` from here, so this + * module cannot import back from ``schemas.py``. The upstream value is + * already validated by the existing ``ObjectiveMetric`` Literal at the + * create-study endpoint (``schemas.py:214``). + */ + HeadlineShape: { + /** K */ + k: number | null; + /** Metric */ + metric: string; + /** N Queries */ + n_queries: number | null; + /** Value */ + value: number; + }; + /** + * HealthCheckResult + * @description Wire shape of the per-cluster health probe (mirrors ``HealthStatus``). + */ + HealthCheckResult: { + /** Checked At */ + checked_at: string; + /** Error */ + error?: string | null; + /** + * Status + * @enum {string} + */ + status: "green" | "yellow" | "red" | "unreachable"; + /** Version */ + version?: string | null; + }; + /** + * HealthResponse + * @description The /healthz response body. Same shape for HTTP 200 and 503. + */ + HealthResponse: { + openai_capabilities: components["schemas"]["OpenAICapabilities"]; + /** + * Openai Endpoint + * @description Configured OPENAI_BASE_URL + */ + openai_endpoint: string; + /** + * Status + * @enum {string} + */ + status: "ok" | "degraded"; + subsystems: components["schemas"]["Subsystems"]; + /** + * Uptime Seconds + * @description Seconds since the API process started + */ + uptime_seconds: number; + /** + * Version + * @description Application version (relyloop_git_sha) + */ + version: string; + }; + /** + * ImportJudgmentItem + * @description One row in :class:`ImportJudgmentListRequest`. + */ + ImportJudgmentItem: { + /** Doc Id */ + doc_id: string; + /** Notes */ + notes?: string | null; + /** Query Id */ + query_id: string; + /** + * Rating + * @enum {integer} + */ + rating: 0 | 1 | 2 | 3; + }; + /** + * ImportJudgmentListRequest + * @description Body for ``POST /api/v1/judgment-lists/import`` (Story 3.2). + */ + ImportJudgmentListRequest: { + /** Cluster Id */ + cluster_id: string; + /** Description */ + description?: string | null; + /** Judgments */ + judgments: components["schemas"]["ImportJudgmentItem"][]; + /** Name */ + name: string; + /** Query Set Id */ + query_set_id: string; + /** Rubric */ + rubric: string; + /** Target */ + target: string; + }; + /** + * IntParam + * @description Integer parameter inclusive of both bounds. + */ + IntParam: { + /** High */ + high: number; + /** Low */ + low: number; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "int"; + }; + /** + * JudgmentListDetail + * @description ``GET /api/v1/judgment-lists/{id}`` response. + * + * Note: ``generation_params`` is populated for UBI lists (feat_ubi_judgments + * Story 1.1's JSONB column) and NULL for LLM lists. The Story 4.3 UI + * (```` + ````) reads the + * payload to discriminate UBI/hybrid lists and to reconstruct the + * original request for the ambiguous-skip "Re-run with most_recent" + * affordance. + */ + JudgmentListDetail: { + /** Calibration */ + calibration: { + [key: string]: unknown; + } | null; + /** Cluster Id */ + cluster_id: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Current Template Id */ + current_template_id: string | null; + /** Description */ + description: string | null; + /** Failed Reason */ + failed_reason: string | null; + /** Generation Params */ + generation_params: { + [key: string]: unknown; + } | null; + /** Id */ + id: string; + /** Judgment Count */ + judgment_count: number; + /** Name */ + name: string; + /** Query Set Id */ + query_set_id: string; + /** Rubric */ + rubric: string; + source_breakdown: components["schemas"]["_SourceBreakdown"]; + /** + * Status + * @enum {string} + */ + status: "generating" | "complete" | "failed"; + /** Target */ + target: string; + }; + /** + * JudgmentListJudgmentsResponse + * @description ``GET /api/v1/judgment-lists/{id}/judgments`` response. + */ + JudgmentListJudgmentsResponse: { + /** Data */ + data: components["schemas"]["JudgmentRow"][]; + /** Has More */ + has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; + }; + /** + * JudgmentListListResponse + * @description ``GET /api/v1/judgment-lists`` response. + */ + JudgmentListListResponse: { + /** Data */ + data: components["schemas"]["JudgmentListSummary"][]; + /** Has More */ + has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; + }; + /** + * JudgmentListRef + * @description One entry in the ``QUERY_HAS_JUDGMENTS`` 409 envelope. + * + * Lives in ``detail.judgment_lists``. Maps from the repo-layer + * :class:`backend.app.db.repo.judgment.JudgmentListRefRow` at the + * router boundary. + */ + JudgmentListRef: { + /** Id */ + id: string; + /** Name */ + name: string; + }; + /** + * JudgmentListSummary + * @description List-view row on ``GET /api/v1/judgment-lists``. + */ + JudgmentListSummary: { + /** Cluster Id */ + cluster_id: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Description */ + description: string | null; + /** Id */ + id: string; + /** Name */ + name: string; + /** Query Set Id */ + query_set_id: string; + /** + * Status + * @enum {string} + */ + status: "generating" | "complete" | "failed"; + /** Target */ + target: string; + }; + /** + * JudgmentRow + * @description ``GET /api/v1/judgment-lists/{id}/judgments`` row + PATCH response. + */ + JudgmentRow: { + /** Confidence */ + confidence: number | null; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Doc Id */ + doc_id: string; + /** Id */ + id: string; + /** Judgment List Id */ + judgment_list_id: string; + /** Notes */ + notes: string | null; + /** Query Id */ + query_id: string; + /** Rater Ref */ + rater_ref: string | null; + /** + * Rating + * @enum {integer} + */ + rating: 0 | 1 | 2 | 3; + /** + * Source + * @enum {string} + */ + source: "llm" | "human" | "click"; + }; + /** + * LateTrialStddevShape + * @description Sample stddev of ``primary_metric`` over the late-trial window. + */ + LateTrialStddevShape: { + /** Min Window Required */ + min_window_required: number; + /** Value */ + value: number; + /** Window Size */ + window_size: number; + }; + /** + * MessageWire + * @description One row of ``GET /api/v1/conversations/{id}.messages``. + */ + MessageWire: { + /** Content */ + content: { + [key: string]: unknown; + }; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Id */ + id: string; + /** + * Role + * @enum {string} + */ + role: "user" | "assistant" | "tool"; + /** Tool Calls */ + tool_calls?: { + [key: string]: unknown; + }[] | null; + }; + /** + * NarrowFollowup + * @description A 'narrow' followup — re-run with a tighter range than the parent. + */ + NarrowFollowup: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + kind: "narrow"; + /** Rationale */ + rationale: string; + search_space: components["schemas"]["SearchSpace"]; + }; + /** + * ObjectiveSpec + * @description Wire shape of ``studies.objective`` (write-side validated at create). + * + * ``k`` is required for ``ndcg`` / ``precision`` / ``recall`` (per + * standard IR-evaluation conventions: those metrics are computed at a + * cutoff rank). ``map`` accepts ``k`` optionally; ``mrr`` / ``err`` ignore + * it. The model_validator enforces this so a malformed objective + * surfaces as 400 ``INVALID_SEARCH_SPACE`` / 422 ``VALIDATION_ERROR`` + * at study-create time rather than failing later inside ``run_trial`` + * when the worker computes the metric. + */ + ObjectiveSpec: { + /** + * Direction + * @default maximize + * @enum {string} + */ + direction: "maximize" | "minimize"; + /** K */ + k?: (1 | 3 | 5 | 10 | 20 | 50 | 100) | null; + /** + * Metric + * @enum {string} + */ + metric: "ndcg" | "map" | "precision" | "recall" | "mrr"; + }; + /** + * OpenAICapabilities + * @description Cached results of the OpenAI capability check (Story 3.3 populates Redis). + * + * Step 1 (``models_endpoint``) is reported first because it gates the rest: + * when it fails, the other three are reported as ``"untested"``. The + * ``models_endpoint_status_code`` field is required-but-nullable + * (per ``bug_openai_capability_check_incapable_on_valid_key`` spec §19 D-3/D-8) + * — always present in the JSON, ``null`` when not applicable. This lets + * operators distinguish ``401 -> bad key``, ``429 -> quota``, + * ``5xx -> upstream outage``, ``null -> network unreachable / cache miss``. + */ + OpenAICapabilities: { + /** + * Chat + * @description Chat completion probe result + * @enum {string} + */ + chat: "ok" | "fail" | "untested"; + /** + * Function Calling + * @description Function-calling probe result (tool_choice=required) + * @enum {string} + */ + function_calling: "ok" | "fail" | "untested"; + /** + * 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 {string} + */ + models_endpoint: "ok" | "fail" | "untested"; + /** + * Models Endpoint Status Code + * @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. + */ + models_endpoint_status_code: number | null; + /** + * Structured Output + * @description JSON-schema response_format probe result + * @enum {string} + */ + structured_output: "ok" | "fail" | "untested"; + }; + /** + * OpenPrResponse + * @description Body of ``POST /api/v1/proposals/{id}/open_pr`` (FR-1). + * + * Returned with HTTP 202 on successful enqueue. Status is always + * ``'pending'`` at enqueue time; the worker flips it to ``'pr_opened'`` + * after the PR is open. + */ + OpenPrResponse: { + /** Message */ + message: string; + /** Proposal Id */ + proposal_id: string; + /** + * Status + * @constant + */ + status: "pending"; + }; + /** + * OverrideJudgmentRequest + * @description Body for ``PATCH /api/v1/judgment-lists/{id}/judgments/{judgment_id}``. + * + * ``rating`` is INTENTIONALLY unbounded at the Pydantic layer — spec §8.5 + * requires out-of-range failures to surface as 400 ``INVALID_RATING`` (not + * Pydantic's default 422 ``VALIDATION_ERROR``). The handler validates the + * value manually and raises the domain code (per GPT-5.5 cycle 1 F4). + */ + OverrideJudgmentRequest: { + /** Notes */ + notes?: string | null; + /** Rating */ + rating: number; + }; + /** + * ParentFollowupRef + * @description Optional lineage payload on ``POST /api/v1/studies``. + * + * feat_digest_executable_followups FR-11 — when the operator clicks + * "Run this followup" on a proposal's digest card, the create-study + * payload carries the parent proposal's id + the 0-based index into + * the digest's ``suggested_followups`` array so the spawned study + * remembers where it came from. + * + * ``proposal_id`` is a UUIDv7 (36-char hex). The exact-length bound + * forces malformed strings to surface as 422 ``VALIDATION_ERROR`` + * rather than reach the DB FK check and emerge as a 404 + * ``PROPOSAL_NOT_FOUND``. + */ + ParentFollowupRef: { + /** Followup Index */ + followup_index: number; + /** Proposal Id */ + proposal_id: string; + }; + /** + * PerQueryOutcomesShape + * @description Per-query outcome counts + the top-5 named regressors and improvers. + */ + PerQueryOutcomesShape: { + /** + * Comparison Against + * @enum {string} + */ + comparison_against: "runner_up" | "baseline"; + /** Improved */ + improved: number; + /** Regressed */ + regressed: number; + /** + * Top Improvers + * @default [] + */ + top_improvers: components["schemas"]["RegressorRowShape"][]; + /** Top Regressors */ + top_regressors: components["schemas"]["RegressorRowShape"][]; + /** Unchanged */ + unchanged: number; + }; + /** + * ProposalDetail + * @description Body of the proposal detail endpoints. + * + * Used by ``GET /api/v1/proposals/{id}``, ``POST /api/v1/proposals``, + * and ``POST /api/v1/proposals/{id}/reject``. + */ + ProposalDetail: { + cluster: components["schemas"]["_ClusterEmbed"]; + /** Config Diff */ + config_diff: { + [key: string]: unknown; + }; + /** + * Created At + * Format: date-time + */ + created_at: string; + digest: components["schemas"]["_DigestEmbed"] | null; + /** Id */ + id: string; + /** + * Is Currently Live + * @default false + */ + is_currently_live: boolean; + /** Metric Delta */ + metric_delta: { + [key: string]: unknown; + } | null; + /** Pr Merged At */ + pr_merged_at: string | null; + /** Pr Open Error */ + pr_open_error: string | null; + /** Pr State */ + pr_state: ("open" | "closed" | "merged") | null; + /** Pr Url */ + pr_url: string | null; + /** Rejected Reason */ + rejected_reason: string | null; + /** + * Status + * @enum {string} + */ + status: "pending" | "pr_opened" | "pr_merged" | "rejected"; + /** Study Id */ + study_id: string | null; + study_summary: components["schemas"]["_StudySummary"] | null; + /** Study Trial Id */ + study_trial_id: string | null; + template: components["schemas"]["_TemplateEmbed"]; + }; + /** + * ProposalSummary + * @description Row in the ``GET /api/v1/proposals`` list response. + */ + ProposalSummary: { + cluster: components["schemas"]["_ClusterEmbed"]; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Id */ + id: string; + /** + * Is Currently Live + * @default false + */ + is_currently_live: boolean; + /** Metric Delta */ + metric_delta: { + [key: string]: unknown; + } | null; + /** Pr State */ + pr_state: ("open" | "closed" | "merged") | null; + /** Pr Url */ + pr_url: string | null; + /** + * Status + * @enum {string} + */ + status: "pending" | "pr_opened" | "pr_merged" | "rejected"; + /** Study Id */ + study_id: string | null; + template: components["schemas"]["_TemplateEmbed"]; + }; + /** + * ProposalsListResponse + * @description Body of ``GET /api/v1/proposals``. + */ + ProposalsListResponse: { + /** Data */ + data: components["schemas"]["ProposalSummary"][]; + /** Has More */ + has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; + }; + /** + * QueryHasJudgmentsDetail + * @description The ``detail`` object of a 409 ``QUERY_HAS_JUDGMENTS`` response. + * + * Extends the canonical ``{error_code, message, retryable}`` envelope + * with two structured fields the frontend consumes directly + * (``judgment_lists`` + ``overflow_count``). Wired into the FastAPI + * route's ``responses={409: {"model": QueryHasJudgmentsEnvelope}}`` so + * the OpenAPI schema documents the contract. + */ + QueryHasJudgmentsDetail: { + /** + * Error Code + * @constant + */ + error_code: "QUERY_HAS_JUDGMENTS"; + /** Judgment Lists */ + judgment_lists: components["schemas"]["JudgmentListRef"][]; + /** Message */ + message: string; + /** Overflow Count */ + overflow_count: number; + /** + * Retryable + * @constant + */ + retryable: false; + }; + /** + * QueryHasJudgmentsEnvelope + * @description Top-level 409 wrapper (FastAPI nests under ``detail`` for HTTPException). + */ + QueryHasJudgmentsEnvelope: { + detail: components["schemas"]["QueryHasJudgmentsDetail"]; + }; + /** + * QueryListResponse + * @description ``GET /api/v1/query-sets/{set_id}/queries`` response. + */ + QueryListResponse: { + /** Data */ + data: components["schemas"]["QueryRow"][]; + /** Has More */ + has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; + }; + /** + * QueryRow + * @description Wire row returned by the per-query GET + PATCH endpoints. + * + * Used by both ``GET /api/v1/query-sets/{set_id}/queries`` and + * ``PATCH /api/v1/query-sets/{set_id}/queries/{query_id}``. + * ``judgment_count`` is a derived field — single batched GROUP BY in the + * router via :func:`backend.app.db.repo.judgment.count_judgments_per_query`. + */ + QueryRow: { + /** Id */ + id: string; + /** Judgment Count */ + judgment_count: number; + /** Query Metadata */ + query_metadata: { + [key: string]: unknown; + } | null; + /** Query Text */ + query_text: string; + /** Reference Answer */ + reference_answer: string | null; + }; + /** + * QuerySetDetail + * @description ``GET /api/v1/query-sets/{id}`` response. + */ + QuerySetDetail: { + /** Cluster Id */ + cluster_id: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Description */ + description: string | null; + /** Id */ + id: string; + /** Name */ + name: string; + /** Query Count */ + query_count: number; + }; + /** + * QuerySetListResponse + * @description ``GET /api/v1/query-sets`` response. + */ + QuerySetListResponse: { + /** Data */ + data: components["schemas"]["QuerySetSummary"][]; + /** Has More */ + has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; + }; + /** + * QuerySetSummary + * @description List-view shape; omits ``query_count`` to avoid N+1 counts at list time. + */ + QuerySetSummary: { + /** Cluster Id */ + cluster_id: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Id */ + id: string; + /** Name */ + name: string; + }; + /** + * QueryTemplateDetail + * @description ``GET /api/v1/query-templates/{id}`` response. + */ + QueryTemplateDetail: { + /** Body */ + body: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Declared Params */ + declared_params: { + [key: string]: string; + }; + /** + * Engine Type + * @enum {string} + */ + engine_type: "elasticsearch" | "opensearch" | "solr"; + /** Id */ + id: string; + /** Name */ + name: string; + /** Parent Id */ + parent_id: string | null; + /** Version */ + version: number; + }; + /** + * QueryTemplateListResponse + * @description ``GET /api/v1/query-templates`` response. + */ + QueryTemplateListResponse: { + /** Data */ + data: components["schemas"]["QueryTemplateSummary"][]; + /** Has More */ + has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; + }; + /** + * QueryTemplateSummary + * @description List-view shape; drops ``body`` + ``declared_params`` for brevity. + */ + QueryTemplateSummary: { + /** + * Created At + * Format: date-time + */ + created_at: string; + /** + * Engine Type + * @enum {string} + */ + engine_type: "elasticsearch" | "opensearch" | "solr"; + /** Id */ + id: string; + /** Name */ + name: string; + /** Version */ + version: number; + }; + /** + * RegressorRowShape + * @description One row in the named-regressors or named-improvers table. + * + * Used for BOTH the ``top_regressors`` and ``top_improvers`` lists. + * The wire shape is identical — ``delta = winner_score - comparison_score`` + * is negative on the regressor list, positive on the improver list. The + * class name is historical (regressors shipped first); reusing the same + * type keeps the schema and the per-row renderer compact. + */ + RegressorRowShape: { + /** Comparison Score */ + comparison_score: number; + /** Delta */ + delta: number; + /** Query Id */ + query_id: string; + /** Query Text */ + query_text: string; + /** Winner Score */ + winner_score: number; + }; + /** + * RejectProposalRequest + * @description Body of ``POST /api/v1/proposals/{id}/reject`` (FR-4 / AC-5). + */ + RejectProposalRequest: { + /** Reason */ + reason?: string | null; + }; + /** + * ReseedStatusResponse + * @description Polling-endpoint response for ``GET /api/v1/_test/demo/reseed/status``. + * + * Per ``bug_demo_reseed_fake_metric_regression`` D-2. Lives in Redis as a + * single JSON blob keyed by :data:`DEMO_RESEED_STATUS_KEY` so the + * handler reads it in one round-trip. + */ + ReseedStatusResponse: { + /** Current Step */ + current_step?: string | null; + /** Failed Reason */ + failed_reason?: string | null; + /** Finished At */ + finished_at?: string | null; + /** + * Scenarios Completed + * @default 0 + */ + scenarios_completed: number; + /** Scenarios Skipped */ + scenarios_skipped?: string[]; + /** + * Scenarios Total + * @default 0 + */ + scenarios_total: number; + /** Started At */ + started_at?: string | null; + /** + * Status + * @enum {string} + */ + status: "idle" | "running" | "complete" | "failed"; + /** Steps */ + steps?: string[]; + summary?: components["schemas"]["ReseedSummary"] | null; + }; + /** + * ReseedSummary + * @description Returned by :func:`reseed_demo_state` on success. + * + * Per spec §9 Required invariants, every counter is exactly 4 on the + * happy path; ``duration_ms`` is wall-clock from orchestration start + * to the rename commit. + */ + ReseedSummary: { + /** Clusters Created */ + clusters_created: number; + /** Duration Ms */ + duration_ms: number; + /** Proposals Created */ + proposals_created: number; + /** Query Sets Created */ + query_sets_created: number; + /** Studies Completed */ + studies_completed: number; + }; + /** + * RunQueryHit + * @description One hit in the ``run_query`` response. + */ + RunQueryHit: { + /** Doc Id */ + doc_id: string; + /** Score */ + score: number; + /** Source */ + source?: { + [key: string]: unknown; + } | null; + }; + /** + * RunQueryRequest + * @description ``POST /api/v1/clusters/{id}/run_query`` body. + */ + RunQueryRequest: { + /** Query Dsl */ + query_dsl: { + [key: string]: unknown; + }; + /** Target */ + target: string; + /** + * Top K + * @default 10 + */ + top_k: number; + }; + /** + * RunQueryResponse + * @description ``POST /api/v1/clusters/{id}/run_query`` response. + */ + RunQueryResponse: { + /** Hits */ + hits: components["schemas"]["RunQueryHit"][]; + }; + /** + * RunnerUpGapShape + * @description Runner-up trial's metric vs the winner. + * + * The whole shape is suppressed to ``None`` when there are <2 complete + * trials (FR-2 + FR-7); ``classification`` is non-null whenever this shape + * is present. + */ + RunnerUpGapShape: { + /** + * Classification + * @enum {string} + */ + classification: "robust_plateau" | "sharp_peak"; + /** Runner Up Metric */ + runner_up_metric: number; + /** Top10 Within */ + top10_within: number; + /** Value */ + value: number; + }; + /** + * Schema + * @description An index / collection's field schema. + */ + Schema: { + /** Fields */ + fields: components["schemas"]["FieldSpec"][]; + /** Name */ + name: string; + }; + /** + * SearchSpace + * @description Pydantic model for the ``studies.search_space`` JSONB column. + * + * Wire format:: + * + * { + * "params": { + * "boost_title": {"type": "float", "low": 0.1, "high": 10.0, "log": true}, + * "min_should_match": {"type": "int", "low": 1, "high": 5}, + * "operator": {"type": "categorical", "choices": ["and", "or"]}, + * } + * } + */ + SearchSpace: { + /** Params */ + params: { + [key: string]: components["schemas"]["FloatParam"] | components["schemas"]["IntParam"] | components["schemas"]["CategoricalParam"]; + }; + }; + /** + * SeedAutoFollowupChainRequest + * @description Payload for ``POST /api/v1/_test/auto-followup/seed-chain``. + * + * Seeds ``depth + 1`` linked studies (root → … → leaf) so E2E tests can + * cover the chain-panel parent-link / children-table / cascade-radio paths + * that the public ``POST /api/v1/studies`` endpoint can't drive + * (``parent_study_id`` is set only by the auto-followup worker). + * + * Closes ``chore_auto_followup_e2e_chain_seed_helper`` (idea #2). + */ + SeedAutoFollowupChainRequest: { + /** Cluster Id */ + cluster_id: string; + /** + * Depth + * @description Number of chain hops to seed. depth=1 → root + leaf (2 nodes). depth=2 → root + 1 middle + leaf (3 nodes). + */ + depth: number; + /** + * In Flight Leaf + * @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. + * @default true + */ + in_flight_leaf: boolean; + /** + * In Flight Middle + * @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). + * @default true + */ + in_flight_middle: boolean; + /** Judgment List Id */ + judgment_list_id: string; + /** Query Set Id */ + query_set_id: string; + /** Template Id */ + template_id: string; + }; + /** + * SeedAutoFollowupChainResponse + * @description IDs of every node in the seeded chain, in parent→child order. + */ + SeedAutoFollowupChainResponse: { + /** Leaf Id */ + leaf_id: string; + /** Middle Ids */ + middle_ids: string[]; + /** Root Id */ + root_id: string; + }; + /** + * SeedCompletedStudyRequest + * @description Payload for ``POST /api/v1/_test/studies/seed-completed``. + * + * All four FK fields are required; the caller is responsible for + * seeding the parent rows first (typically via the public + * ``seedFullChain`` E2E helper). + */ + SeedCompletedStudyRequest: { + /** Cluster Id */ + cluster_id: string; + /** + * Extra Trial Metrics + * @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. + */ + extra_trial_metrics?: number[] | null; + /** Judgment List Id */ + judgment_list_id: string; + /** Query Set Id */ + query_set_id: string; + /** + * Runner Up Per Query + * @description Optional per-query metrics for the runner-up trial; pairs with `winner_per_query`. + */ + runner_up_per_query?: { + [key: string]: { + [key: string]: unknown; + }; + } | null; + /** + * Suggested Followups + * @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. + */ + suggested_followups?: { + [key: string]: unknown; + }[] | null; + /** Template Id */ + template_id: string; + /** + * Winner Per Query + * @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). + */ + winner_per_query?: { + [key: string]: { + [key: string]: unknown; + }; + } | null; + /** + * With Pending Proposal + * @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. + * @default true + */ + with_pending_proposal: boolean; + }; + /** + * SeedCompletedStudyResponse + * @description IDs of the inserted rows; mirrors :class:`SeededStudyTriple`. + */ + SeedCompletedStudyResponse: { + /** Digest Id */ + digest_id: string; + /** Proposal Id */ + proposal_id: string | null; + /** Study Id */ + study_id: string; + }; + /** + * SendMessageRequest + * @description ``POST /api/v1/conversations/{id}/messages`` body (Story 3.2). + */ + SendMessageRequest: { + content: components["schemas"]["SendMessageRequestContent"]; + /** + * Role + * @default user + * @constant + */ + role: "user"; + }; + /** + * SendMessageRequestContent + * @description Sub-shape inside :class:`SendMessageRequest`. + */ + SendMessageRequestContent: { + /** Text */ + text: string; + }; + /** + * StudyChainLink + * @description One link in the rolled-up overnight-chain summary (feat_overnight_autopilot §8.3). + */ + StudyChainLink: { + /** Auto Followup Depth Remaining */ + auto_followup_depth_remaining: number | null; + /** Baseline Metric */ + baseline_metric: number | null; + /** Best Metric */ + best_metric: number | null; + /** Completed At */ + completed_at: string | null; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Delta From Prev */ + delta_from_prev: number | null; + /** + * Direction + * @enum {string} + */ + direction: "maximize" | "minimize"; + /** Failed Reason */ + failed_reason: string | null; + /** Id */ + id: string; + /** Name */ + name: string; + /** Proposal Id */ + proposal_id: string | null; + /** + * Status + * @enum {string} + */ + status: "queued" | "running" | "completed" | "cancelled" | "failed"; + }; + /** + * StudyChainResponse + * @description ``GET /api/v1/studies/{id}/chain`` response (feat_overnight_autopilot §8.3). + */ + StudyChainResponse: { + /** Anchor Study Id */ + anchor_study_id: string; + /** Best Link Id */ + best_link_id: string | null; + /** Best Metric */ + best_metric: number | null; + /** Cumulative Lift */ + cumulative_lift: number | null; + /** + * Direction + * @enum {string} + */ + direction: "maximize" | "minimize"; + /** Links */ + links: components["schemas"]["StudyChainLink"][]; + /** Proposal Id For Best Link */ + proposal_id_for_best_link: string | null; + /** + * Stop Reason + * @enum {string} + */ + stop_reason: "depth_exhausted" | "no_lift" | "budget" | "parent_failed" | "cancelled" | "in_flight"; + }; + /** + * StudyConfigSpec + * @description Wire shape of ``studies.config`` (write-side). + * + * The model_validator below enforces that at least one stop condition is + * set — otherwise the study has no terminating condition (FR-4). + * ``parallelism`` / ``trial_timeout_s`` are optional; when absent the + * worker reads ``Settings.studies_default_parallelism`` / + * ``studies_default_timeout_s`` at job time. The API layer does NOT + * materialize these fields into the stored row — see Story 1.5 + + * Story 3.3's ``config.model_dump(exclude_none=True, exclude_unset=True)`` + * contract. + */ + StudyConfigSpec: { + /** Auto Followup Depth */ + auto_followup_depth?: number | null; + /** Baseline Params */ + baseline_params?: { + [key: string]: string | number | boolean | null; + } | null; + /** Max Trials */ + max_trials?: number | null; + /** Parallelism */ + parallelism?: number | null; + /** Pruner */ + pruner?: ("median" | "none") | null; + /** Sampler */ + sampler?: ("tpe" | "random") | null; + /** Secondary Metrics */ + secondary_metrics?: string[] | null; + /** Seed */ + seed?: number | null; + /** Time Budget Min */ + time_budget_min?: number | null; + /** Trial Timeout S */ + trial_timeout_s?: number | null; + }; + /** + * StudyConvergenceShape + * @description Verdict + supporting numerics for the UI panel and the digest narrative. + * + * Mirrors the ``ConfidenceShape`` pattern from ``confidence.py``: the + * domain module owns the Pydantic model, and ``backend.app.api.v1.schemas`` + * re-exports it for the ``StudyDetail.convergence`` field. The + * ``best_so_far_curve`` is the chart's data series; ``verdict`` is the + * badge label. + * + * **Name discipline (plan §0).** The bare class name ``ConvergenceShape`` + * is already taken by :class:`backend.app.domain.study.confidence.ConvergenceShape` + * (a different concept — winner-trial *timing*, not metric plateau). + * ``StudyConvergenceShape`` is the study-level analogue; the confidence + * sub-shape stays on its inner module. The two coexist on ``StudyDetail`` + * (``confidence.convergence`` is the inner one; ``convergence`` is this + * one), and FastAPI emits both under their bare class names in the + * OpenAPI schema — no fully-qualified disambiguation noise leaks to the + * frontend. + */ + StudyConvergenceShape: { + /** Best So Far Curve */ + best_so_far_curve: components["schemas"]["CurvePoint"][]; + /** + * Direction + * @enum {string} + */ + direction: "maximize" | "minimize"; + /** Epsilon */ + epsilon: number; + /** Improvement In Window */ + improvement_in_window: number; + /** Total Complete Trials */ + total_complete_trials: number; + /** + * Verdict + * @enum {string} + */ + verdict: "converged" | "still_improving" | "too_few_trials"; + /** Warmup Floor */ + warmup_floor: number; + /** Window Size */ + window_size: number; + }; + /** + * StudyDetail + * @description ``GET /api/v1/studies/{id}`` response + ``POST/cancel`` response. + */ + StudyDetail: { + /** Baseline Metric */ + baseline_metric: number | null; + /** Baseline Trial Id */ + baseline_trial_id: string | null; + /** Best Metric */ + best_metric: number | null; + /** Best Trial Id */ + best_trial_id: string | null; + /** Cluster Id */ + cluster_id: string; + /** Completed At */ + completed_at: string | null; + confidence?: components["schemas"]["ConfidenceShape"] | null; + /** Config */ + config: { + [key: string]: unknown; + }; + convergence?: components["schemas"]["StudyConvergenceShape"] | null; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Failed Reason */ + failed_reason: string | null; + /** Id */ + id: string; + /** Judgment List Id */ + judgment_list_id: string; + /** Name */ + name: string; + /** Objective */ + objective: { + [key: string]: unknown; + }; + /** Optuna Study Name */ + optuna_study_name: string; + /** Parent Study Id */ + parent_study_id: string | null; + /** Query Set Id */ + query_set_id: string; + /** Search Space */ + search_space: { + [key: string]: unknown; + }; + /** Started At */ + started_at: string | null; + /** + * Status + * @enum {string} + */ + status: "queued" | "running" | "completed" | "cancelled" | "failed"; + /** Target */ + target: string; + /** Template Id */ + template_id: string; + trials_summary: components["schemas"]["TrialsSummaryShape"]; + }; + /** + * StudyListResponse + * @description ``GET /api/v1/studies`` response. + */ + StudyListResponse: { + /** Data */ + data: components["schemas"]["StudySummary"][]; + /** Has More */ + has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; + }; + /** + * StudySummary + * @description List-view shape. + */ + StudySummary: { + /** Best Metric */ + best_metric: number | null; + /** Cluster Id */ + cluster_id: string; + /** Completed At */ + completed_at: string | null; + /** Convergence Verdict */ + convergence_verdict?: ("converged" | "still_improving" | "too_few_trials") | null; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** + * Direction + * @default maximize + * @enum {string} + */ + direction: "maximize" | "minimize"; + /** Id */ + id: string; + /** Name */ + name: string; + /** + * Status + * @enum {string} + */ + status: "queued" | "running" | "completed" | "cancelled" | "failed"; + /** + * Trial Count + * @default 0 + */ + trial_count: number; + }; + /** + * Subsystems + * @description Per-subsystem reachability/configuration state. Wire values per spec §7.4. + */ + Subsystems: { + /** + * Db + * @description Postgres reachability + * @enum {string} + */ + db: "ok" | "down"; + /** + * Elasticsearch + * @description Local Elasticsearch container reachability + * @enum {string} + */ + elasticsearch: "reachable" | "unreachable"; + /** @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`. */ + elasticsearch_clusters: components["schemas"]["ClusterAggregateHealth"]; + /** + * 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 {string} + */ + openai: "configured" | "missing_key" | "incapable"; + /** + * Opensearch + * @description Local OpenSearch container reachability + * @enum {string} + */ + opensearch: "reachable" | "unreachable"; + /** + * Redis + * @description Redis reachability + * @enum {string} + */ + redis: "ok" | "down"; + /** + * Solr + * @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. + * @default not_configured + * @enum {string} + */ + solr: "reachable" | "unreachable" | "not_configured"; + }; + /** + * SwapTemplateFollowup + * @description A 'swap_template' followup — re-run against a different query template. + * + * Carries the LLM-proposed bounds for params shared with the parent template + * in ``search_space``. The digest worker calls + * :func:`backend.app.domain.study.template_swap.remap_search_space_for_swap_target` + * after parsing to merge these bounds with heuristic defaults for any + * swap-target params not shared with the parent. + * + * Owner: ``feat_digest_executable_followups_swap_template`` (Tier B). + */ + SwapTemplateFollowup: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + kind: "swap_template"; + /** Rationale */ + rationale: string; + search_space: components["schemas"]["SearchSpace"]; + /** Template Id */ + template_id: string; + }; + /** + * TargetInfo + * @description One target (index / collection) on a cluster. + */ + TargetInfo: { + /** Doc Count */ + doc_count?: number | null; + /** Name */ + name: string; + }; + /** + * TargetListResponse + * @description Response for ``GET /api/v1/clusters/{cluster_id}/targets`` (FR-1). + * + * Unpaginated by design — see feature_spec.md §7.1 "pagination shape + * rationale". The single-resource lookup pattern matches + * ``/clusters/{id}/schema`` rather than the queryable ``/clusters`` list. + * ``EntitySelectListPage``'s ``next_cursor`` and ``has_more`` fields + * are optional, so this bare ``data``-only shape consumes correctly on + * the frontend without pretending to be a cursor endpoint. + */ + TargetListResponse: { + /** Data */ + data: components["schemas"]["TargetInfo"][]; + }; + /** + * TextFollowup + * @description A free-form textual suggestion — no auto-prefill, operator interprets. + */ + TextFollowup: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + kind: "text"; + /** Rationale */ + rationale: string; + /** Search Space */ + search_space?: null; + }; + /** + * TrialDetail + * @description ``GET /api/v1/studies/{id}/trials`` response row. + */ + TrialDetail: { + /** Duration Ms */ + duration_ms: number | null; + /** Ended At */ + ended_at: string | null; + /** Error */ + error: string | null; + /** Id */ + id: string; + /** + * Is Baseline + * @default false + */ + is_baseline: boolean; + /** Metrics */ + metrics: { + [key: string]: unknown; + }; + /** Optuna Trial Number */ + optuna_trial_number: number; + /** Params */ + params: { + [key: string]: unknown; + }; + /** Primary Metric */ + primary_metric: number | null; + /** Started At */ + started_at: string | null; + /** + * Status + * @enum {string} + */ + status: "complete" | "failed" | "pruned"; + /** Study Id */ + study_id: string; + }; + /** + * TrialListResponse + * @description ``GET /api/v1/studies/{id}/trials`` response. + */ + TrialListResponse: { + /** Data */ + data: components["schemas"]["TrialDetail"][]; + /** Has More */ + has_more: boolean; + /** Next Cursor */ + next_cursor: string | null; + }; + /** + * TrialsSummaryShape + * @description The ``trials_summary`` field embedded in :class:`StudyDetail`. + */ + TrialsSummaryShape: { + /** Best Primary Metric */ + best_primary_metric: number | null; + /** Complete */ + complete: number; + /** Failed */ + failed: number; + /** Pruned */ + pruned: number; + /** Total */ + total: number; + }; + /** + * UbiReadinessResponse + * @description ``GET /api/v1/clusters/{cluster_id}/ubi-readiness`` response (FR-7). + * + * ``covered_pairs_pct`` and ``head_covered`` are nullable — MVP2's + * rung classifier uses event-count thresholds (the SearchAdapter + * Protocol doesn't expose an exact ``_count`` endpoint). The fields + * are reserved on the wire so a future ``infra_adapter_count_method`` + * can fill them without breaking the contract. See + * :mod:`backend.app.services.ubi_readiness` for the rationale. + */ + UbiReadinessResponse: { + /** + * Checked At + * Format: date-time + */ + checked_at: string; + /** Covered Pairs Pct */ + covered_pairs_pct: number | null; + /** Head Covered */ + head_covered: boolean | null; + /** + * Rung + * @enum {string} + */ + rung: "rung_0" | "rung_1" | "rung_2" | "rung_3"; + }; + /** + * UpdateQueryRequest + * @description ``PATCH /api/v1/query-sets/{set_id}/queries/{query_id}`` body. + * + * Whole-object replace on ``query_metadata`` (NOT deep-merge); explicit + * ``null`` removes a nullable field; omitted key = no change. Empty + * body ``{}`` validates as a no-op (AC-28). + * + * ``query_text`` is NOT NULL on the underlying table, so explicit-null + * is rejected by the ``@model_validator`` below (a 422 surfaces sooner + * than the SQL ``NotNullViolation``). + */ + UpdateQueryRequest: { + /** Query Metadata */ + query_metadata?: { + [key: string]: unknown; + } | null; + /** Query Text */ + query_text?: string | null; + /** Reference Answer */ + reference_answer?: string | null; + }; + /** ValidationError */ + ValidationError: { + /** Context */ + ctx?: Record; + /** Input */ + input?: unknown; + /** Location */ + loc: (string | number)[]; + /** Message */ + msg: string; + /** Error Type */ + type: string; + }; + /** + * WidenFollowup + * @description A 'widen' followup — re-run with a broader range than the parent. + */ + WidenFollowup: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + kind: "widen"; + /** Rationale */ + rationale: string; + search_space: components["schemas"]["SearchSpace"]; + }; + /** + * _ClusterEmbed + * @description Inline cluster summary on proposal responses. + */ + _ClusterEmbed: { + /** Engine Type */ + engine_type: string; + /** Environment */ + environment?: string | null; + /** Id */ + id: string; + /** Name */ + name: string; + }; + /** + * _DigestEmbed + * @description Inline digest summary on the proposal-detail response. + * + * feat_digest_executable_followups Story 4.1 — ``suggested_followups`` is + * now a discriminated-union list (see ``DigestResponse``). + */ + _DigestEmbed: { + /** + * Generated At + * Format: date-time + */ + generated_at: string; + /** Id */ + id: string; + /** Narrative */ + narrative: string; + /** Parameter Importance */ + parameter_importance: { + [key: string]: number; + }; + /** Recommended Config */ + recommended_config: { + [key: string]: unknown; + }; + /** Suggested Followups */ + suggested_followups: components["schemas"]["FollowupItem"][]; + }; + /** + * _SourceBreakdown + * @description Source-breakdown sub-shape on :class:`JudgmentListDetail`. + * + * Evolved 2026-05-29 by ``feat_ubi_judgments`` FR-10 — now three terms + * (``llm + human + click == judgment_count``). The cycle-2 F6 + * "click folds into human" contract is superseded the moment UBI ships + * click rows; the UI's source-breakdown card now renders all three + * buckets separately so operators see the mix at a glance. + */ + _SourceBreakdown: { + /** Click */ + click: number; + /** Human */ + human: number; + /** Llm */ + llm: number; + }; + /** + * _StudySummary + * @description Inline study summary on the proposal-detail response. + */ + _StudySummary: { + /** Best Metric */ + best_metric: number | null; + /** Best Trial Id */ + best_trial_id: string | null; + /** Id */ + id: string; + /** Judgment List */ + judgment_list: { + [key: string]: unknown; + }; + /** Name */ + name: string; + /** Query Set */ + query_set: { + [key: string]: unknown; + }; + /** Status */ + status: string; + }; + /** + * _TemplateEmbed + * @description Inline template summary on proposal responses. + */ + _TemplateEmbed: { + /** Engine Type */ + engine_type?: string | null; + /** Id */ + id: string; + /** Name */ + name: string; + /** Version */ + version: number; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } export type $defs = Record; export interface operations { - seed_auto_followup_chain_endpoint_api_v1__test_auto_followup_seed_chain_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['SeedAutoFollowupChainRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SeedAutoFollowupChainResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - reseed_demo_api_v1__test_demo_reseed_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 202: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ReseedStatusResponse']; - }; - }; - }; - }; - reseed_demo_status_api_v1__test_demo_reseed_status_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ReseedStatusResponse']; - }; - }; - }; - }; - delete_test_digest_api_v1__test_digests__digest_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - digest_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - delete_test_judgment_list_api_v1__test_judgment_lists__judgment_list_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - judgment_list_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - delete_test_proposal_api_v1__test_proposals__proposal_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - proposal_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - delete_test_query_set_api_v1__test_query_sets__query_set_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - query_set_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - delete_test_query_template_api_v1__test_query_templates__template_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - template_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - seed_completed_study_api_v1__test_studies_seed_completed_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['SeedCompletedStudyRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SeedCompletedStudyResponse']; + seed_auto_followup_chain_endpoint_api_v1__test_auto_followup_seed_chain_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SeedAutoFollowupChainRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SeedAutoFollowupChainResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + reseed_demo_api_v1__test_demo_reseed_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ReseedStatusResponse"]; + }; + }; + }; + }; + reseed_demo_status_api_v1__test_demo_reseed_status_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ReseedStatusResponse"]; + }; + }; + }; + }; + delete_test_digest_api_v1__test_digests__digest_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + digest_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_test_judgment_list_api_v1__test_judgment_lists__judgment_list_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + judgment_list_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_test_proposal_api_v1__test_proposals__proposal_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + proposal_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_test_query_set_api_v1__test_query_sets__query_set_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + query_set_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_test_query_template_api_v1__test_query_templates__template_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + template_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + seed_completed_study_api_v1__test_studies_seed_completed_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SeedCompletedStudyRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SeedCompletedStudyResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_test_study_api_v1__test_studies__study_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + study_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_clusters_api_v1_clusters_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + since?: string | null; + q?: string | null; + sort?: ("name:asc" | "name:desc" | "created_at:asc" | "created_at:desc" | "environment:asc" | "environment:desc") | null; + engine_type?: ("elasticsearch" | "opensearch" | "solr") | null; + environment?: ("prod" | "staging" | "dev") | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ClusterListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_cluster_api_v1_clusters_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateClusterRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ClusterDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + test_connection_api_v1_clusters_test_connection_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ConnectionTestRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ConnectionTestResult"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_cluster_detail_api_v1_clusters__cluster_id__get: { + parameters: { + query?: never; + header?: never; + path: { + cluster_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ClusterDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_cluster_api_v1_clusters__cluster_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + cluster_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + reprobe_cluster_api_v1_clusters__cluster_id__reprobe_post: { + parameters: { + query?: never; + header?: never; + path: { + cluster_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ClusterDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + run_query_api_v1_clusters__cluster_id__run_query_post: { + parameters: { + query?: { + timeout_s?: number; + }; + header?: never; + path: { + cluster_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["RunQueryRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RunQueryResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_cluster_schema_api_v1_clusters__cluster_id__schema_get: { + parameters: { + query: { + target: string; + }; + header?: never; + path: { + cluster_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Schema"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_cluster_targets_api_v1_clusters__cluster_id__targets_get: { + parameters: { + query?: never; + header?: never; + path: { + cluster_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TargetListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_target_documents_api_v1_clusters__cluster_id__targets__target__documents_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + fields?: string | null; + }; + header?: never; + path: { + cluster_id: string; + target: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DocumentListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_target_document_api_v1_clusters__cluster_id__targets__target__documents__doc_id__get: { + parameters: { + query?: never; + header?: never; + path: { + cluster_id: string; + target: string; + doc_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Document"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_cluster_ubi_readiness_api_v1_clusters__cluster_id__ubi_readiness_get: { + parameters: { + query: { + query_set_id: string; + target: string; + }; + header?: never; + path: { + cluster_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UbiReadinessResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_config_repos_endpoint_api_v1_config_repos_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ConfigReposListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_config_repo_endpoint_api_v1_config_repos_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateConfigRepoRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ConfigRepoDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_config_repo_endpoint_api_v1_config_repos__config_repo_id__get: { + parameters: { + query?: never; + header?: never; + path: { + config_repo_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ConfigRepoDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_conversations_endpoint_api_v1_conversations_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + since?: string | null; + q?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ConversationsListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_conversation_endpoint_api_v1_conversations_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateConversationRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ConversationSummary"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_conversation_endpoint_api_v1_conversations__conversation_id__get: { + parameters: { + query?: never; + header?: never; + path: { + conversation_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ConversationDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_conversation_endpoint_api_v1_conversations__conversation_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + conversation_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + post_message_endpoint_api_v1_conversations__conversation_id__messages_post: { + parameters: { + query?: never; + header?: never; + path: { + conversation_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SendMessageRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_judgment_lists_endpoint_api_v1_judgment_lists_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + since?: string | null; + q?: string | null; + sort?: ("name:asc" | "name:desc" | "created_at:asc" | "created_at:desc" | "status:asc" | "status:desc") | null; + query_set_id?: string | null; + cluster_id?: string | null; + target?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["JudgmentListListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + import_judgment_list_api_v1_judgment_lists_import_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ImportJudgmentListRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["JudgmentListDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_judgment_list_endpoint_api_v1_judgment_lists__judgment_list_id__get: { + parameters: { + query?: never; + header?: never; + path: { + judgment_list_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["JudgmentListDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + calibrate_judgment_list_api_v1_judgment_lists__judgment_list_id__calibration_post: { + parameters: { + query?: never; + header?: never; + path: { + judgment_list_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CalibrationSamplesRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CalibrationResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_judgments_endpoint_api_v1_judgment_lists__judgment_list_id__judgments_get: { + parameters: { + query?: { + source?: ("llm" | "human" | "click") | null; + cursor?: string | null; + limit?: number; + sort?: ("created_at:asc" | "created_at:desc" | "rating:asc" | "rating:desc" | "source:asc" | "source:desc") | null; + }; + header?: never; + path: { + judgment_list_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["JudgmentListJudgmentsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + override_judgment_api_v1_judgment_lists__judgment_list_id__judgments__judgment_id__patch: { + parameters: { + query?: never; + header?: never; + path: { + judgment_list_id: string; + judgment_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["OverrideJudgmentRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["JudgmentRow"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + generate_judgments_api_v1_judgments_generate_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateJudgmentListGenerateRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GenerateJudgmentsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + generate_judgments_from_ubi_api_v1_judgments_generate_from_ubi_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateJudgmentListFromUbiRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GenerateJudgmentsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_proposals_endpoint_api_v1_proposals_get: { + parameters: { + query?: { + status?: ("pending" | "pr_opened" | "pr_merged" | "rejected") | null; + cluster_id?: string | null; + source?: ("study" | "manual") | null; + template_id?: string | null; + study_id?: string | null; + is_last_merged?: boolean | null; + cursor?: string | null; + limit?: number; + sort?: ("created_at:asc" | "created_at:desc" | "status:asc" | "status:desc" | "pr_state:asc" | "pr_state:desc") | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProposalsListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_manual_proposal_api_v1_proposals_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateProposalRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProposalDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_proposal_endpoint_api_v1_proposals__proposal_id__get: { + parameters: { + query?: never; + header?: never; + path: { + proposal_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProposalDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + open_pr_endpoint_api_v1_proposals__proposal_id__open_pr_post: { + parameters: { + query?: never; + header?: never; + path: { + proposal_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OpenPrResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + reject_proposal_endpoint_api_v1_proposals__proposal_id__reject_post: { + parameters: { + query?: never; + header?: never; + path: { + proposal_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["RejectProposalRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProposalDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_query_sets_api_v1_query_sets_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + since?: string | null; + q?: string | null; + sort?: ("name:asc" | "name:desc" | "created_at:asc" | "created_at:desc") | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["QuerySetListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_query_set_api_v1_query_sets_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateQuerySetRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["QuerySetDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_query_set_detail_api_v1_query_sets__query_set_id__get: { + parameters: { + query?: never; + header?: never; + path: { + query_set_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["QuerySetDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_queries_in_set_api_v1_query_sets__query_set_id__queries_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + since?: string | null; + }; + header?: never; + path: { + query_set_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["QueryListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + bulk_add_queries_api_v1_query_sets__query_set_id__queries_post: { + parameters: { + query?: never; + header?: never; + path: { + query_set_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BulkQueriesResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + query_set_id: string; + query_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["QueryHasJudgmentsEnvelope"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + update_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__patch: { + parameters: { + query?: never; + header?: never; + path: { + query_set_id: string; + query_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateQueryRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["QueryRow"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_query_templates_api_v1_query_templates_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + since?: string | null; + q?: string | null; + sort?: ("name:asc" | "name:desc" | "created_at:asc" | "created_at:desc" | "engine_type:asc" | "engine_type:desc" | "version:asc" | "version:desc") | null; + engine_type?: ("elasticsearch" | "opensearch" | "solr") | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["QueryTemplateListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_query_template_api_v1_query_templates_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateQueryTemplateRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["QueryTemplateDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_query_template_detail_api_v1_query_templates__template_id__get: { + parameters: { + query?: never; + header?: never; + path: { + template_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["QueryTemplateDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_studies_api_v1_studies_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + since?: string | null; + status?: ("queued" | "running" | "completed" | "cancelled" | "failed") | null; + cluster_id?: string | null; + target?: string | null; + q?: string | null; + sort?: ("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") | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StudyListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_study_api_v1_studies_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateStudyRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StudyDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_study_detail_api_v1_studies__study_id__get: { + parameters: { + query?: never; + header?: never; + path: { + study_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StudyDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + cancel_study_api_v1_studies__study_id__cancel_post: { + parameters: { + query?: { + cascade?: string; + }; + header?: never; + path: { + study_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StudyDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_study_chain_api_v1_studies__study_id__chain_get: { + parameters: { + query?: never; + header?: never; + path: { + study_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StudyChainResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_study_children_api_v1_studies__study_id__children_get: { + parameters: { + query?: never; + header?: never; + path: { + study_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StudyListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_study_digest_api_v1_studies__study_id__digest_get: { + parameters: { + query?: never; + header?: never; + path: { + study_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DigestResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_study_trials_api_v1_studies__study_id__trials_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + since?: string | null; + sort?: string; + }; + header?: never; + path: { + study_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrialListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + healthz_healthz_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HealthResponse"]; + }; + }; + /** @description One or more required subsystems is down */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HealthResponse"]; + }; + }; + }; + }; + github_webhook_webhooks_github_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: string; + }; + }; + }; }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - delete_test_study_api_v1__test_studies__study_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - study_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_clusters_api_v1_clusters_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - since?: string | null; - q?: string | null; - sort?: - | ( - | 'name:asc' - | 'name:desc' - | 'created_at:asc' - | 'created_at:desc' - | 'environment:asc' - | 'environment:desc' - ) - | null; - engine_type?: ('elasticsearch' | 'opensearch' | 'solr') | null; - environment?: ('prod' | 'staging' | 'dev') | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ClusterListResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - create_cluster_api_v1_clusters_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateClusterRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ClusterDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - test_connection_api_v1_clusters_test_connection_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['ConnectionTestRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ConnectionTestResult']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_cluster_detail_api_v1_clusters__cluster_id__get: { - parameters: { - query?: never; - header?: never; - path: { - cluster_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ClusterDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - delete_cluster_api_v1_clusters__cluster_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - cluster_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - reprobe_cluster_api_v1_clusters__cluster_id__reprobe_post: { - parameters: { - query?: never; - header?: never; - path: { - cluster_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 202: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ClusterDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - run_query_api_v1_clusters__cluster_id__run_query_post: { - parameters: { - query?: { - timeout_s?: number; - }; - header?: never; - path: { - cluster_id: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['RunQueryRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['RunQueryResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_cluster_schema_api_v1_clusters__cluster_id__schema_get: { - parameters: { - query: { - target: string; - }; - header?: never; - path: { - cluster_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Schema']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_cluster_targets_api_v1_clusters__cluster_id__targets_get: { - parameters: { - query?: never; - header?: never; - path: { - cluster_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['TargetListResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_target_documents_api_v1_clusters__cluster_id__targets__target__documents_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - fields?: string | null; - }; - header?: never; - path: { - cluster_id: string; - target: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['DocumentListResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_target_document_api_v1_clusters__cluster_id__targets__target__documents__doc_id__get: { - parameters: { - query?: never; - header?: never; - path: { - cluster_id: string; - target: string; - doc_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Document']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_cluster_ubi_readiness_api_v1_clusters__cluster_id__ubi_readiness_get: { - parameters: { - query: { - query_set_id: string; - target: string; - }; - header?: never; - path: { - cluster_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['UbiReadinessResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_config_repos_endpoint_api_v1_config_repos_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ConfigReposListResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - create_config_repo_endpoint_api_v1_config_repos_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateConfigRepoRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ConfigRepoDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_config_repo_endpoint_api_v1_config_repos__config_repo_id__get: { - parameters: { - query?: never; - header?: never; - path: { - config_repo_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ConfigRepoDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_conversations_endpoint_api_v1_conversations_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - since?: string | null; - q?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ConversationsListResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - create_conversation_endpoint_api_v1_conversations_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateConversationRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ConversationSummary']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_conversation_endpoint_api_v1_conversations__conversation_id__get: { - parameters: { - query?: never; - header?: never; - path: { - conversation_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ConversationDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - delete_conversation_endpoint_api_v1_conversations__conversation_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - conversation_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - post_message_endpoint_api_v1_conversations__conversation_id__messages_post: { - parameters: { - query?: never; - header?: never; - path: { - conversation_id: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['SendMessageRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_judgment_lists_endpoint_api_v1_judgment_lists_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - since?: string | null; - q?: string | null; - sort?: - | ( - | 'name:asc' - | 'name:desc' - | 'created_at:asc' - | 'created_at:desc' - | 'status:asc' - | 'status:desc' - ) - | null; - query_set_id?: string | null; - cluster_id?: string | null; - target?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['JudgmentListListResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - import_judgment_list_api_v1_judgment_lists_import_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['ImportJudgmentListRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['JudgmentListDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_judgment_list_endpoint_api_v1_judgment_lists__judgment_list_id__get: { - parameters: { - query?: never; - header?: never; - path: { - judgment_list_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['JudgmentListDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - calibrate_judgment_list_api_v1_judgment_lists__judgment_list_id__calibration_post: { - parameters: { - query?: never; - header?: never; - path: { - judgment_list_id: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CalibrationSamplesRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['CalibrationResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_judgments_endpoint_api_v1_judgment_lists__judgment_list_id__judgments_get: { - parameters: { - query?: { - source?: ('llm' | 'human' | 'click') | null; - cursor?: string | null; - limit?: number; - sort?: - | ( - | 'created_at:asc' - | 'created_at:desc' - | 'rating:asc' - | 'rating:desc' - | 'source:asc' - | 'source:desc' - ) - | null; - }; - header?: never; - path: { - judgment_list_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['JudgmentListJudgmentsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - override_judgment_api_v1_judgment_lists__judgment_list_id__judgments__judgment_id__patch: { - parameters: { - query?: never; - header?: never; - path: { - judgment_list_id: string; - judgment_id: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['OverrideJudgmentRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['JudgmentRow']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - generate_judgments_api_v1_judgments_generate_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateJudgmentListGenerateRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 202: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GenerateJudgmentsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - generate_judgments_from_ubi_api_v1_judgments_generate_from_ubi_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateJudgmentListFromUbiRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 202: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GenerateJudgmentsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_proposals_endpoint_api_v1_proposals_get: { - parameters: { - query?: { - status?: ('pending' | 'pr_opened' | 'pr_merged' | 'rejected') | null; - cluster_id?: string | null; - source?: ('study' | 'manual') | null; - template_id?: string | null; - study_id?: string | null; - is_last_merged?: boolean | null; - cursor?: string | null; - limit?: number; - sort?: - | ( - | 'created_at:asc' - | 'created_at:desc' - | 'status:asc' - | 'status:desc' - | 'pr_state:asc' - | 'pr_state:desc' - ) - | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ProposalsListResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - create_manual_proposal_api_v1_proposals_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateProposalRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ProposalDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_proposal_endpoint_api_v1_proposals__proposal_id__get: { - parameters: { - query?: never; - header?: never; - path: { - proposal_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ProposalDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - open_pr_endpoint_api_v1_proposals__proposal_id__open_pr_post: { - parameters: { - query?: never; - header?: never; - path: { - proposal_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 202: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['OpenPrResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - reject_proposal_endpoint_api_v1_proposals__proposal_id__reject_post: { - parameters: { - query?: never; - header?: never; - path: { - proposal_id: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['RejectProposalRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ProposalDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_query_sets_api_v1_query_sets_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - since?: string | null; - q?: string | null; - sort?: ('name:asc' | 'name:desc' | 'created_at:asc' | 'created_at:desc') | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['QuerySetListResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - create_query_set_api_v1_query_sets_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateQuerySetRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['QuerySetDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_query_set_detail_api_v1_query_sets__query_set_id__get: { - parameters: { - query?: never; - header?: never; - path: { - query_set_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['QuerySetDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_queries_in_set_api_v1_query_sets__query_set_id__queries_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - since?: string | null; - }; - header?: never; - path: { - query_set_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['QueryListResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - bulk_add_queries_api_v1_query_sets__query_set_id__queries_post: { - parameters: { - query?: never; - header?: never; - path: { - query_set_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['BulkQueriesResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - delete_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - query_set_id: string; - query_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Conflict */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['QueryHasJudgmentsEnvelope']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - update_query_endpoint_api_v1_query_sets__query_set_id__queries__query_id__patch: { - parameters: { - query?: never; - header?: never; - path: { - query_set_id: string; - query_id: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['UpdateQueryRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['QueryRow']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_query_templates_api_v1_query_templates_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - since?: string | null; - q?: string | null; - sort?: - | ( - | 'name:asc' - | 'name:desc' - | 'created_at:asc' - | 'created_at:desc' - | 'engine_type:asc' - | 'engine_type:desc' - | 'version:asc' - | 'version:desc' - ) - | null; - engine_type?: ('elasticsearch' | 'opensearch' | 'solr') | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['QueryTemplateListResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - create_query_template_api_v1_query_templates_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateQueryTemplateRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['QueryTemplateDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_query_template_detail_api_v1_query_templates__template_id__get: { - parameters: { - query?: never; - header?: never; - path: { - template_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['QueryTemplateDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_studies_api_v1_studies_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - since?: string | null; - status?: ('queued' | 'running' | 'completed' | 'cancelled' | 'failed') | null; - cluster_id?: string | null; - target?: string | null; - q?: string | null; - sort?: - | ( - | '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' - ) - | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['StudyListResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - create_study_api_v1_studies_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateStudyRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['StudyDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_study_detail_api_v1_studies__study_id__get: { - parameters: { - query?: never; - header?: never; - path: { - study_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['StudyDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - cancel_study_api_v1_studies__study_id__cancel_post: { - parameters: { - query?: { - cascade?: string; - }; - header?: never; - path: { - study_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['StudyDetail']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_study_chain_api_v1_studies__study_id__chain_get: { - parameters: { - query?: never; - header?: never; - path: { - study_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['StudyChainResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_study_children_api_v1_studies__study_id__children_get: { - parameters: { - query?: never; - header?: never; - path: { - study_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['StudyListResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_study_digest_api_v1_studies__study_id__digest_get: { - parameters: { - query?: never; - header?: never; - path: { - study_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['DigestResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_study_trials_api_v1_studies__study_id__trials_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - since?: string | null; - sort?: string; - }; - header?: never; - path: { - study_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['TrialListResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - healthz_healthz_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HealthResponse']; - }; - }; - /** @description One or more required subsystems is down */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HealthResponse']; - }; - }; - }; - }; - github_webhook_webhooks_github_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - [key: string]: string; - }; - }; - }; }; - }; } From 9057d39df6af8e1cb826429afb1415891ba9621c Mon Sep 17 00:00:00 2001 From: SoundMindsAI Date: Tue, 2 Jun 2026 23:35:33 -0400 Subject: [PATCH 09/10] docs(state): note infra_generated_artifact_freshness_gate in-flight Adds the merge one-liner to "Last 5 merges" (drops the now-6th entry to state_history.md's pointer); flips the "Current branch / execution context" section to the new feature branch + 8 commits; updates the "In flight" + "Plan-stage" sections. state.md size: 24,725 bytes (60KB cap). Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: SoundMindsAI --- state.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/state.md b/state.md index fc5c5c03..04fd94fb 100644 --- a/state.md +++ b/state.md @@ -16,8 +16,8 @@ MVP1 (v0.1) **shipped** — all six differentiators live (Bayesian/TPE optimizer ## Current branch / execution context -- **Branch:** `main` (PR #430 `chore_scorecard_pin_deps_postcss` just merged). All `pr.yml` checks green (smoke skipped — opt-in/off). -- **Active feature:** _None in flight._ `chore_scorecard_pin_deps_postcss` shipped 2026-06-03 (PR #430). One idea-stage chore filed but unmerged: `chore_pr_yml_parallelize_backend_job` (PR #427, docs-only — split the 8m20s backend CI job into parallel lanes). Next: pull from the MVP2 Idea/Plan backlog (run `/pipeline status`). +- **Branch:** `infra_generated_artifact_freshness_gate` (8 commits ahead of main, PR forthcoming). `pr.yml` not yet observed against the new branch — the new `generated-artifacts-fresh` job + `copy-docs-freshness.yml` workflow will fire on first push. +- **Active feature:** `infra_generated_artifact_freshness_gate` shipping both phases together (the standalone `infra_openapi_types_freshness_gate/` Phase-2-only record is retired at finalization). The latest pre-feature merge was `chore_scorecard_pin_deps_postcss` (PR #430, 2026-06-03). Next after this PR merges: pull from the MVP2 Idea/Plan backlog (run `/pipeline status`). - **Alembic head:** `0022_solr_engine_auth_check` (added by `infra_adapter_solr` Story A6 — extends `clusters.engine_type` + `clusters.auth_kind` CHECK constraints for Solr). - **Python:** 3.13. **Frontend stack:** Next 16 (App Router + Turbopack), React 19, Tailwind 4 (CSS-first), Vitest 4, ESLint 9 (flat), TypeScript 6, Playwright (chromium, single worker) for E2E. - **Coverage gates:** backend 80% (`fail_under` in pyproject), UI vitest + tsc + ESLint + Next build, plus a full-stack smoke E2E job. Live pass counts: see the latest `pr.yml` run (the historical per-feature counts moved to `state_history.md`). @@ -26,17 +26,17 @@ MVP1 (v0.1) **shipped** — all six differentiators live (Bayesian/TPE optimizer Detail + reasoning for each is in [`state_history.md`](state_history.md). +- **2026-06-03** — `infra_generated_artifact_freshness_gate` (PR forthcoming). Both phases shipped together: Phase 1 (`copy-docs` freshness gate) + Phase 2 (offline OpenAPI exporter + `openapi.json` snapshot + `types.ts` gate + chained fix). The standalone `infra_openapi_types_freshness_gate/` folder (the discoverable-record-if-Phase-2-ships-alone) is retired at finalization. **Phase 1:** `copy-docs.mjs` now prunes `ui/public/docs/` to `{README.md} ∪ {DOCS[].dest}` (FR-9, so a renamed entry never leaves a stale public copy); new `.github/workflows/copy-docs-freshness.yml` runs on every PR with no `paths-ignore` filter (FR-3 escape from pr.yml's `docs/**` filter so docs-only PRs still get the check). **Phase 2:** `backend/app/openapi_export.py` emits the canonical OpenAPI schema offline (no live DB/Redis/ES/OpenSearch/Solr/OpenAI — verified by `test_openapi_export.py` running against deliberately-unreachable REDIS_URL); `ui/openapi.json` (149KB, 52 paths) committed as the canonical snapshot; `gen-types.mjs` refactored to use the lockfile-pinned `node_modules/.bin/openapi-typescript` (no `npx` fallback) with a source-invariant banner extracted to the pure module `ui/scripts/gen-types-banner.mjs`; new `pr.yml` job `generated-artifacts-fresh` runs the snapshot + types guards + an AC-7 clean-tree determinism step that proves the regenerator is itself deterministic across runs. Single chained fix command: `bash scripts/regen-generated-artifacts.sh`. New `ui/.prettierignore` lists `src/lib/types.ts` + `public/docs/*.md` — the generator is the source of truth, prettier on them would make the gates flap. Tangential inline fix (per CLAUDE.md tangential-discoveries rule): `studies-table-ceiling-badge.test.tsx` fixture was missing `trial_count: 0`; the regen of types.ts surfaced the schema/test drift. 48 new test cases total (10 backend unit + 11 + 6 vitest + 7×3 shell-guard self-tests). No migration (head stays `0022`). Cross-model: Epic 1 GPT-5.5 phase-gate 3 findings (1 accepted-and-fixed, 2 rejected with cited counter-evidence); Epic 2 GPT-5.5 phase-gate 5 findings (all 5 rejected — 2 false positives from the slim-diff input, 2 plan-authorized override patterns, 1 inline-fix-per-CLAUDE.md guidance). - **2026-06-03** — `chore_scorecard_pin_deps_postcss` (PR #430). Resolved the actionable OSSF Scorecard findings on the public code-scanning surface — the one real vulnerability + the ~60 `PinnedDependencies` alerts. **Vulnerability #72:** `postcss < 8.5.10` (moderate XSS via unescaped `` in CSS stringify) was transitive — `next@16.2.6` hard-pins `postcss@8.4.31`; added a pnpm `overrides` (`postcss@<8.5.10` → `^8.5.15`) so the whole tree (incl. Next's bundled copy) resolves to 8.5.15, regenerated `ui/pnpm-lock.yaml`, verified `pnpm build` + 1008 vitest green. **PinnedDependencies:** pinned all 56 GitHub Action `uses:` refs to 40-char commit SHAs (`# vX` comments) across all 5 workflows; pinned the 4 `pr.yml` service-container images (postgres/redis/elasticsearch/opensearch) by manifest digest; pinned the Dockerfile base images by digest via single `BASE_IMAGE` ARGs (`python:3.14-slim` in `Dockerfile` — collapsed from the original split `PYTHON_VERSION`/`PYTHON_DIGEST` after Gemini flagged the digest-wins-over-tag override footgun; `node:26-bookworm-slim` declared once + reused by the 3 `ui/Dockerfile` stages). Dependabot already runs github-actions + docker weekly so the pins stay fresh. **Left intentionally:** npmCommand (`npm install -g pnpm@9`) + pipCommand (docs-site `pip install`) — impractical to hash-pin, not "images"; workflow `services.*.image` digests need manual refresh (Dependabot's github-actions ecosystem updates `uses:` only); Tier-3 intrinsic findings (relaxed branch protection, solo-dev review ratio, project age, fuzzing, OpenSSF badge, SAST). No `backend/app/` source, no migration (head stays `0022`). Cross-model: Gemini 2 findings (both accepted + fixed — the `BASE_IMAGE` consolidations above), each re-validated with `docker buildx build --check`. Both `docker buildx` CI jobs green on the final commit. - **2026-06-02** — `bug_llm_capability_cache_no_refresh` (PR #426, squash-merged `432dcf59`). The OpenAI capability check ran exactly once at api startup (`main.py:94`, fire-and-forget lifespan task) + cached in Redis with a 24h TTL (`capability_check.py:48`); nothing repopulated it, so any stack up >24h silently lost all LLM-dependent capability — `POST /judgments/generate` returned `503 LLM_PROVIDER_INCAPABLE "cache miss"` until an api restart. Confirmed live at 34h uptime (zero `openai:capabilities:*` keys; `docker compose restart api` fixed it). **Fix (Option A, locked at preflight D-1):** new `read_or_recompute_capability_result()` helper reads the cache, recomputes inline via `check_capabilities()` on miss (writes back), returns `None` on empty key (preserves the `/healthz` "no key" semantic). `agent_judgments_dispatch._check_llm_preflight` opts in; `/healthz` (200ms SLO, Rule #11) + chat orchestrator stay read-only (D-5). A per-worker `asyncio.Lock` single-flight + in-lock double-checked read collapses concurrent in-worker recompute bursts to 1 probe (D-4, refined after GPT-5.5 caught the original "WEB_CONCURRENCY × probes" bound undercounting concurrent requests); defensive try/except returns `None` on unexpected failure (→ caller's existing 503 envelope, not a bare 500). Options B (background refresh) + C (stale-but-usable) rejected (D-2/D-3). Shipped via `/bug-fix --ship` → `/impl-execute --ad-hoc`. No `backend/app/` source beyond the helper + call-site swap, no migration (head stays `0022`). 7 unit tests (`TestReadOrRecomputeCapabilityResult`) + 1 integration test (`test_generate_recovers_after_capability_cache_expiry`); test-fixture monkeypatch sites updated to the new symbol. 2194 unit pass, 330 contract pass. Cross-model: Gemini 4 (1 accepted — `api_key: str | None`; 3 rejected as hunk-isolated false positives on `AsyncMock.assert_not_awaited`, stdlib since 3.8), GPT-5.5 final 2 (both accepted — the asyncio.Lock single-flight + the exception wrapper, each with a new regression test). Ride-along: `/idea-preflight` SKILL.md routing fix (no longer hard-codes `/pipeline --auto` — routes to `/bug-fix`/`/impl-execute --ad-hoc` by prefix+scope). All 12 `pr.yml` checks green. - **2026-06-02** — `infra_smoke_reseed_runtime_budget` (PR #424, squash-merged `035d7941`). Clears the last of the three-PR Solr-CI debt chain (`infra_solr_ci_readiness` backend half → `infra_solr_smoke_stability` Solr boot → this, the reseed-runtime half). The smoke job's `demo-ubi.spec.ts` `beforeAll` reseed exceeded the 25-min `timeout-minutes` cap once Solr actually booted (AC-8 of `feat_demo_ubi_study_comparison` bounds the in-flight reseed at 1140s/~19 min hard ceiling, ~28 min worst case per §14 — Playwright + setup overhead pushed total past 25 min; PR #383 run 26790636716 hit it at 25:18). **Fix (Option A, locked at idea-preflight):** extend `ui/playwright.config.ts`'s `testIgnore` CI-gated branch by one entry (`'**/demo-ubi.spec.ts'`, the 7th alongside the 6 pre-existing demo-data-dependent specs) — the `process.env.CI ? [...] : []` ternary gates it to GHA runs, so local `make up` smoke (`CI=` unset) keeps full demo-ubi coverage. Option B (timeout bump → 35 min) rejected (D-3: <7 min margin against §14 worst case); Option C (env-var reseed scenario filter, ~2-3h multi-file) deferred per operator (D-2). New vitest regression guard `ui/src/__tests__/playwright-config-test-ignore.test.ts` (3 assertions: demo-ubi in CI branch, all 7 entries present, demo-ubi not outside the ternary). Runbook `docs/03_runbooks/smoke-solr-stability.md` §5 documents the exclusion + the reseed-runtime-vs-Solr-stability split; pr.yml + state.md stale "exceeds the cap" framing refreshed to "runtime block cleared, flip `SMOKE_TEST=true` after the §16 `playwright test --list` verification". 5 stories / 1 epic. No `backend/app/` source, no migration (head stays `0022`). §16 manual verification confirmed AC-1 (`CI=true` → 86 tests/30 files, 0 demo-ubi) + AC-2 (`CI=` unset → 110 tests/37 files, demo-ubi discovered). Cross-model: spec GPT-5.5 3 cycles (13 findings, all applied), plan GPT-5.5 3 cycles (11 findings, all applied), Gemini 2 (both accepted — `import.meta.url` path resolution + CRLF normalization), GPT-5.5 final 3 (2 accepted: §4→§5 pointer + runbook markdown links; 1 rejected: AC-7 file-shape re-raise, counter-evidence cited). All 12 `pr.yml` checks green. - **2026-06-02** — `feat_studies_convergence_visibility` (Epic 1 via PR #421 `e5c3b8b9`; Epic 2 via PR #422 `49a0e1b0`). **Epic 1** — studies-list convergence visibility: `GET /api/v1/studies` items gain `trial_count` (non-baseline total) + `convergence_verdict` (reuses the shipped `classify_convergence` via a count-gated path; bounded to 1–2 queries/page via `count_trials_for_studies` + `resolve_list_convergence_verdicts`); `/studies` UI gains Trials + Convergence columns reusing `CONVERGENCE_VERDICT_VALUES` + the `convergence_verdict` glossary key. (Epic 1 landed bundled inside the PR #421 squash-merge alongside `complementary-architecture.md` — surfaced during the Epic 2 CI watch; the Epic 2 branch was rebased onto `e5c3b8b9` to drop the duplicate Epic 1 commits.) **Epic 2** — demo data that shows real optimization: rewrote the 5 small `SCENARIOS` with the decoy-by-title pattern (best-answer terms in description/body/bullets, decoy terms in title) so the equal-midpoint baseline under-ranks and a differentiated boost lifts ≥ 0.10 (per-scenario headroom: baseline 0.561–0.690, lift +0.230 to +0.295, all `best < 0.99`); bumped small-scenario `max_trials` 12 → 50 via the new shared `DEMO_SMALL_STUDY_MAX_TRIALS` constant single-sourced from `scripts/seed_meaningful_demos.py` (imported by `demo_seeding.py` so CLI + home-button reseed can't drift) so demo studies clear `STUDIES_TPE_WARMUP_FLOOR` and convergence reads `converged`/`still_improving` instead of a uniform `too_few_trials`. New tests: engine-backed headroom (6 — 5 scenarios + resolver-parity guard; ES/OS hard-gated in CI via `_require_es_or_fail`, Solr skip-gated per D-18); shape invariants (21 — full {0,1,2,3} rubric per query); max_trials single-source guards (4); heavy-lane AC-7/AC-8 block reading persisted `Study.baseline_metric`/`best_metric` via the live list path. Tangential inline fix: `/healthz` contract test now accepts the `solr` subsystem the live response carries (live since 2026-05-31). No migration (head stays `0022`). Cross-model: Epic 2 phase-gate GPT-5.5 cycle 1 (6 findings — 4 accepted+fixed, 1 accepted-as-comment, 1 deferred to docs), cycle 2 clean; final GPT-5.5 review (2 findings — both rejected: Solr-CLI scope is `infra_adapter_solr` Story A13 territory, header-tooltip UX matches the sibling-column convention); Gemini (2 pre-rebase findings on Epic 1 code — moot after rebase). All 12 `pr.yml` checks green. -- **2026-06-02** — `bug/cli-seed-ubi-missing-engine-type` (PR #419, squash-merged `5a6f9d75`). Two Solr-drift bugs in the `scripts/seed_meaningful_demos.py` CLI seed path, surfaced live during a `make seed-demo` run once the Solr container was reachable: (1) `_async_seed_synthetic_ubi` omitted the required `engine_type` kwarg on `ensure_ubi_indices()` + `seed_synthetic_ubi()` (added by `infra_adapter_solr`; service path was updated, CLI wrapper drifted) → `TypeError` killed every UBI scenario; (2) `apply_study_renames` + the `seed complete` summary dereferenced `study_name`/`study_id` on the study-less Solr-minimum result dict → `KeyError`. Fixed both + 2 regression tests in `backend/tests/unit/scripts/`; verified end-to-end (full `make seed-demo FORCE=1` completes all 5 scenarios + rich ESCI). Gemini: 1 finding (guard rename on both study keys) accepted. Also captured P1 idea `bug_llm_capability_cache_no_refresh` (the original symptom — see Known debt). No `backend/app/` source, no migration (head stays `0022`). -_(older entries — full narrative in [`state_history.md`](state_history.md): `chore_template_library_expansion` PR #416, `infra_smoke_reseed_runtime_budget` PR #424, `infra_solr_smoke_stability` PR #383, `infra_solr_ci_readiness` Phase 1 PR #367, MVP2 backlog batch PR #364, `feat_study_convergence_indicator` PR #352, `feat_overnight_autopilot` PR #343, `infra_adapter_solr` PR #336, …)_ +_(older entries — full narrative in [`state_history.md`](state_history.md): `bug/cli-seed-ubi-missing-engine-type` PR #419, `chore_template_library_expansion` PR #416, `infra_smoke_reseed_runtime_budget` PR #424, `infra_solr_smoke_stability` PR #383, `infra_solr_ci_readiness` Phase 1 PR #367, MVP2 backlog batch PR #364, `feat_study_convergence_indicator` PR #352, `feat_overnight_autopilot` PR #343, `infra_adapter_solr` PR #336, …)_ ## In flight -- _None._ `infra_smoke_reseed_runtime_budget` (PR #424) merged 2026-06-02 — the Solr-CI debt chain is now fully drained (backend + Solr-boot + reseed-runtime all shipped). The `smoke` job stays OFF by default (operator preference); flip `SMOKE_TEST=true` after the §16 `playwright test --list` verification to restore per-PR smoke signal. -- **Plan-stage, `/impl-execute`-ready (no gates):** the 4 remaining PR #413 (2026-06-02) spec/plan pairs in `02_mvp2/` (`chore_template_library_expansion` shipped via PR #416): `chore_studies_post_arq_spy_fixture`, `bug_judgment_header_omits_click_bucket`, `bug_baseline_phase_test_isolation`, `chore_ubi_reader_search_after_pagination`. Plus the 6 pairs from PR #364 — of which two are **design-ahead** (`feat_apply_path_normalizer_declaration` + `feat_query_normalizer_typed_pipeline`, both gated on `feat_query_normalization_tuning` Phase 1 merging — do not `/impl-execute` until then); the other four (`feat_overnight_studies_summary_card`, `infra_generated_artifact_freshness_gate`, `chore_arq_pool_aclose_deprecation`, `chore_cluster_detail_rung_badge`) are ungated. +- **`infra_generated_artifact_freshness_gate`** — branch ready, PR forthcoming. 8 commits (Stories 1.1, 1.2, 1.2-fix, 2.1, 2.2 a, 2.2 b, 2.3, 2.4). 48 new test cases. AC-7 clean-tree determinism verified locally. +- **Plan-stage, `/impl-execute`-ready (no gates):** the 4 remaining PR #413 (2026-06-02) spec/plan pairs in `02_mvp2/` (`chore_template_library_expansion` shipped via PR #416): `chore_studies_post_arq_spy_fixture`, `bug_judgment_header_omits_click_bucket`, `bug_baseline_phase_test_isolation`, `chore_ubi_reader_search_after_pagination`. Plus the 5 pairs from PR #364 still pending after this PR ships — of which two are **design-ahead** (`feat_apply_path_normalizer_declaration` + `feat_query_normalizer_typed_pipeline`, both gated on `feat_query_normalization_tuning` Phase 1 merging — do not `/impl-execute` until then); the other three (`feat_overnight_studies_summary_card`, `chore_arq_pool_aclose_deprecation`, `chore_cluster_detail_rung_badge`) are ungated. ## Queued (priority-ordered by dashboard / dep graph) From b0579fb757db3dc65a4fe23d025ba1a952a2ff2a Mon Sep 17 00:00:00 2001 From: SoundMindsAI Date: Tue, 2 Jun 2026 23:42:33 -0400 Subject: [PATCH 10/10] fix(openapi-export): adjudicate Gemini Code Assist review (3 accepts) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #433 Gemini Code Assist review surfaced three medium-severity resource-hygiene findings, all accepted: 1. backend/app/openapi_export.py:91 — register atexit cleanup for the dummy *_FILE tmpdir created by _ensure_dummy_settings_env(). Each invocation leaked ~100 bytes; not a real disk concern but sloppy. atexit.register(shutil.rmtree, ..., ignore_errors=True) is the stdlib pattern. 2. backend/app/openapi_export.py:_write_atomic — wrap the NamedTemporaryFile(delete=False) + os.replace flow in try/finally. If write/flush/fsync OR the rename raised (disk full, permission denied), the orphan `...tmp` would persist next to the destination. tmp_path = None after a successful replace tells the finally block "the rename took ownership; don't try to delete the now-renamed file". The finally's unlink is best-effort (missing_ok=True + caught OSError) so it never masks the original exception. 3. ui/scripts/gen-types.mjs:execFileSync — add `shell: process.platform === 'win32'` so Node can invoke the openapi-typescript.cmd shim on Windows (cmd.exe is required to interpret batch files; per the Node child_process docs: https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files). POSIX stays shell-free. Each fix carries an inline citation back to the Gemini finding so a future archeologist can trace the rationale. Verification: 10/10 unit tests still passing; live snapshot + types guards still emit OK on a clean tree; rtk mypy --strict + ruff clean on the modified Python; rtk prettier clean on gen-types.mjs. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: SoundMindsAI --- backend/app/openapi_export.py | 55 ++++++++++++++++++++++++++--------- ui/scripts/gen-types.mjs | 14 +++++++-- 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/backend/app/openapi_export.py b/backend/app/openapi_export.py index 92f30dfa..8bc419b5 100644 --- a/backend/app/openapi_export.py +++ b/backend/app/openapi_export.py @@ -58,8 +58,10 @@ from __future__ import annotations import argparse +import atexit import json import os +import shutil import sys import tempfile from pathlib import Path @@ -88,6 +90,13 @@ def _ensure_dummy_settings_env() -> None: return tmp_dir = Path(tempfile.mkdtemp(prefix="relyloop-openapi-export-")) + # Clean up the dummy-secrets dir at process exit (Gemini Code Assist + # review finding #1, PR #433). The directory holds <100 bytes of + # placeholder content, so the leak is small — but accumulating one + # per CLI invocation is sloppy. The env-var publish below is purely + # diagnostic and the contract documents it as such; cleanup at exit + # is compatible. + atexit.register(shutil.rmtree, tmp_dir, ignore_errors=True) os.environ.setdefault("RELYLOOP_OPENAPI_EXPORT_TMP", str(tmp_dir)) if not os.environ.get("DATABASE_URL_FILE"): @@ -163,20 +172,38 @@ def _write_atomic(path: Path, content: str) -> None: ``os.replace`` is a same-filesystem rename (atomic on POSIX/NTFS). """ path.parent.mkdir(parents=True, exist_ok=True) - # ``delete=False`` because os.replace handles the rename + cleanup. - with tempfile.NamedTemporaryFile( - mode="w", - encoding="utf-8", - dir=str(path.parent), - prefix=f".{path.name}.", - suffix=".tmp", - delete=False, - ) as tmp: - tmp.write(content) - tmp.flush() - os.fsync(tmp.fileno()) - tmp_path = Path(tmp.name) - os.replace(tmp_path, path) + # ``delete=False`` because os.replace handles the rename. If anything + # between the NamedTemporaryFile context and the successful + # ``os.replace`` raises (write/flush/fsync error, disk full, + # permission denied on the rename), the orphan ``.tmp`` would + # otherwise persist next to ``path`` — see Gemini Code Assist review + # finding #2 on PR #433. ``tmp_path = None`` after a successful + # replace tells the finally block "the rename took ownership, don't + # try to delete the now-renamed file". + tmp_path: Path | None = None + try: + with tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + dir=str(path.parent), + prefix=f".{path.name}.", + suffix=".tmp", + delete=False, + ) as tmp: + tmp.write(content) + tmp.flush() + os.fsync(tmp.fileno()) + tmp_path = Path(tmp.name) + os.replace(tmp_path, path) + tmp_path = None + finally: + if tmp_path is not None: + try: + tmp_path.unlink(missing_ok=True) + except OSError: + # Best-effort cleanup; never raise from a finally clause + # masking the original exception. + pass def main(argv: list[str] | None = None) -> int: diff --git a/ui/scripts/gen-types.mjs b/ui/scripts/gen-types.mjs index 8f2d2c95..28280f59 100644 --- a/ui/scripts/gen-types.mjs +++ b/ui/scripts/gen-types.mjs @@ -92,13 +92,21 @@ function resolvePinnedBinary() { */ function generate() { const bin = resolvePinnedBinary(); - // execFileSync (no shell) — SOURCE_URL comes from OPENAPI_URL env var, - // and a shell-interpolated command would let a crafted value inject. - // Array argv is shell-free. + // execFileSync (no shell on POSIX) — SOURCE_URL comes from + // OPENAPI_URL env var, and a shell-interpolated command would let a + // crafted value inject. Array argv is shell-free. + // + // On Windows the pinned binary is a `.cmd` shim, which Node's + // execFileSync cannot invoke without `shell: true` (Windows requires + // cmd.exe to interpret batch files; see + // https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files). + // We gate `shell: true` to win32 only so POSIX stays shell-free + // (Gemini Code Assist review finding #3 on PR #433). console.log(`Generating ${OUTPUT} from ${SOURCE_URL}…`); execFileSync(bin, [SOURCE_URL, '-o', OUTPUT], { stdio: 'inherit', cwd: UI_ROOT, + shell: process.platform === 'win32', }); const generated = readFileSync(OUTPUT, 'utf8');