Skip to main content

Cell Specification Default Model Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Let a cell specification designate one of its own parameterized models (PMs) as the “default model”, settable via the API and highlighted/settable in the UI, and auto-set on the first PM created for a spec. Architecture: Add a nullable default_parameterized_model_id FK column on cell_specifications. The existing cell-spec PATCH endpoint carries the field; the service validates the PM belongs to the spec (service-layer integrity, not a DB composite FK) and supports explicit-null clearing via model_fields_set. A shared backend helper auto-sets the default at both PM-creation call sites. The frontend gets a new updateCell RTK Query mutation, a “Default” chip + 3-dot “Set as default” action on the spec’s PM tab, and a badge + button on the PM detail page. Tech Stack: FastAPI, Supabase/Postgres, Pydantic v2, React, Redux Toolkit Query, MUI DataGridPro. Tests via just (pytest + vitest + Playwright). Spec: docs/superpowers/specs/2026-07-21-cell-spec-default-model-design.md Conventions to obey: .claude/rules/fastapi-backend.md (domain exceptions, thin routes, PATCH), .claude/rules/supabase-migrations.md (DDL only, additive, idempotent), .claude/rules/frontend-conventions.md (buttons, toasts, three-dot menus, no dialog for reversible actions), .claude/rules/frontend-data-tables.md (StandardDataGrid, actions column), .claude/rules/redux-state.md (RTK Query patterns), .claude/rules/playwright-e2e-tests.md (exclusion assertions, idempotent seeds). Testing commands:
  • Backend: just test-backend backend/tests/<path> -v
  • Frontend unit: just test-frontend-unit
  • E2E: just test-frontend <spec>

File Structure

Backend
  • Create: supabase/migrations/<ts>_add_default_parameterized_model_to_cell_specs.sql — the DDL column + index.
  • Create: backend/src/services/default_parameterized_model.py — small shared helper set_default_pm_if_unset(cell_spec_repo, cell_spec_id, pm_id) used by both PM-creation sites (keeps cross-table write logic in one place; standalone because the two call sites are module functions, not CellSpecificationService methods).
  • Modify: backend/src/pydantic_models/cells.py — add field to CellSpecification (response) + UpdateCellSpecificationNested.
  • Modify: backend/src/services/cell_specification_service.py — inject PM repo; validate + write the field in update_cell_spec_nested.
  • Modify: backend/src/services/ecm.py (~line 1366, after create_parameterized_model) — call the helper.
  • Modify: backend/src/services/cycler_protocol_service.py (~line 235, after create_parameterized_model) — call the helper.
  • Test: backend/tests/test_services/test_cell_specification_default_model.py.
  • Test: backend/tests/test_services/test_default_parameterized_model_helper.py.
Frontend
  • Modify: frontend/src/redux/types/cell.ts — add default_parameterized_model_id.
  • Modify: frontend/src/redux/api/cells-api.ts — map the field; add updateCell mutation.
  • Modify: frontend/src/sections/data-management/components/spec-parameterized-models-tab.tsx — default chip + set-as-default action.
  • Modify: frontend/src/sections/parameterized-model/containers/parameterized-model-details-container.tsx — default badge + set-as-default button.
  • Test: frontend/src/redux/api/cells-api.test.ts (flat, sibling of the source — this file already exists; add the new case there).
  • Test (E2E): frontend/e2e/<cell-spec-default-model>.spec.ts.

Task 1: Database migration

Files:
  • Create: supabase/migrations/<ts>_add_default_parameterized_model_to_cell_specs.sql
Use a timestamp strictly greater than the latest existing migration. Determine it:
Pick <ts> = a new UTC timestamp after the newest (e.g. 20260721120000). If a same-timestamp collision appears vs main later, git mv to the next second (see memory: migration-version-collision).
  • Step 1: Write the migration
  • Step 2: Apply the migration to local Supabase
Run (do NOT db reset — see memory feedback_no_db_reset):
Expected: migration applies cleanly; column exists. Verify:
Expected: one row showing the uuid column. (If SUPABASE_DB_URL unset, use the local connection string from backend/.env / supabase status.)
  • Step 3: Commit

