Files
integreat/.claude/skills/ssr-form-migration/SKILL.md
Bryce 3ecd115f76 docs(skill): distil ssr-form-migration skill from transaction-edit reference (Phase 1)
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.
2026-06-03 00:05:11 -07:00

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

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