feat(FAR-775): implement /api/v1/variant-batches backend API for variant comparison UI - #347
Conversation
farnalabs
commented
Sep 10, 2026
- feat(FAR-775): variant-batches API — list, detail, soft-delete, re-fire
- test(FAR-775): use pytest.approx for non-representable float asserts
- fix(FAR-775): QA fixes — RLS context, re-fire hard-fail, guardrail filter, pagination/type/API-shape corrections
- fix(FAR-775): legacy batch total excludes all state-table batch ids, not just the current page
Backend routes matching the TypeScript contract in variantBatches.ts: - GET /api/v1/variant-batches — list (state-table + legacy synthesis) - GET /api/v1/variant-batches/:id — detail (runs + evals + node outputs) - DEL /api/v1/variant-batches/:id — soft-delete (sets deleted_at) - POST /api/v1/variant-batches/:id/re-fire — re-fires from frozen variant_group snapshot Adds variant_batch_state table (migration 0208) for batch name/pipeline_id/ variant_group_id/input_payload, created_at timestamps, and soft-delete support. Legacy batches (pre-state-table) are synthesised from the runs table so the API surface is complete from day one. run_variant_batch stamps a state row after generating the batch_id so all future batches are tracked. 13 unit tests covering _compute_batch_status and _run_to_variant_run helpers.
Architecture test-style suite flagged precision-fragile float comparisons (0.8, 0.05) in test_variant_batches.py. Switch to pytest.approx and add type args to the snapshot hint. Adds pytest import back.
…lter, pagination/type/API-shape corrections Apply 13 QA findings from the 5-lens review of the variant-batches API: Critical: - C1: Add RLS org+user context in every handler's transaction block - C2: Re-fire stale-batch extraction hard-fails with 502 instead of falling back to old batch_id Major: - M1: Batch-load eval results via get_evals_for_runs instead of untyped getattr(run, '_eval_results') - M2: Apply non_guardrail_eval_results_clause() to batch eval stats query - M3: Wrap handlers with @handle_db_errors decorator (replaces try/except) - M4: Type principal as TenantPrincipal (from modulo.auth.jwt) - M5: Validate pagination with ge=1 and le=100 constraints - M6: Legacy batch count is true total (states + legacy distinct batch_ids) - M7: Batch-load runs for all page batch_ids in ONE query (no N+1) - M8: HTTPException propagates naturally via decorator - M9: Re-fire org defense-in-depth with explicit org predicate on group - M10: Cross-tenant isolation tests for get/delete/re-fire - M11: Register VariantBatchState in models/__init__.py; fix defunct mid-file noqa import in crud/variant_group.py Deleted: - Remove _RUN_STATUS_MAP pure identity passthrough map Also: - Add list_batch_runs_for_batch_ids to crud/variant_group.py for M7 - Update _run_to_variant_run to accept eval_results parameter - Fix unused imports flagged by ruff
…not just the current page
farnalabs
left a comment
There was a problem hiding this comment.
CI is failing on this commit (BDD (full suite): failure, Lint (Backend): failure, Check migration heads: failure, Test (Backend): failure, Schema freshness: failure). Please fix the failing checks before this PR can be approved.
modulo-reviewbot
left a comment
There was a problem hiding this comment.
CHANGES_REQUESTED — PR #347 (head SHA 7f892d9)
The automated review pipeline requires changes before this PR can merge. CI is failing on the head commit with 5 failing checks:
- BDD (full suite): failure
- Lint (Backend): failure
- Check migration heads: failure
- Test (Backend): failure
- Schema freshness: failure
These blocking findings must be resolved (fix the underlying failures so all checks pass) before the PR is approved for merge.
modulo-reviewbot
left a comment
There was a problem hiding this comment.
CHANGES_REQUESTED — PR #347 (head SHA 7f892d9)
The automated review pipeline requires changes before this PR can merge. CI is failing on the head commit with 5 failing checks:
- BDD (full suite): failure
- Lint (Backend): failure
- Check migration heads: failure
- Test (Backend): failure
- Schema freshness: failure
These blocking findings must be resolved (fix the underlying failures so all checks pass) before the PR is approved for merge.
…in head Renumber 0208_variant_batch_state -> 0209_variant_batch_state so the Alembic graph has a single head once merged with main (which already has 0208_notification_indexes_and_constraint as its head). Set down_revision to the real main head. Also move inline route-file imports to module level (Semgrep rule) and regenerate frontend/src/lib/api/schema.ts (stale after new routes).
Automated fix: migration collision + lint + stale schemaThis commit resolves the failing CI on this branch (c759654). 1. Migration collision (root cause of 2. Lint (Backend) — Semgrep inline-import rule 3. Schema freshness All pre-commit hooks pass (ruff, ruff-format, bandit, semgrep, gitleaks, import-linter, eslint, etc.). Note (out of scope): |
The migration 0208_variant_batch_state was renumbered to 0209_variant_batch_state (chained off the real main head 0208_notification_indexes_and_constraint) to resolve the Alembic two-heads collision. These head/chain-pinning tests still asserted the stale 0207_collection_install_tracking head, so update them to the new single head 0209_variant_batch_state (including the 0207->0208_notification-> 0209_variant chain in test_eval_suite_run).
Automated fix — migration collision + stale head/CI failures (FAR-775)This PR's branch was red because of a migration-head collision plus stale head-pinning tests: Root cause
What was fixed (two commits)
Verified locally: |
The concurrent Branch Fixer renumber (0208->0209, head-pinning tests, schema.ts regen, route inline-imports) landed first; this adds the three gaps it missed: - Add variant_batch_state to _POST_0194_TABLES so the frozen 0194 uuid-PK coverage test stays at its expected count (new table owns its own PK). - Add variant_batch_state to the test_schema required-tables set. - Declare _ORG_SCOPED_TABLES in the 0209 migration so the RLS-coverage drift test detects the table's ENABLE ROW LEVEL SECURITY migration (emitted via an f-string the test's literal regex intentionally skips).
Automated fix: migration-collision + cascading test gaps (PR #347)Root cause: The PR added What was fixed (commit 60b4c3e):
All affected unit tests verified green locally (single head, uuid-PK count, RLS coverage, schema required-tables, and the head/chain-pinning assertions). Note: The separate |
farnalabs
left a comment
There was a problem hiding this comment.
CI is failing and the PR has merge conflicts with main — the root cause appears to be the new migration 0209_variant_batch_state.
Test (Backend)failure: the branch tree does not contain migration0208_notification_indexes_and_constraint(it exists on origin/main), so0209_variant_batch_state'sdown_revisiondangles and the new migration shows up as an orphaned/extra head. Failing guards: test_eval_suite_run::test_single_migration_head, test_trigger_streak_engine, test_migration_guardrail_kill_switch / test_migration_guardrail_trust_pr_b, test_migration_ongoing_trigger, test_migration_reconcile_staging_schema, test_migration_sync_feature_flag_catalog, test_trigger_event_vocabulary (all "expected a single head, got ['0209_variant_batch_state']").- Related drift failures: test_migration_0194_uuid_pk_server_defaults (metadata uuid-PK count drifted: 85), test_rls_coverage::test_every_org_scoped_table_has_rls_policy (variant_batch_state missing ENABLE ROW LEVEL SECURITY + rls_org_isolation policy per the guardrail message), test_schema::test_initial_schema_contains_required_tables.
- Suggested fix: merge/rebase origin/main so 0208 is in the tree and 0209 chains onto it, add the RLS ceremony for variant_batch_state in the shape the guardrail expects, and regenerate the schema fixtures the drift tests compare against. That should also resolve the merge conflicts with main.
(Feedback only; formal decision follows the standard review pipeline.)
modulo-reviewbot
left a comment
There was a problem hiding this comment.
CHANGES REQUESTED — PR #347 (g-far-775-variant-batches-api)
CI check Test (Backend) is failing on head c759654cdd62627a3b0843e492a2c3faf51eeeef and the PR is not mergeable (mergeable=false, merge conflicts with main).
Blocking findings:
-
Orphaned migration head —
0209_variant_batch_state.pysetsdown_revisionto0208_notification_indexes_and_constraint, which exists only onorigin/mainand not in this branch tree. This surfaces 0209 as an extra/orphaned head and breaks single-head guardrails:test_single_migration_head,test_trigger_streak_engine,test_migration_guardrail_kill_switch,test_migration_guardrail_trust_pr_b,test_migration_ongoing_trigger,test_migration_reconcile_staging_schema,test_migration_sync_feature_flag_catalog,test_trigger_event_vocabulary, plus drift checkstest_migration_0194_uuid_pk_server_defaults(uuid-PK count drifted to 85) andtest_schema::test_initial_schema_contains_required_tables. Fix: rebase/mergeorigin/mainso 0209 chains onto 0208. -
Missing RLS on
variant_batch_state—test_rls_coverage::test_every_org_scoped_table_has_rls_policyfails. The guardrail requiresENABLE ROW LEVEL SECURITY+CREATE POLICY rls_org_isolationfor the new table in the shape it scans (see0002_rls_policies.py/0088_rls_missing_policies.pyprecedent); the migration's own ceremony does not satisfy the guardrail scan. -
Merge conflicts with main — likely the root cause of the missing 0208 migration file. Resolving the merge and regenerating schema fixtures should fix both the conflicts and most CI failures.
Re-request review once these are addressed.
…ins correctly The branch renumbered its variant_batch_state migration to 0209 to avoid colliding with main's head, but set down_revision to 0208_notification_indexes_and_constraint without actually containing that migration file. Alembic then failed to resolve the parent revision (KeyError), breaking the single-chain-head and RLS-coverage tests. Cherry-pick 0208 from main so the graph is ...0207 -> 0208 -> 0209 with a single head at 0209_variant_batch_state.
Branch Fixer: migration-chain break (FAR-775)Commit: a784840 — fix(FAR-775): bring main's 0208 migration into the branch so 0209 chains correctly Root causeThe branch renumbered its
FixCherry-picked main's VerificationLocally re-ran the three previously-failing tests + the full |
Root cause of the CI: Fast Validation failureThe failing run was on commit
FixBrought
Verified locally: |
…ches-api # Conflicts: # backend/tests/unit/core/test_trigger_streak_engine.py # backend/tests/unit/db/test_eval_suite_run.py # backend/tests/unit/db/test_migration_guardrail_kill_switch.py # backend/tests/unit/db/test_migration_guardrail_trust_pr_b.py # backend/tests/unit/db/test_migration_ongoing_trigger.py # backend/tests/unit/db/test_migration_reconcile_staging_schema.py # backend/tests/unit/db/test_migration_sync_feature_flag_catalog.py # backend/tests/unit/db/test_trigger_event_vocabulary.py
Automated Branch Fixer — merge-conflict resolution (PR #347)Root cause: The branch had diverged from What changed: Merged
Already satisfied on the branch (verified green locally): the Verification: Commit: |
farnalabs
left a comment
There was a problem hiding this comment.
Initial review passed (feedback only — formal decision comes from post-decision). Backend+contract round-trip looks consistent: list/detail/re-fire/delete response keys match frontend/src/lib/api/variantBatches.ts snake_case types, schema.ts regenerated. Minor: (1) batch detail loads node outputs with one blob query per run — per-batch N queries, consider a batch read if the store supports it; (2) unit tests mock the session/DB layer entirely — no integration/BDD test round-trips the real /api/v1/variant-batches payloads, so wire shape is only verified by the schema regeneration; (3) state rows with name=NULL render as empty string in summaries (upsert_batch_state defaults name=None) — frontend may show blank names for legacy/re-fired batches. Migration 0209 RLS ceremony and soft-delete isolation look correct, cross-org IDOR 404 tests included.
modulo-reviewbot
left a comment
There was a problem hiding this comment.
APPROVE — CI green on head c0f2d57 and PR is mergeable. Three-dot diff adds a variant_batch_state table (migration 0209 with correct RLS/migrate-role ceremony, FORCE RLS, org-isolation policy), variant-batches list/detail/soft-delete/re-fire REST endpoints with RLS org scoping and cross-org IDOR 404 tests, batch-loaded (N+1-free) eval stats/results/runs, a double-count fix for legacy vs state batches in pagination totals, and regenerated OpenAPI schema matching the frontend variantBatches.ts snake_case contract. Non-blocking feedback noted: per-run node-output blob queries could be batched; no integration test round-trips the real endpoint payload shape; state rows with name=NULL render as empty-string batch names in list summaries. Policy-router assessed the diff as low-risk (no auth/RLS/audit/pipeline/workflow paths touched).
…0209->0210) Merge origin/main into g-far-775-variant-batches-api and resolve the migration-graph collision: main landed 0209_collection_install_id_entity_columns, which collided with this branch's renumbered 0209_variant_batch_state. Renumber the FAR-775 migration to 0210_variant_batch_state and chain it off 0209_collection_install_id_entity_columns so the graph has a single head again. Sweep all head/chain-pinning test references to the new head.
Automated fix: merge conflict with
|
…ognitive complexity - Migration 0209 added the denormalised collection_install_id column via op.add_column, colliding with 0207 (merged to main) which already adds the same column via ADD COLUMN IF NOT EXISTS. Replaying the full chain on a fresh DB raised 'column collection_install_id of relation schemas already exists', failing BDD + break-glass boot. Make 0209's column/index adds idempotent (IF NOT EXISTS) so the chain is replayable. - Refactor list_batches / _load_batch_detail in variant_batches.py into small helpers to bring cognitive complexity under the SonarCloud 15 threshold (was 23 and 17).
Automated fix — resolves BDD / break-glass / SonarCloud CI failures (PR #347)Two independent root causes were failing CI on this branch: 1. Duplicate-column migration (BDD + break-glass boot) —
|
farnalabs
left a comment
There was a problem hiding this comment.
Review feedback (contract re-verified on head dc05072): Overall solid. Backend summary/detail keys match frontend/src/lib/api/variantBatches.ts types exactly, migration 0210 follows the RLS ceremony precedent, the page-2+ double-count fix has a regression test, and re-fire's batch_id extraction is provable from frozen_snapshot. Two minor notes (non-blocking): (1) legacy batches (no state row) cannot be soft-deleted and are silently absent from delete/fallback paths; (2) _load_run_blobs is a per-run N+1 on batch detail (acknowledged in-code but worth batching when sizes grow).
modulo-reviewbot
left a comment
There was a problem hiding this comment.
Approving PR #347 (g-far-775-variant-batches-api) at SHA dc05072.
Policy-router: low-risk — no high-risk path matches (changed files under api/routes, db/crud, db/models, migrations, frontend schema, tests; none under workflows/auth/RLS/HITL/audit/connectors/pipeline).
Review node findings (all passed):
- backend/src/modulo/db/migrations/versions/0210_variant_batch_state.py follows the 0066/0204/0207 RLS ceremony precedent (migrate-role ownership, FORCE RLS, org isolation policy, grants); head-chain tests updated consistently.
- 0209_collection_install_id_entity_columns.py idempotent ADD COLUMN IF NOT EXISTS fix is correct.
- FE/BE contract keys match the pre-existing frontend schema exactly; schema.ts regenerated and Schema freshness CI passed.
- Page-2+ double-count fix in the legacy scan has a regression test (TestListBatchesTotalCount); cross-tenant isolation covered.
- Re-fire batch_id extraction (C2) is proven — _fire_batch_variant always writes batch_id into frozen_snapshot.
- Permission codes variant.list/variant.delete/variant.run exist with appropriate role gates.
CI green on head (13 checks), mergeable=true. No blocking findings; feedback-only comments posted. Merge authorized.
Branch Fixer — merge-conflict resolvedRoot cause: Fix: Merged
|
Raise SonarCloud new-code coverage above the 80% gate (was 57.5%). Drives every handler/helper in variant_batches.py (list_batches legacy scan, get/delete/re-fire paths + error branches) and the FAR-775 variant_batch_state CRUD in variant_group.py with mocked sessions.
Automated fix: SonarCloud new-code coverage gate (57.5% < 80%)Root cause: The SonarCloud quality gate failed on Fix (
Result: Pre-commit hooks (ruff, ruff-format, bandit, gitleaks, et al.) all passed on the commit. |
farnalabs
left a comment
There was a problem hiding this comment.
CI is failing on the current head (edaf553):
- Test (Backend) — "Run architecture tests" step fails: https://github.com/farnalabs/modulo/actions/runs/34533759397/job/103060432602
Also, the PR is not mergeable: it has conflicts with the current main branch and needs a rebase/merge of origin/main (or Branch Fixer) before it can land.
Once the failing backend test job is green and merge conflicts are resolved, the PR will be re-reviewed.
modulo-reviewbot
left a comment
There was a problem hiding this comment.
Blocking findings
Not mergeable — merge conflicts with main
The PR has merge conflicts with main; it needs a rebase/merge of origin/main before it can be merged.
CI failure — Test (Backend)
The Test (Backend) check failed at the Run architecture tests step (run 34533759397). 13 of 14 required checks completed with 1 failure. All other checks passed (Integration tests, Schema freshness, Lint, Frontend/WCAG).
Verdict: changes requested. Merge is blocked until conflicts are resolved and CI is green on head edaf553f9ec970c1304913d21583b96d6acfce85.
…s coverage test
Replace '== []' / '== {}' assertions with 'assert not ...' to satisfy the
test-suite-quality architecture lint (test_no_empty_container_literal_equality).
Automated fix: empty-container literal equality in variant batches coverage testThe Test (Backend) CI job failed on the architecture lint
The lint intentionally skips bare-name operands (so the existing Commit: ce9b733 All pre-commit hooks (ruff, ruff-format, bandit, gitleaks, etc.) passed locally. CI should now be green on this check. |
…ve migration collision with 0210_community_gate - main merged 0210_community_gate (FAR-764) as the head off 0209; the PR's 0210_variant_batch_state collided on the same numeric prefix. - Renumbered the PR migration to 0211_variant_batch_state with down_revision = 0210_community_gate so the chain stays linear and single-headed. - Swept all head-chain assertions in the affected unit tests to the new head. - Resolved merge conflicts in test files (test_eval_suite_run, test_trigger_streak_engine, guardrail/ongoing/reconcile/staging/feature-flag/vocabulary tests).
Automated fix: merge-conflict + migration collisionThis branch had two problems blocking merge, both now resolved in commit 1. Migration number collision (root cause of the "2 heads" / architecture-test failure)
Fix: renumbered the PR's migration to 2. Merge conflictsMerged All pre-commit hooks (ruff, ruff-format, bandit, semgrep, gitleaks, import-linter, eslint, merge-conflict) pass. Re-running CI should now go green. |
modulo-reviewbot
left a comment
There was a problem hiding this comment.
Review Decision: Changes Requested
Blocking finding from the review node:
- CI check
Test (Backend)is failing on head SHAb2c404cd01b4c52b7f0e5213d6e2a755915a9755. The backend test suite must pass before this PR can merge.
Notes:
- Mergeability checks pass; no other checks are failing.
- Risk classification: low-risk (no high-risk-path match).
Please fix the backend test failure, re-run CI, and re-request review.
…survives The migration guard test defined _POST_0194_TABLES twice (a latent duplicate inherited from main); the PR's edit only updated the first copy, so the second definition overrode it and left variant_batch_state counted in the frozen 0194 uuid-PK coverage set (85 vs expected 84). Remove the duplicate so the corrected exclusion takes effect.
Automated fix: migration guard test (
|
farnalabs
left a comment
There was a problem hiding this comment.
Re-review on head 40f963a (FAR-775): CI is green on the head commit (Test (Backend), BDD, Integration, Schema freshness, Lint all passed — the previously failing _POST_0194_TABLES duplicate is now deduped so the variant_batch_state exclusion survives), the PR is mergeable, and the head chain 0211_variant_batch_state -> 0210_community_gate is intact. Backend contract keys in variant_batches.py match frontend/src/lib/api/variantBatches.ts snake_case types exactly and schema.ts is regenerated. RLS ceremony (migrate-role ownership, FORCE RLS, org-isolation policy) and soft-delete/IDOR handling confirmed correct. Non-blocking notes carried over from prior heads: (1) _load_run_blobs does one blob query per run on batch detail; (2) no integration/BDD test round-trips the real /api/v1/variant-batches payload shape; (3) legacy (pre-state-row) batches cannot be soft-deleted. Verdict: recommend APPROVE — feedback only, formal decision comes from post-decision.
modulo-reviewbot
left a comment
There was a problem hiding this comment.
Approved on head 40f963a (CI green, mergeable=true).
Key findings from the review node:
- New variant_batch_state table (migration 0211) follows the 0066/0204/0207 RLS ceremony precedent (migrate-role ownership, FORCE RLS, rls_org_isolation, app/system grants); chained correctly on 0210_community_gate.
- List/detail/soft-delete/re-fire endpoints verified: RLS org scoping, permission codes, cross-org IDOR 404s, and re-fire batch_id extraction all correct; response keys match frontend variantBatches.ts snake_case contract.
- Batch-loaded eval stats/results are N+1-free; page-2+ legacy double-count regression fixed with unit coverage (incl. IDOR tests).
- Fix commit dedupes the duplicate _POST_0194_TABLES definition, resolving the prior Test (Backend) failure; head is green.
- OpenAPI schema.ts regenerated for the three new operations.
Policy-router: low-risk (no changed file matches high-risk globs). Non-blocking notes carried: _load_run_blobs per-run N+1 on batch detail, legacy (no state row) batches cannot be soft-deleted, and no integration/BDD round-trip test of the real endpoint payload.
|


