---
name: ssr-form-migration
description: Migrate an SSR form or wizard modal to the whole-form HTMX swap doctrine, top-rooted render functions, the data-driven session-backed wizard engine, and (where it helps) Selmer templates. Use when simplifying a server-rendered form/wizard modal in this codebase, removing EDN-snapshot round-trips, deleting *-no-cursor* duplicate render fns, collapsing per-interaction routes, or replacing the multi-step wizard protocol machinery.
---

# SSR Form & Wizard Migration

A repeatable method for making a server-rendered form/wizard modal **simpler** without
changing user-facing behavior. Distilled from the first proven migration — the
`transaction/edit.clj` modal, which already runs on the whole-form `hx-select` swap
approach with **zero out-of-band swaps**. Every migration *reads this skill first* and
*extends it last* (the Growth contract below). If migration N+1 is not easier than N,
the skill-update step was skipped — treat that as a bug.

The four patterns every migration moves code toward live in `reference/`:

- `reference/swap-doctrine.md` — the four-rule HTMX swap priority order + the focus
  invariant + Alpine-survives-swap hardening + target-selector strategy.
- `reference/render-functions.md` — one render fn per component, taking explicit data
  **or a top-rooted cursor**; no faked cursor positions, no `*-no-cursor*` twins.
- `reference/form-vs-wizard.md` — single-step → plain form; multi-step → data-driven
  engine with **per-step state in the Ring session** (the Django `formtools` model).
- `reference/selmer-conventions.md` — plain-HTML attributes via Selmer, the
  Hiccup↔Selmer interop bridge, include/block patterns.

Growing cookbooks (append every migration):
`component-cookbook.md`, `gotchas.md`, `test-recipes.md`, `scorecard.md`.

---

## The per-migration playbook

Run this loop for each modal. The phase notes in the migration plan list only what is
*specific* to a modal; this loop is the constant.

1. **Read the skill.** Skim `reference/` and note which `component-cookbook.md`
   entries and `gotchas.md` you can reuse. Start from the cookbook, not a blank file.

2. **Classify** (`reference/form-vs-wizard.md`).
   - Single logical step (even with a `?mode=` toggle or add/remove rows) → **plain
     form**: no server-side wizard state, no snapshot, no protocol.
   - Genuinely multiple steps the user advances through → **wizard**: the data-driven
     engine + per-step session storage.
   - When in doubt, it's a form.

3. **Baseline the scorecard** (`scorecard.md`, heuristics in §6 of the plan). Record
   before-numbers with cheap tools:
   ```bash
   F=src/clj/auto_ap/ssr/<modal>.clj
   wc -l $F                                   # LOC (heuristic 4)
   grep -c 'defn.*-no-cursor' $F              # *-no-cursor* twins (heuristic 1)
   grep -cE 'with-cursor|MapCursor\.' $F      # faked cursor re-roots (heuristic 1)
   grep -c 'hx-swap-oob' $F                   # OOB swaps (heuristic 7)
   grep -cE '"hx-[a-z]' $F                    # mixed string hx- attrs (heuristic 8)
   # route count: count this modal's entries in src/cljc/auto_ap/routes/*.cljc
   ```

4. **Characterize behavior (test-first).** Write or confirm a Playwright spec that
   captures *current* behavior before you touch anything — focus/caret survival across
   swaps, each field round-trip, validation errors, and the real save. This spec is the
   parity contract; it must stay green through every commit. See `test-recipes.md`.

5. **Consolidate render functions** (`reference/render-functions.md`). Make each render
   fn take explicit data or a **top-rooted cursor**. Delete `*-no-cursor*` duplicates
   and any `with-cursor`/`MapCursor` rebind that fakes a deep starting position
   (heuristics 1, 2). Using a cursor is fine; faking where it *starts* is not.

6. **Templatize in Selmer** (`reference/selmer-conventions.md`) where the component is
   interactive/attribute-heavy. Reuse cookbook bits; add new ones back (heuristics 5, 8).
   Static markup may stay Hiccup — Selmer scope is hybrid (Open decision 2).

7. **Wire HTMX per the swap doctrine** (`reference/swap-doctrine.md`). Keep the focus
   invariant intact. No OOB except a genuinely disjoint region, documented (heuristic 7).

8. **Collapse routes** to 2 (`GET` open, `POST` submit) — `+1` for an add-row endpoint,
   `+1` for the single `*-form-changed` whole-form re-render endpoint (heuristic 6).

9. **Verify.** Modal e2e green + full e2e suite at-or-above baseline; assert DB
   mutations by querying Datomic, not markup; REPL-check the pure render/data fns.
   Re-measure the scorecard — **no metric may regress for the touched modal** without a
   written exception in `gotchas.md`.

10. **Commit** one reversible feature commit. The message includes the scorecard delta
    and the reused/new cookbook entries.

11. **Feed the skill** (the Growth contract). *Not optional.*

---

## Growth contract — the last task of every migration

- Converted a component? → add its before/after to `component-cookbook.md`.
- Hit a surprise? → one entry in `gotchas.md`.
- Found a test pattern? → `test-recipes.md`.
- Playbook step missing or wrong? → fix this `SKILL.md`.
- Measured the scorecard? → append the row to `scorecard.md`.

**Success signal:** each migration reuses more cookbook entries and starts from a better
scorecard baseline than the previous one.

---

## Non-negotiables

- **Focus invariant:** the input the user is typing in is *never* inside the region its
  own request swaps. Violating this drops the caret. (Proven by the
  `transaction-edit-swap.spec.ts` caret tests.)
- **No new OOB swaps.** If tempted to OOB something inside the same feature, restructure
  the DOM so the dependent element shares an ancestor with the trigger and use an
  ordinary swap (e.g. totals in a sibling `<tbody>`).
- **Behavior parity is proven by tests, not by reading.** The full e2e suite stays green
  after every migration.
- **Don't game the heuristics.** They're directional evidence paired with the e2e parity
  gate; review the trend, not single numbers.

## Project conventions that bite (see `gotchas.md`)

- Edit Clojure with the clojure-mcp tools (`clojure_edit`, `clojure_edit_replace_sexp`),
  not the raw file editor. `clj-paren-repair` then `lein cljfmt fix` when a file won't
  compile.
- Run tests via the `clojure-eval` skill / `clj-nrepl-eval -p PORT`, not `lein test`.
- Temp files go in `./tmp/`.
