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 helperset_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, notCellSpecificationServicemethods). - Modify:
backend/src/pydantic_models/cells.py— add field toCellSpecification(response) +UpdateCellSpecificationNested. - Modify:
backend/src/services/cell_specification_service.py— inject PM repo; validate + write the field inupdate_cell_spec_nested. - Modify:
backend/src/services/ecm.py(~line 1366, aftercreate_parameterized_model) — call the helper. - Modify:
backend/src/services/cycler_protocol_service.py(~line 235, aftercreate_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.
- Modify:
frontend/src/redux/types/cell.ts— adddefault_parameterized_model_id. - Modify:
frontend/src/redux/api/cells-api.ts— map the field; addupdateCellmutation. - 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
<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
db reset — see memory feedback_no_db_reset):
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
class CellSpecification(CellSpecificationBase), after the instance_ids field:
- Step 2: Add field to the update model
class UpdateCellSpecificationNested(BaseModel), alongside the other optional fields:
- Step 3: Sanity import
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
- Step 1: Write the failing test
- Step 2: Run test to verify it fails
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
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
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
update_cell_spec_nested tests (find one to copy fixtures/mocks from):
CellSpecificationService with mocked repos (mirror the existing test’s construction), then:
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
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
__init__ (after analysis_bucket_repo):
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
notes handling, before update_payload.update(component_ids)):
BadRequestError is imported at the top of the file (it is used elsewhere in the service).
- Step 5: Run tests to verify they pass
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)
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(aftercreate_parameterized_model, ~line 1366) - Modify:
backend/src/services/cycler_protocol_service.py(aftercreate_parameterized_model, ~line 235)
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
ecm.py:
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
cycler_protocol_service.py (if not present). After parameterized_model_record is created and its id is validated (~line 236, before return str(...)):
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
- 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
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
frontend/src/redux/types/cell.ts, add to the Cell interface:
- Step 3: Write the failing mutation test
updateCell and asserts a PATCH to /cell_specifications/:id with the right body and that the Cell tag is invalidated:
mockAxios.onPatch(...), others a custom helper — mirror a sibling test exactly).
- Step 4: Run test to verify it fails
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
cells-api.ts:
- Add
'default_parameterized_model_id'toKNOWN_CELL_KEYS. - In
mapCell, add:default_parameterized_model_id: cell.default_parameterized_model_id ?? null, - Add the mutation type + endpoint:
- Export the hook: add
useUpdateCellMutationto the destructured exports.
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
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
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
name column renderCell, wrap the existing Typography in a row that appends a chip when default:
Stack to the @mui/material import. Add defaultPmId to the columns useMemo dependency array.
- Step 3: Add the actions column
columns array (follows .claude/rules/frontend-data-tables.md):
handleSetDefault to the useMemo deps.
- Step 4: Verify in the browser
read_console_messages for errors. Screenshot for proof.
- Step 5: Lint + commit
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
parameterizedModel is available):
skipToken, toast, extractApiError are already imported in this file.)
- Step 2: Show the badge in the heading
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
action Stack, add (shown only when not already default and the PM has a spec):
- Step 4: Verify in the browser
- Step 5: Lint + commit
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
.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
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.
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
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
- Step 2: Run prek on the diff
prek skill (or git diff --cached staged) to validate: API conventions (PATCH, domain exceptions), migration uniqueness, no forbidden patterns.
- Step 3: Push + open PR
.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_idis the correct single-fetch method (repositories/parameterized_models.py:51) — notget_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. invalidateOnSuccesscall signature varies — match a sibling endpoint exactly rather than the illustrative form here.toastimport path: repo standard issrc/components/snackbar(the PM detail file already uses it); the frontend-conventions doc mentionssonner— 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 mvper memoryreference_migration_version_collision_vs_base. - Do not
db reset(memoryfeedback_no_db_reset) — usesupabase migration up.