Skip to content

feat(FAR-775): implement /api/v1/variant-batches backend API for variant comparison UI - #347

Merged
github-actions[bot] merged 16 commits into
mainfrom
g-far-775-variant-batches-api
Sep 11, 2026
Merged

feat(FAR-775): implement /api/v1/variant-batches backend API for variant comparison UI#347
github-actions[bot] merged 16 commits into
mainfrom
g-far-775-variant-batches-api

Conversation

@farnalabs

Copy link
Copy Markdown
Owner
  • 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

Duncan Tait added 4 commits September 10, 2026 16:57
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
@farnalabs farnalabs added the agent-generated PR created by an autonomous agent label Sep 10, 2026

@farnalabs farnalabs left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 modulo-reviewbot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 modulo-reviewbot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).
@farnalabs

Copy link
Copy Markdown
Owner Author

Automated fix: migration collision + lint + stale schema

This commit resolves the failing CI on this branch (c759654).

1. Migration collision (root cause of Check migration heads + Test (Backend)::test_0008_is_single_chain_head)
The branch's migration was numbered 0208_variant_batch_state, which collides with main's head 0208_notification_indexes_and_constraint (both children of 0207_collection_install_tracking), producing a 2-head Alembic graph. Renamed it to 0209_variant_batch_state, set revision = "0209_variant_batch_state", and set down_revision = "0208_notification_indexes_and_constraint" (the real main head, not an assumed one). Verified locally that once main is merged the graph resolves to a single head (0209_variant_batch_state). No external references to the old revision id existed, so no sweep was needed.

2. Lint (Backend) — Semgrep inline-import rule
Moved module-level-safe inline imports in backend/src/modulo/api/routes/variant_batches.py to the top of the file (sqlalchemy, EvalResult, Run as RunModel, run_variant_batch). These do not break circular deps (the module already imports from these modules at top level).

3. Schema freshness
Regenerated frontend/src/lib/api/schema.ts via pnpm run generate:api (was stale after the new variant-batches routes).

All pre-commit hooks pass (ruff, ruff-format, bandit, semgrep, gitleaks, import-linter, eslint, etc.).

Note (out of scope): BDD (full suite) failed on a break-glass role posture assertion about the accounts table (modulo_app extra UPDATE grants on break-glass columns + modulo role flagged as superuser). This PR (FAR-775 variant batches) does not touch accounts/break-glass or the bootstrap role, and main's own CI shows no such posture failure — so this looks like a pre-existing/environmental BDD issue, not caused by this branch. Recommend investigating it separately rather than coupling it to this PR.

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).
@farnalabs

Copy link
Copy Markdown
Owner Author

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

  • The branch added 0208_variant_batch_state chained off 0207_collection_install_tracking, but main had since advanced and already owns 0208_notification_indexes_and_constraint (also off 0207). Two migrations with prefix 0208 → Alembic reported 2 heads (CI: Check migration heads failed), and the head-pinning tests asserted a stale 0207_collection_install_tracking head (CI: Test (Backend) failed in test_trigger_streak_engine / test_eval_suite_run::test_single_migration_head).
  • api/routes/variant_batches.py had inline imports inside async def handlers → Semgrep inline-import-route-files finding (Lint (Backend) failed).
  • frontend/src/lib/api/schema.ts was stale after the new variant-batches routes (Schema freshness failed).

What was fixed (two commits)

  1. c759654cd — renumbered the migration 0208_variant_batch_state0209_variant_batch_state and chained it off the real main head 0208_notification_indexes_and_constraint (single head once merged), moved the inline route imports to module level, and regenerated schema.ts.
  2. b4e3ca9ef — updated the head/chain-pinning tests (test_trigger_streak_engine, test_migration_ongoing_trigger, test_migration_guardrail_kill_switch, test_migration_reconcile_staging_schema, test_migration_sync_feature_flag_catalog, test_trigger_event_vocabulary, test_migration_guardrail_trust_pr_b, and the 0207→0208→0209 chain assertion in test_eval_suite_run) to the new single head 0209_variant_batch_state.

Verified locally: alembic heads → exactly one head (0209_variant_batch_state), all head/chain-pinning tests pass, ruff + semgrep clean; the cross-platform pre-commit hooks passed on both commits.

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).
@farnalabs

Copy link
Copy Markdown
Owner Author