Task 2: Pydantic models

Files:
  • Modify: backend/src/pydantic_models/cells.py (CellSpecification:453, UpdateCellSpecificationNested:574)
  • Step 1: Add field to the response model
In class CellSpecification(CellSpecificationBase), after the instance_ids field:
  • Step 2: Add field to the update model
In class UpdateCellSpecificationNested(BaseModel), alongside the other optional fields:
  • Step 3: Sanity import
Run:
(from backend/). Expected: ok / no import error.
  • Step 4: Commit

Task 3: Shared auto-default helper (TDD)

Files:
  • Create: backend/src/services/default_parameterized_model.py
  • Test: backend/tests/test_services/test_default_parameterized_model_helper.py
The helper only fills an empty default; it never overrides an existing one. It reads the spec via the cell-spec repo and updates it via the same repo.
  • Step 1: Write the failing test
  • Step 2: Run test to verify it fails
Run: just test-backend backend/tests/test_services/test_default_parameterized_model_helper.py -v Expected: FAIL (ModuleNotFoundError: src.services.default_parameterized_model).
  • Step 3: Write the helper
  • Step 4: Run test to verify it passes
Run: just test-backend backend/tests/test_services/test_default_parameterized_model_helper.py -v Expected: 3 passed.
  • Step 5: Commit

Task 4: Validate + persist the field in the update service (TDD)

Files:
  • Modify: backend/src/services/cell_specification_service.py (__init__:71, update_cell_spec_nested:504)
  • Test: backend/tests/test_services/test_cell_specification_default_model.py
Behaviour: when default_parameterized_model_id is in data.model_fields_set, validate (unless null) that the PM exists and pm.cell_spec_id == cell_spec_id, else raise BadRequestError; write the value (including null) to the update payload.
  • Step 1: Write the failing tests
Model the test on existing update_cell_spec_nested tests (find one to copy fixtures/mocks from):
Write, in the new test file, cases that construct a CellSpecificationService with mocked repos (mirror the existing test’s construction), then:
Use UpdateCellSpecificationNested(default_parameterized_model_id=...) and, for case 4, confirm "default_parameterized_model_id" in data.model_fields_set. For case 5 build UpdateCellSpecificationNested(name="x") and assert the key is absent from the write. Assert BadRequestError via pytest.raises(BadRequestError).
  • Step 2: Run tests to verify they fail
Run: just test-backend backend/tests/test_services/test_cell_specification_default_model.py -v Expected: FAIL (service doesn’t inject the PM repo / doesn’t handle the field yet).
  • Step 3: Inject the PM repo into the service
At the top of the file, ensure imports exist:
Confirm the dependency provider name:
Add a constructor param + assignment in __init__ (after analysis_bucket_repo):
If no DI provider exists for the PM repo, add one to backend/src/deps.py following the pattern of the other get_*_repository providers (it takes supabase: AsyncClient = Depends(get_supabase_user_client) and returns ParameterizedModelsRepository(supabase)).
  • Step 4: Handle the field in update_cell_spec_nested
In the “Build update payload” block (after the notes handling, before update_payload.update(component_ids)):
Confirm BadRequestError is imported at the top of the file (it is used elsewhere in the service).
  • Step 5: Run tests to verify they pass
Run: just test-backend backend/tests/test_services/test_cell_specification_default_model.py -v Expected: all passed.
  • Step 6: Run the existing cell-spec service tests (no regressions)
Run: just test-backend backend/tests/test_services/ -k cell_spec -v Expected: pass (constructor change didn’t break existing tests; if a test constructs the service directly, update it to pass a mock PM repo).
  • Step 7: Commit

Task 5: Wire the helper into both PM-creation sites

Files:
  • Modify: backend/src/services/ecm.py (after create_parameterized_model, ~line 1366)
  • Modify: backend/src/services/cycler_protocol_service.py (after create_parameterized_model, ~line 235)
