Capture the proven whole-form hx-select swap method as a reusable skill so every later modal migration is cheaper and consistent. No app code changes. - SKILL.md: the per-migration playbook (classify → baseline → characterize → consolidate render fns → templatize → wire HTMX → collapse routes → verify → commit → feed skill) + Growth contract + non-negotiables. - reference/swap-doctrine.md: the four swap rules, focus invariant, Alpine-survives- swap hardening, target-selector strategy — worked from the real edit.clj swaps (memo no-request, account→location targeted cell, amount→totals sibling-tbody, vendor/mode/row whole-form). 0 OOB. - reference/render-functions.md: explicit-data or top-rooted cursor; the MapCursor fake + transaction-account-row-no-cursor* twin as the smell to remove. - reference/form-vs-wizard.md: classification + the data-driven session-backed (formtools SessionStorage) engine that replaces the snapshot round-trip + protocol. - reference/selmer-conventions.md: STUB, validated in Phase 2. - component-cookbook.md / gotchas.md / test-recipes.md / scorecard.md: seeded from what transaction-edit proves (7 cookbook entries, caret-survival + typeahead test recipes, scorecard baseline LOC 1608 / ~12 routes / 1 no-cursor twin / 2 faked roots / 0 OOB). Scorecard (Transaction Edit baseline, before Phase 2): LOC 1608, routes ~12, no-cursor twins 1, faked-cursor roots 2, snapshot merges ~75, OOB 0, mixed hx- 8.
6.4 KiB
name, description
| name | description |
|---|---|
| ssr-form-migration | 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 Djangoformtoolsmodel).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.
-
Read the skill. Skim
reference/and note whichcomponent-cookbook.mdentries andgotchas.mdyou can reuse. Start from the cookbook, not a blank file. -
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.
- Single logical step (even with a
-
Baseline the scorecard (
scorecard.md, heuristics in §6 of the plan). Record before-numbers with cheap tools: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 -
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. -
Consolidate render functions (
reference/render-functions.md). Make each render fn take explicit data or a top-rooted cursor. Delete*-no-cursor*duplicates and anywith-cursor/MapCursorrebind that fakes a deep starting position (heuristics 1, 2). Using a cursor is fine; faking where it starts is not. -
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). -
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). -
Collapse routes to 2 (
GETopen,POSTsubmit) —+1for an add-row endpoint,+1for the single*-form-changedwhole-form re-render endpoint (heuristic 6). -
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. -
Commit one reversible feature commit. The message includes the scorecard delta and the reused/new cookbook entries.
-
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.tscaret 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-repairthenlein cljfmt fixwhen a file won't compile. - Run tests via the
clojure-evalskill /clj-nrepl-eval -p PORT, notlein test. - Temp files go in
./tmp/.