Automated fix: migration-collision + cascading test gaps (PR #347)

Root cause: The PR added 0208_variant_batch_state, colliding with main's 0208_notification_indexes_and_constraint — the Alembic graph had two heads, which cascaded into failures across Check migration heads, Schema freshness, Lint (Backend) (semgrep inline-import-route-files in the new route handlers), and Test (Backend) (test_0008_is_single_chain_head + every chain-head/RLS/schema/uuid-PK guard test).

What was fixed (commit 60b4c3e):

  • Renumbered the PR migration to 0209_variant_batch_state with down_revision = "0208_notification_indexes_and_constraint" (the real main head) — single head restored.
  • A concurrent Branch Fixer already landed the core renumber, the head/chain-pinning test updates, the regenerated frontend/src/lib/api/schema.ts, and the route inline-import → module-level move.
  • This commit adds the three gaps that first pass missed:
    1. variant_batch_state added to _POST_0194_TABLES (frozen 0194 uuid-PK coverage test).
    2. variant_batch_state added to the test_schema required-tables set.
    3. _ORG_SCOPED_TABLES = (_TABLE,) declared in the 0209 migration so the RLS-coverage drift test detects the table's ENABLE ROW LEVEL SECURITY (emitted via an f-string the test's literal regex skips).

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 BDD (full suite) failure is a break_glass_role_posture_failed DB-grant assertion (extra UPDATE grants on the accounts table for modulo_app) — an environment/role-provisioning issue in the BDD DB, unrelated to this PR's code. It is not addressed here.

@farnalabs farnalabs left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 migration 0208_notification_indexes_and_constraint (it exists on origin/main), so 0209_variant_batch_state's down_revision dangles 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 modulo-reviewbot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Orphaned migration head0209_variant_batch_state.py sets down_revision to 0208_notification_indexes_and_constraint, which exists only on origin/main and 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 checks test_migration_0194_uuid_pk_server_defaults (uuid-PK count drifted to 85) and test_schema::test_initial_schema_contains_required_tables. Fix: rebase/merge origin/main so 0209 chains onto 0208.

  2. Missing RLS on variant_batch_statetest_rls_coverage::test_every_org_scoped_table_has_rls_policy fails. The guardrail requires ENABLE ROW LEVEL SECURITY + CREATE POLICY rls_org_isolation for the new table in the shape it scans (see 0002_rls_policies.py / 0088_rls_missing_policies.py precedent); the migration's own ceremony does not satisfy the guardrail scan.

  3. 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.
@farnalabs

Copy link
Copy Markdown
Owner Author

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 cause

The branch renumbered its variant_batch_state migration to 0209_* to avoid colliding with main's head, and set down_revision = "0208_notification_indexes_and_constraint". But 0208_notification_indexes_and_constraint only exists on main — the branch was cut before main gained it, so the file was never present in this branch's versions/ directory. Alembic therefore could not resolve the parent revision (KeyError: '0208_notification_indexes_and_constraint'), which broke the migration graph and cascaded into three failures:

  • test_schema.py::test_initial_schema_contains_required_tables
  • test_rls_coverage.py::test_every_org_scoped_table_has_rls_policy
  • test_trigger_event_vocabulary.py::TestReconciliationMigration::test_0008_is_single_chain_head (expected single head 0209_variant_batch_state, got a broken graph)

Fix

Cherry-picked main's 0208_notification_indexes_and_constraint.py into the branch (the single missing link). The migration graph is now …0207_collection_install_tracking → 0208_notification_indexes_and_constraint → 0209_variant_batch_state with a single head at 0209_variant_batch_state. This is a non-rewrite, surgical one-file add; on merge to main git auto-resolves the identical file (no conflict).

Verification

Locally re-ran the three previously-failing tests + the full test_trigger_event_vocabulary.py / test_schema.py / test_rls_coverage.py suites — all pass. alembic heads shows exactly one head. Pre-commit hooks (ruff, ruff-format, bandit, semgrep, gitleaks, import-linter) all pass on the added file.

@farnalabs

Copy link
Copy Markdown
Owner Author

Root cause of the CI: Fast Validation failure

The failing run was on commit c759654c. The real defect is a dangling down_revision, not the head-pinning tests themselves:

  • This branch's 0209_variant_batch_state migration declares down_revision = "0208_notification_indexes_and_constraint".
  • 0208_notification_indexes_and_constraint.py only exists on main — this PR branched before it landed, so the file was absent from the branch's versions/ directory.
  • With the parent missing, Alembic's ScriptDirectory.get_heads() raised KeyError: '0208_notification_indexes_and_constraint' (verified locally against 1.19.2). Every head-pinning test that calls get_heads() (test_eval_suite_run::test_single_migration_head, test_trigger_streak_engine::test_migration_backfills_epoch_and_branches_off_current_head, the guardrail / ongoing-trigger / reconcile / sync-feature-flag / trigger-event chain tests) therefore errored instead of passing. The RLS-coverage, schema-table-set, and uuid-PK drift tests were already corrected in 60b4c3e97.

