Files
integreat/.claude/skills/ssr-form-migration/SKILL.md
Bryce 8b43017d6e refactor(ssr): revert hiccup→Selmer migration; render forms in Hiccup again
Abandons the Selmer-templating step of the SSR re-authoring and moves the four
migrated form/wizard modals back to Hiccup (com/* components), keeping the
whole-form HTMX swap doctrine, top-rooted render functions, and the
session-backed wizard engine unchanged.

- transaction/edit, transaction/bulk_code, invoices (bulk-edit group), and
  pos/sales_summaries render via com/* again; every hx-* swap (whole-form +
  targeted location-cell / totals-tbody / inline account-cell swaps) is
  preserved exactly.
- add com/single-modal-card to centralize the md:w-[950px] md:h-[650px] modal
  chrome that previously lived only in the Selmer modal-card templates.
- delete auto-ap.ssr.selmer, auto-ap.ssr.components.selmer, selmer_test, the
  whole resources/templates tree (55 files), the selmer dependency, and the
  tailwind resources/templates content glob.
- strip Selmer guidance from the ssr-form-migration skill + modernization plan.

Verified: all four namespaces compile and render with no stringified-hiccup
leaks; output.css rebuilds byte-identically (no Tailwind class loss); 60 e2e
specs pass — the four reverted modals (incl. whole-form-swap focus/caret tests)
plus the untouched wizard/pay/new/rule modals.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 00:37:21 -07:00

6.6 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, 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:

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