> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ionworks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 2026 07 21 cell spec default model

# 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:

```bash theme={null}
ls supabase/migrations/ | sort | tail -5
```

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**

```sql theme={null}
-- Add the "default model" pointer from a cell specification to one of its
-- parameterized models. Nullable; cleared automatically if the PM is deleted.
-- Integrity that the PM belongs to the same spec is enforced in the service
-- layer (see cell_specification_service.update_cell_spec_nested), not by a
-- composite FK.
ALTER TABLE cell_specifications
  ADD COLUMN IF NOT EXISTS default_parameterized_model_id uuid
  REFERENCES parameterized_models(id) ON DELETE SET NULL;

CREATE INDEX IF NOT EXISTS idx_cell_specifications_default_pm
  ON cell_specifications(default_parameterized_model_id);
```

* [ ] **Step 2: Apply the migration to local Supabase**

Run (do NOT `db reset` — see memory `feedback_no_db_reset`):

```bash theme={null}
supabase migration up
```

Expected: migration applies cleanly; column exists.

Verify:

```bash theme={null}
psql "$SUPABASE_DB_URL" -c "\d cell_specifications" | grep default_parameterized_model_id
```

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**

```bash theme={null}
git add supabase/migrations/<ts>_add_default_parameterized_model_to_cell_specs.sql
git commit -m "feat(db): add default_parameterized_model_id to cell_specifications"
```

***

## 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:

```python theme={null}
    default_parameterized_model_id: str | None = Field(
        None,
        description=(
            "ID of the parameterized model marked as this specification's "
            "default model, or None if unset."
        ),
    )
```

* [ ] **Step 2: Add field to the update model**

In `class UpdateCellSpecificationNested(BaseModel)`, alongside the other optional fields:

```python theme={null}
    default_parameterized_model_id: str | None = Field(
        None,
        description=(
            "Set the default parameterized model for this specification. Must "
            "reference one of this specification's own parameterized models. "
            "Pass null to clear the default."
        ),
    )
```

* [ ] **Step 3: Sanity import**

Run:

```bash theme={null}
just test-backend backend/tests/test_pydantic_models -q -k cell 2>/dev/null || uv run python -c "from src.pydantic_models.cells import CellSpecification, UpdateCellSpecificationNested; print('ok')"
```

(from `backend/`). Expected: `ok` / no import error.

* [ ] **Step 4: Commit**

```bash theme={null}
git add backend/src/pydantic_models/cells.py
git commit -m "feat(cells): add default_parameterized_model_id to cell spec models"
```

***

## 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**

```python theme={null}
import pytest
from unittest.mock import AsyncMock

from src.services.default_parameterized_model import set_default_pm_if_unset


@pytest.mark.asyncio
async def test_sets_default_when_unset():
    spec = AsyncMock()
    spec.default_parameterized_model_id = None
    repo = AsyncMock()
    repo.get_cell_specification.return_value = spec

    await set_default_pm_if_unset(repo, "spec-1", "pm-1")

    repo.update_cell_specification.assert_awaited_once_with(
        "spec-1", {"default_parameterized_model_id": "pm-1"}
    )


@pytest.mark.asyncio
async def test_noop_when_already_set():
    spec = AsyncMock()
    spec.default_parameterized_model_id = "pm-existing"
    repo = AsyncMock()
    repo.get_cell_specification.return_value = spec

    await set_default_pm_if_unset(repo, "spec-1", "pm-1")

    repo.update_cell_specification.assert_not_awaited()


@pytest.mark.asyncio
async def test_noop_when_spec_missing():
    repo = AsyncMock()
    repo.get_cell_specification.return_value = None

    await set_default_pm_if_unset(repo, "spec-1", "pm-1")

    repo.update_cell_specification.assert_not_awaited()
```

* [ ] **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**

```python theme={null}
"""Shared helper for auto-populating a cell spec's default parameterized model.

Called from every parameterized-model-creation site that supplies a
``cell_spec_id`` so the first model created for a specification becomes its
default. Never overrides an existing default.
"""

from src.repositories.cell_specifications import CellSpecificationRepository


async def set_default_pm_if_unset(
    cell_spec_repo: CellSpecificationRepository,
    cell_spec_id: str,
    parameterized_model_id: str,
) -> None:
    """Set ``cell_spec_id``'s default parameterized model if it has none yet.

    Parameters
    ----------
    cell_spec_repo : CellSpecificationRepository
        Repository used to read and update the cell specification.
    cell_spec_id : str
        Cell specification to (maybe) update.
    parameterized_model_id : str
        Parameterized model to install as the default when none is set.
    """
    spec = await cell_spec_repo.get_cell_specification(cell_spec_id)
    if spec is None or spec.default_parameterized_model_id is not None:
        return
    await cell_spec_repo.update_cell_specification(
        cell_spec_id,
        {"default_parameterized_model_id": parameterized_model_id},
    )
```

* [ ] **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**

```bash theme={null}
git add backend/src/services/default_parameterized_model.py backend/tests/test_services/test_default_parameterized_model_helper.py
git commit -m "feat(cells): add shared auto-default parameterized-model helper"
```