Fix

Brought 0208_notification_indexes_and_constraint.py from origin/main into the branch (commit a7848405e, "bring main's 0208 migration into the branch so 0209 chains correctly", applied by the concurrent Branch Fixer). The chain is now linear and single-headed:

0207_collection_install_tracking → 0208_notification_indexes_and_constraint → 0209_variant_batch_state

alembic heads now resolves to exactly one head, 0209_variant_batch_state, so all head-pinning tests pass. No revision/down_revision values changed and the added file is byte-identical to main, so the eventual merge back to main stays clean.

Verified locally: get_heads() == ['0209_variant_batch_state'] and the chain-parent assertions from test_eval_suite_run::test_single_migration_head hold (nothing chains off 0209).

…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
@farnalabs

Copy link
Copy Markdown
Owner Author

Automated Branch Fixer — merge-conflict resolution (PR #347)

Root cause: The branch had diverged from main and carried merge conflicts. The conflicted files were all single-head migration guardrail tests whose head pin had been advanced to 0209_variant_batch_state on this branch, while main's equivalent pins still pointed at 0208_notification_indexes_and_constraint. main's current head is 0208_notification_indexes_and_constraint, and this branch's 0209_variant_batch_state correctly chains onto it (down_revision = "0208_notification_indexes_and_constraint"), so the merged graph has exactly one head: 0209_variant_batch_state.

What changed: Merged origin/main and resolved all 8 conflicts in the head-pinning tests, keeping the branch's 0209_variant_batch_state assertion in every case (this is the correct post-merge head), while correcting comment accuracy (0208 is from PR #337, not FAR-697/FAR-775):

  • test_migration_guardrail_kill_switch.py, test_migration_guardrail_trust_pr_b.py, test_migration_ongoing_trigger.py, test_migration_sync_feature_flag_catalog.py
  • test_migration_reconcile_staging_schema.py, test_trigger_event_vocabulary.py (_CHAIN_HEAD_MIGRATION0209_variant_batch_state)
  • test_eval_suite_run.py (adds the 0209-chains-off-0208 assertions; nothing chains off 0209 → it is the single head)
  • test_trigger_streak_engine.py (linear chain comment + head assertion)

Already satisfied on the branch (verified green locally): the 02090208 chain (single-head guardrail shows exactly one head), the ENABLE/FORCE ROW LEVEL SECURITY + rls_org_isolation policy + DML grants for variant_batch_state (test_rls_coverage passes), and the drift fixtures (test_migration_0194_uuid_pk_server_defaults already excludes variant_batch_state via _POST_0194_TABLES; test_initial_schema_contains_required_tables already lists variant_batch_state). No further migration rework was needed.

Verification: alembic heads → single head 0209_variant_batch_state; ran the guardrail, RLS-coverage, 0194, schema, and variant-batches API tests — all pass. Pre-commit hooks passed on the merge commit.

Commit: c0f2d5798

@farnalabs farnalabs left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 modulo-reviewbot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@farnalabs

Copy link
Copy Markdown
Owner Author

Automated fix: merge conflict with main resolved

Commit: 42761900f00a94c80e0694af25ca3b1b16ae3d72

Root cause

This branch previously renumbered the FAR-775 migration to 0209_variant_batch_state. Meanwhile main landed 0209_collection_install_id_entity_columns (FAR-762/FAR-761 drift fix), so merging main produced two 0209_ migrations both chained off 0208 — a two-head Alembic graph that blocks every head/chain-pinning test and the migration guardrails.

Fix

  • git mv the FAR-775 migration to 0210_variant_batch_state and set its down_revision = "0209_collection_install_id_entity_columns", so the chain is now 0208 → 0209_collection_install_id_entity_columns → 0210_variant_batch_state with a single head.
  • Swept every head/chain-pinning test reference from 0209_variant_batch_state to 0210_variant_batch_state (the new chain head): test_trigger_streak_engine, test_eval_suite_run (incl. the chaining_off_0209 chain walk), test_migration_guardrail_kill_switch, test_migration_ongoing_trigger, test_migration_reconcile_staging_schema, test_migration_sync_feature_flag_catalog, test_migration_guardrail_trust_pr_b, test_trigger_event_vocabulary.
  • Merged origin/main in (resolving the other conflicted test files to the new head) and verified alembic heads reports exactly one head: 0210_variant_batch_state.

Verification

  • alembic heads → single head 0210_variant_batch_state.
  • All 8 migration/chain-pinning test files: 164 passed.
  • Pre-commit cross-platform gate green (ruff, ruff-format, bandit, semgrep, gitleaks, import-linter, check-merge-conflict, EOF/whitespace, no-commit-to-branch).

This re-check of the Sonar quality gate after conflict resolution: the change is a pure rename + test-reference sweep, so no new code-smell/coverage regressions are introduced beyond what the gate already enforced via pre-commit.

…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).
@farnalabs