Both sites already hold a PM repo (pm_repo / parameterized_models_repository) and a Supabase client. The helper needs a CellSpecificationRepository; construct it from the same Supabase client.
  • Step 1: ecm.py — call the helper after successful creation
Add import at top of ecm.py:
After created_pm is assigned (right before the return {...} at ~line 1381), guarded on cell_spec_id:
  • Step 2: cycler_protocol_service.py — call the helper after successful creation
Add the same two imports at the top of cycler_protocol_service.py (if not present). After parameterized_model_record is created and its id is validated (~line 236, before return str(...)):
Note: the except/duplicate branch that returns an existing model must NOT set the default (an existing PM shouldn’t hijack a default). Only the fresh-creation path calls the helper.
  • Step 3: Run the relevant service tests
Run:
Expected: pass. If these paths are only covered by integration tests, at minimum confirm imports resolve:
  • Step 4: Commit

Task 6: Frontend — Cell type + updateCell mutation (TDD)

Files:
  • Modify: frontend/src/redux/types/cell.ts
  • Modify: frontend/src/redux/api/cells-api.ts
  • Test: frontend/src/redux/api/cells-api.test.ts (flat sibling — already exists; extend it)
  • Step 1: Locate the api-test convention
Run:
Extend the existing cells-api.test.ts using the same helpers (makeTestStore, mockAxios from src/test-utils/) as the other RTK Query api tests.
  • Step 2: Add the type field
In frontend/src/redux/types/cell.ts, add to the Cell interface:
  • Step 3: Write the failing mutation test
In the api test file, add a test that dispatches updateCell and asserts a PATCH to /cell_specifications/:id with the right body and that the Cell tag is invalidated:
Adjust import paths / mockAxios API to match the existing tests (some use mockAxios.onPatch(...), others a custom helper — mirror a sibling test exactly).
  • Step 4: Run test to verify it fails
Run: just test-frontend-unit (or the vitest single-file form the repo uses) Expected: FAIL (updateCell is not an endpoint).
  • Step 5: Implement the mutation + mapping
In cells-api.ts:
  • Add 'default_parameterized_model_id' to KNOWN_CELL_KEYS.
  • In mapCell, add: default_parameterized_model_id: cell.default_parameterized_model_id ?? null,
  • Add the mutation type + endpoint:
  • Export the hook: add useUpdateCellMutation to the destructured exports.
Confirm the invalidateOnSuccess call signature by checking a sibling mutation in cells-api.ts / parameterized-models-api.ts and match it exactly (the arg shape varies).
  • Step 6: Run test to verify it passes
Run: just test-frontend-unit Expected: the new test passes; no regressions.
  • Step 7: Commit

Task 7: Frontend — Spec’s PM tab (chip + set-as-default)

Files:
  • Modify: frontend/src/sections/data-management/components/spec-parameterized-models-tab.tsx
  • Step 1: Fetch the spec’s current default
Add imports:
(Confirm the exact toast import path used elsewhere in this folder — some files import from sonner, this repo standard is src/components/snackbar; match sibling files.) Inside the component:
  • Step 2: Show the “Default” chip in the name cell
In the name column renderCell, wrap the existing Typography in a row that appends a chip when default:
Add Stack to the @mui/material import. Add defaultPmId to the columns useMemo dependency array.
  • Step 3: Add the actions column
Append to the columns array (follows .claude/rules/frontend-data-tables.md):
Add handleSetDefault to the useMemo deps.
  • Step 4: Verify in the browser
Follow the preview verification workflow: open the frontend, log in (auto-login snippet), navigate to a cell spec with ≥1 PM, open the “Parameterized models” tab. Confirm: the auto-defaulted PM shows the “Default” chip; the 3-dot menu shows “Set as default” (disabled on the default row); clicking it on another row moves the chip and shows the success toast. Check read_console_messages for errors. Screenshot for proof.
  • Step 5: Lint + commit
Run: cd frontend && npx eslint src/sections/data-management/components/spec-parameterized-models-tab.tsx Expected: clean.

