--- name: ssr-form-migration description: Migrate an SSR form or wizard modal to the whole-form HTMX swap doctrine, top-rooted render functions, and the data-driven session-backed wizard engine. 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. Rendering stays in Hiccup (`com/*` components) — the earlier Selmer-templating step was abandoned. --- # 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 three 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). > **Rendering stays in Hiccup.** An earlier iteration of this skill added a fourth > pattern — templating interactive components in Selmer — which was later **abandoned**. > All modals render through the shared Hiccup components (`com/*`); there is no Selmer > layer. Ignore any residual Selmer references in the cookbooks below. 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/.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. **Render in Hiccup** with the shared `com/*` components. Reuse cookbook bits; add new ones back (heuristic 5). (An earlier version of this step templated interactive components in Selmer; that was abandoned — everything renders through Hiccup.) 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 ``). - **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/`.