***

## 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):

```bash theme={null}
grep -rln "update_cell_spec_nested" backend/tests/
```

Write, in the new test file, cases that construct a `CellSpecificationService` with mocked repos (mirror the existing test's construction), then:

```python theme={null}
# 1. Valid PM belonging to this spec -> persisted
#    - parameterized_model_repo.get_parameterized_model_by_id returns a PM
#      whose cell_spec_id == the spec id
#    - assert update payload includes default_parameterized_model_id
# 2. PM belonging to a different spec -> BadRequestError
# 3. Non-existent PM (repo returns None) -> BadRequestError
# 4. Explicit null clears (field in model_fields_set, value None)
#    -> payload includes default_parameterized_model_id: None, no PM lookup
# 5. Field omitted -> no default_parameterized_model_id key in payload,
#    no PM lookup
```

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:

```python theme={null}
from src.repositories.parameterized_models import ParameterizedModelsRepository
from src.deps import get_parameterized_models_repository  # confirm exact name
```

Confirm the dependency provider name:

```bash theme={null}
grep -rn "ParameterizedModelsRepository" backend/src/deps.py backend/src/routes/parameterized_models.py
```

Add a constructor param + assignment in `__init__` (after `analysis_bucket_repo`):

```python theme={null}
        parameterized_model_repo: Annotated[
            ParameterizedModelsRepository,
            Depends(get_parameterized_models_repository),
        ],
    ):
        ...
        self.parameterized_model_repo = parameterized_model_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)`):

```python theme={null}
        if "default_parameterized_model_id" in data.model_fields_set:
            new_default = data.default_parameterized_model_id
            if new_default is not None:
                pm = await self.parameterized_model_repo.get_parameterized_model_by_id(
                    new_default
                )
                if pm is None or pm.cell_spec_id != cell_spec_id:
                    raise BadRequestError(
                        f"Parameterized model '{new_default}' does not belong to "
                        f"cell specification '{cell_spec_id}'. The default model "
                        f"must be one of this specification's own parameterized "
                        f"models."
                    )
            update_payload["default_parameterized_model_id"] = new_default
```

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**

```bash theme={null}
git add backend/src/services/cell_specification_service.py backend/src/deps.py backend/tests/test_services/test_cell_specification_default_model.py
git commit -m "feat(cells): validate and persist default_parameterized_model_id on update"
```

***

## 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`:

```python theme={null}
from src.repositories.cell_specifications import CellSpecificationRepository
from src.services.default_parameterized_model import set_default_pm_if_unset
```

After `created_pm` is assigned (right before the `return {...}` at \~line 1381), guarded on `cell_spec_id`:

```python theme={null}
    if cell_spec_id:
        await set_default_pm_if_unset(
            CellSpecificationRepository(supabase_client),
            cell_spec_id,
            str(created_pm.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(...)`):

```python theme={null}
            cs_id = model_config.get("cell_spec_id")
            if cs_id:
                await set_default_pm_if_unset(
                    CellSpecificationRepository(
                        parameterized_models_repository.supabase
                    ),
                    cs_id,
                    str(parameterized_model_record.id),
                )
```

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:

```bash theme={null}
just test-backend backend/tests/ -k "ecm or cycler_protocol" -v
```

Expected: pass. If these paths are only covered by integration tests, at minimum confirm imports resolve:

```bash theme={null}
cd backend && uv run python -c "import src.services.ecm, src.services.cycler_protocol_service; print('ok')"
```

* [ ] **Step 4: Commit**

```bash theme={null}
git add backend/src/services/ecm.py backend/src/services/cycler_protocol_service.py
git commit -m "feat(cells): auto-set default parameterized model at both creation sites"
```

***

## 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:

```bash theme={null}
ls frontend/src/redux/api/*.test.ts; grep -rln "mockAxios" frontend/src/redux/api | head
```

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:

```typescript theme={null}
  default_parameterized_model_id?: string | null;
```

* [ ] **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:

```typescript theme={null}
it('updateCell PATCHes /cell_specifications/:id with the body', async () => {
  const store = makeTestStore();
  mockAxios.onPatch('/cell_specifications/spec-1').reply(200, {
    id: 'spec-1',
    name: 'Spec',
    default_parameterized_model_id: 'pm-1',
  });

  await store
    .dispatch(
      cellsApi.endpoints.updateCell.initiate({
        cellSpecId: 'spec-1',
        body: { default_parameterized_model_id: 'pm-1' },
      })
    )
    .unwrap();

  const patchCall = mockAxios.history.patch[0];
  expect(patchCall.url).toBe('/cell_specifications/spec-1');
  expect(JSON.parse(patchCall.data)).toEqual({
    default_parameterized_model_id: 'pm-1',
  });
});
```

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:

```typescript theme={null}
type UpdateCellBody = Partial<Pick<Cell, 'default_parameterized_model_id'>>;

    updateCell: builder.mutation<Cell, { cellSpecId: string; body: UpdateCellBody }>({
      query: ({ cellSpecId, body }) => ({
        url: `/cell_specifications/${cellSpecId}`,
        method: 'patch',
        data: body,
      }),
      transformResponse: (cell: any) => mapCell(cell),
      invalidatesTags: (result, error, { cellSpecId }) =>
        invalidateOnSuccess([{ type: 'Cell', id: cellSpecId }])(result, error, undefined),
    }),
```

* 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**

```bash theme={null}
git add frontend/src/redux/types/cell.ts frontend/src/redux/api/cells-api.ts frontend/src/redux/api/__tests__/cells-api.test.ts
git commit -m "feat(cells): add updateCell mutation + default_parameterized_model_id mapping"
```

***

## 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:

```typescript theme={null}
import { Chip } from '@mui/material';
import { useGetCellQuery, useUpdateCellMutation } from 'src/redux/api/cells-api';
import { CustomGridActionsCellItem } from 'src/components/custom-data-grid';
import { Iconify } from 'src/components/iconify';
import { toast } from 'src/components/snackbar';
import { extractApiError } from 'src/utils/api-error';
```

(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:

```typescript theme={null}
  const { data: cellSpec } = useGetCellQuery(specId);
  const defaultPmId = cellSpec?.default_parameterized_model_id ?? null;
  const [updateCell] = useUpdateCellMutation();

  const handleSetDefault = useCallback(
    async (pmId: string, pmName: string) => {
      try {
        await updateCell({
          cellSpecId: specId,
          body: { default_parameterized_model_id: pmId },
        }).unwrap();
        toast.success(`Set "${pmName}" as the default model`);
      } catch (error) {
        toast.error(extractApiError(error, 'Failed to set default model').message);
      }
    },
    [specId, updateCell]
  );
```

* [ ] **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:

```typescript theme={null}
        renderCell: (params: GridRenderCellParams) => (
          <Stack direction="row" spacing={1} alignItems="center">
            <Typography
              component={RouterLink}
              href={paths.dashboard.parameterizedModels.detail(projectId, params.row.id)}
              sx={{ color: COLORS.link }}
              variant="body2"
              lineHeight="inherit"
            >
              {params.value}
            </Typography>
            {params.row.id === defaultPmId && (
              <Chip label="Default" size="small" color="primary" />
            )}
          </Stack>
        ),
```

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`):

```typescript theme={null}
      {
        field: 'actions',
        type: 'actions',
        align: 'center',
        headerAlign: 'center',
        width: 40,
        sortable: false,
        filterable: false,
        disableColumnMenu: true,
        getActions: (params) => [
          <CustomGridActionsCellItem
            key="set-default"
            showInMenu
            icon={<Iconify icon="mdi:star-outline" />}
            label="Set as default"
            disabled={params.row.id === defaultPmId}
            onClick={() => handleSetDefault(params.row.id, params.row.name)}
          />,
        ],
      },
```

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.

```bash theme={null}
git add frontend/src/sections/data-management/components/spec-parameterized-models-tab.tsx
git commit -m "feat(cells): default chip + set-as-default action on spec PM tab"
```

***

## 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:

```typescript theme={null}
import { Chip } from '@mui/material';
import { useGetCellQuery, useUpdateCellMutation } from 'src/redux/api/cells-api';
```

Inside the component (after `parameterizedModel` is available):

```typescript theme={null}
  const { data: cellSpec } = useGetCellQuery(
    parameterizedModel?.cell_spec_id ?? skipToken
  );
  const [updateCell] = useUpdateCellMutation();
  const isDefault =
    !!parameterizedModel &&
    cellSpec?.default_parameterized_model_id === parameterizedModel.id;

  const handleSetDefault = async () => {
    if (!parameterizedModel?.cell_spec_id) return;
    try {
      await updateCell({
        cellSpecId: parameterizedModel.cell_spec_id,
        body: { default_parameterized_model_id: parameterizedModel.id },
      }).unwrap();
      toast.success(`Set "${parameterizedModel.name}" as the default model`);
    } catch (error) {
      toast.error(extractApiError(error, 'Failed to set default model').message);
    }
  };
```

(`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):

```typescript theme={null}
            {isDefault && <Chip label="Default model" size="small" color="primary" />}
```

* [ ] **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):

```typescript theme={null}
            {!isDefault && parameterizedModel.cell_spec_id && (
              <Button
                variant="outlined"
                startIcon={<Iconify icon="mdi:star-outline" />}
                onClick={handleSetDefault}
              >
                Set as default
              </Button>
            )}
```

* [ ] **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.

```bash theme={null}
git add frontend/src/sections/parameterized-model/containers/parameterized-model-details-container.tsx
git commit -m "feat(cells): default badge + set-as-default button on PM detail page"
```

***

## 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**

```bash theme={null}
git add frontend/e2e/cell-spec-default-model.spec.ts
git commit -m "test(e2e): cell spec default model chip + set-as-default"
```

***

## Task 10: Full verification + PR

* [ ] **Step 1: Run the full relevant suites**

```bash theme={null}
just test-backend backend/tests/test_services/ -k "cell or default_parameterized or ecm or cycler" -v
just test-frontend-unit
```

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`.