Task 8: Frontend — PM detail page (badge + button)

Files:
  • Modify: frontend/src/sections/parameterized-model/containers/parameterized-model-details-container.tsx
  • Step 1: Fetch the owning spec + wire update
Add imports:
Inside the component (after parameterizedModel is available):
(skipToken, toast, extractApiError are already imported in this file.)
  • Step 2: Show the badge in the heading
Change the CustomBreadcrumbs heading from the bare name to a name + conditional chip. Simplest: keep heading={parameterizedModel.name} and add the chip into the action Stack, OR render a small inline heading. Add to the top of the action Stack (before the Edit button):
  • Step 3: Add the “Set as default” button
In the same action Stack, add (shown only when not already default and the PM has a spec):
  • Step 4: Verify in the browser
Navigate to a PM detail page. Confirm: default PM shows the “Default model” chip and no button; a non-default PM shows the “Set as default” button; clicking it flips to the chip + success toast; navigating back to the spec’s PM tab shows the chip moved (tag invalidation). Check console for errors. Screenshot.
  • Step 5: Lint + commit
Run: cd frontend && npx eslint src/sections/parameterized-model/containers/parameterized-model-details-container.tsx Expected: clean.

Task 9: E2E test

Files:
  • Create: frontend/e2e/cell-spec-default-model.spec.ts
Follows .claude/rules/playwright-e2e-tests.md: idempotent timestamp-suffixed seed, exact-count + exclusion assertions, ensureAuthenticated, data rows via [role="grid"] [role="rowgroup"] [role="row"].
  • Step 1: Write the E2E test
Seed (via page.request.post for API-contract resources) a project + cell spec, then create two PMs for that spec (timestamp-suffixed names). Because the first PM auto-becomes default, assertions:
  • Open the spec detail page → “Parameterized models” tab.
  • Assert exactly one row shows a “Default” chip, and it’s the first-created PM.
  • Assert the second PM’s row does NOT show the chip (exclusion).
  • Open the 3-dot menu on the first (default) row → “Set as default” is disabled.
  • Open the 3-dot menu on the second row → click “Set as default”.
  • Wait for the request (networkidle) → assert the chip now appears on the second PM and is ABSENT from the first.
Reuse seed helpers from frontend/e2e/test-helpers.ts (getSupabaseClient for fast related-resource seeding; page.request.post with auth header for spec/PM creation via routes).
  • Step 2: Run the E2E test
Run: just test-frontend frontend/e2e/cell-spec-default-model.spec.ts Expected: pass. (Requires local Supabase running + both dev servers; per worktree memory, confirm frontend/.env points at the running backend.)
  • Step 3: Commit

Task 10: Full verification + PR

  • Step 1: Run the full relevant suites
Expected: all green.
  • Step 2: Run prek on the diff
Use the prek skill (or git diff --cached staged) to validate: API conventions (PATCH, domain exceptions), migration uniqueness, no forbidden patterns.
  • Step 3: Push + open PR
Follow .claude/rules branch/PR conventions and the git-workflow / github-pr-labels skills. Branch is feat/cell-spec-default-model. PR body: summarize DB + backend validation + auto-default + two UI surfaces; link the spec; note out-of-scope items.

Notes / risk register

  • get_parameterized_model_by_id is the correct single-fetch method (repositories/parameterized_models.py:51) — not get_parameterized_model. Use it in the service validation.
  • DI provider for the PM repo may not exist in deps.py; Task 4 Step 3 covers adding one if missing.
  • invalidateOnSuccess call signature varies — match a sibling endpoint exactly rather than the illustrative form here.
  • toast import path: repo standard is src/components/snackbar (the PM detail file already uses it); the frontend-conventions doc mentions sonner — follow the actual sibling-file import, not the doc.
  • cycler duplicate branch must not set default — only the fresh-creation path calls the helper (Task 5 Step 2).
  • Migration timestamp collisions vs main — resolve with git mv per memory reference_migration_version_collision_vs_base.
  • Do not db reset (memory feedback_no_db_reset) — use supabase migration up.