Copy link
Copy Markdown
Owner Author

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) — column collection_install_id of relation schemas already exists

Migration 0209 added the denormalised collection_install_id column on schemas/agents/pipelines via op.add_column, but 0207 (merged to main in #352) now already adds the same column via ADD COLUMN IF NOT EXISTS. Replaying the full migration chain on a fresh DB therefore hit 0209 after the column already existed and aborted — which broke both the BDD migration step and the break-glass boot allow-list smoke test.

Fix: made 0209 idempotent — ALTER TABLE … ADD COLUMN IF NOT EXISTS + CREATE INDEX IF NOT EXISTS. The index ix_{table}_collection_install_id is unique to 0209 (0207 does not create it), so it stays owned by this migration. Verified the chain still resolves to a single head (0210_variant_batch_state).

2. SonarCloud cognitive-complexity gate (CRITICAL, S3776)

list_batches (cc=23) and _load_batch_detail (cc=17) exceeded the SonarCloud threshold of 15. Refactored them into small helpers (_build_state_summaries, _build_legacy_summaries, _legacy_batch_id_filter, _resolve_batch_meta, _resolve_batch_timestamps); both functions now sit well under 15. Behavior is unchanged — pipeline_name was already always None in the original code, and the legacy exclusion still uses all org-wide state ids. All 17 test_variant_batches.py unit tests pass.

Commit: dc050723df44c8d1130ff62670ad71283ef337f8 (pre-commit hooks: ruff, ruff-format, bandit, semgrep, gitleaks, import-linter all green).

@farnalabs farnalabs left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 modulo-reviewbot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@farnalabs

Copy link
Copy Markdown
Owner Author

Branch Fixer — merge-conflict resolved

Root cause: main already merged an idempotent version of migration 0209_collection_install_id_entity_columns via #355 (fix(deploy): make 0209 idempotent to avoid duplicate collection_install_id column). This branch still carried a duplicate idempotency fix (raw ALTER TABLE ... ADD COLUMN IF NOT EXISTS), so merging main raised a conflict on that single file.

Fix: Merged origin/main and resolved the only conflicting file — backend/src/modulo/db/migrations/versions/0209_collection_install_id_entity_columns.py — by taking main's already-merged version (now byte-identical to main). All pre-commit quality hooks pass. Commit 5de11d436. The branch is now MERGEABLE.

⚠️ Residual structural conflict (needs author/Conductor coordination — not auto-fixable): This branch adds 0210_variant_batch_state with down_revision = 0209, while PR #339 adds 0210_community_gate also with down_revision = 0209. Merging both into main would create two alembic heads. To merge them together the migration graph must be a single chain: one 0210 must set down_revision to the other, and the corresponding assert heads == [...] test updated to the final head. Please coordinate the merge order between #347 and #339 before the merge queue processes them together.

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.
@farnalabs

Copy link
Copy Markdown
Owner Author

Automated fix: SonarCloud new-code coverage gate (57.5% < 80%)

Root cause: The SonarCloud quality gate failed on Coverage on New Code (57.5%, required ≥ 80%). The failure was not a logic bug — it was missing test coverage on the new FAR-775 production code: the entire backend/src/modulo/api/routes/variant_batches.py route module and the new variant_batch_state CRUD section in backend/src/modulo/db/crud/variant_group.py had no unit tests exercising them.

Fix (edaf553f9): Added two pure unit-test files (mocked sessions, no DB/RLS) that drive every handler and helper in the new code:

  • tests/unit/api/test_variant_batches_coverage.py — covers list_batches (including the legacy-scan page-fill path _build_legacy_summaries that the existing test skipped), get_batch, delete_batch, re_fire_batch (success + the 422/404/429/502 error branches), plus the pure helpers _legacy_batch_id_filter, _resolve_batch_meta, _resolve_batch_timestamps, _summarise_batch_runs, _build_state_summaries, _load_run_blobs, _batch_load_eval_stats, _batch_load_eval_results, _load_batch_detail.
  • tests/unit/db/test_variant_group_coverage.py — covers the new FAR-775 variant_batch_state CRUD: get_batch_state, get_batch_runs, list_batch_runs_for_batch_ids, get_all_state_batch_ids, list_batch_states, upsert_batch_state (insert + update), soft_delete_batch_state (absent / already-deleted / soft-delete).

Result: variant_batches.py reaches 100% line coverage (with the pre-existing test_variant_batches.py), the new CRUD section is fully covered, and the new-code coverage now clears the 80% gate. No production code was modified — this is a coverage-only fix.

Pre-commit hooks (ruff, ruff-format, bandit, gitleaks, et al.) all passed on the commit.

@farnalabs farnalabs left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is failing on the current head (edaf553):

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 modulo-reviewbot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).
@farnalabs

Copy link
Copy Markdown
Owner Author

Automated fix: empty-container literal equality in variant batches coverage test

The Test (Backend) CI job failed on the architecture lint test_no_empty_container_literal_equality (in tests/architecture/test_test_suite_quality.py). It flagged 3 assertions in backend/tests/unit/api/test_variant_batches_coverage.py that compare against empty container literals:

  • test_variant_batches_coverage.py:178assert _build_state_summaries([], {}) == []assert not _build_state_summaries([], {})
  • test_variant_batches_coverage.py:224assert await vb._batch_load_eval_results(...) == {}assert not await vb._batch_load_eval_results(...)
  • test_variant_batches_coverage.py:245assert await vb._batch_load_eval_stats(...) == {}assert not await vb._batch_load_eval_stats(...)

The lint intentionally skips bare-name operands (so the existing assert out == {} at line 401 was correctly left untouched — out may bind None), but these three expressions are call/await results, which the lint flags. Each was rewritten to the falsy assert not ... form, preserving the original intent.

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).
@farnalabs

Copy link
Copy Markdown
Owner Author

Automated fix: merge-conflict + migration collision

This branch had two problems blocking merge, both now resolved in commit b2c404cd0:

1. Migration number collision (root cause of the "2 heads" / architecture-test failure)

main had already merged 0210_community_gate (FAR-764) as the head off 0209_collection_install_id_entity_columns, and this PR added 0210_variant_batch_state on the same numeric prefix with the same down_revision. That produced a two-headed Alembic graph, which is what made the Test (Backend): Run architecture tests step fail.

Fix: renumbered the PR's migration to 0211_variant_batch_state.py with down_revision = "0210_community_gate", so the chain is again linear:
0209_collection_install_id_entity_columns -> 0210_community_gate (FAR-764) -> 0211_variant_batch_state (FAR-775).
Verified the graph now reports exactly one head (0211_variant_batch_state).

2. Merge conflicts

Merged origin/main and resolved the conflicts in the migration head-chain assertions across 8 test files (test_eval_suite_run, 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) to point at the new single head 0211_variant_batch_state. Product code and tests were not deleted — only the head-chain comments/assertions were updated to the renumbered migration.

All pre-commit hooks (ruff, ruff-format, bandit, semgrep, gitleaks, import-linter, eslint, merge-conflict) pass. Re-running CI should now go green.

@farnalabs farnalabs left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is failing on head b2c404c: check 'Test (Backend)' reports failure. Please fix the failing backend tests and push; re-review will follow. (Mergeability check passes; no other checks failing.)

@modulo-reviewbot modulo-reviewbot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Decision: Changes Requested

Blocking finding from the review node:

  • CI check Test (Backend) is failing on head SHA b2c404cd01b4c52b7f0e5213d6e2a755915a9755. 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.
@farnalabs

Copy link
Copy Markdown
Owner Author

Automated fix: migration guard test (test_upgrade_covers_exactly_every_metadata_uuid_pk)

CI failure: CI: Fast Validation run 34543429192 — backend unit tests failed with AssertionError: metadata uuid-PK count drifted: 85 / assert 85 == 84.

Root cause: backend/tests/unit/db/test_migration_0194_uuid_pk_server_defaults.py defined _POST_0194_TABLES twice (a latent duplicate already present on main). This PR added variant_batch_state to the first copy (so the new 0211 migration's table is excluded from the frozen 0194 uuid-PK coverage count of 84), but the second definition still omitted it and overrode the first — so variant_batch_state was counted, pushing the total to 85.

Fix: removed the duplicate # comment + _POST_0194_TABLES = ... block so the single, correct definition (including variant_batch_state) takes effect. Verified: the test now passes (6/6) and ruff check is clean.

Commit: 40f963a

@farnalabs farnalabs left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 modulo-reviewbot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@sonarqubecloud

Copy link
Copy Markdown

@github-actions
github-actions Bot merged commit 2f5c2c5 into main Sep 11, 2026
22 checks passed
@github-actions
github-actions Bot deleted the g-far-775-variant-batches-api branch September 11, 2026 01:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-generated PR created by an autonomous agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants