Files
integreat/.claude/skills/ssr-form-migration/reference/gotchas.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

2.6 KiB

Gotchas

GROWS every migration. One entry per surprise. Also the home for any written exception to the scorecard ratchet (a metric that regressed for a documented reason).


Stale $refs / tippy after a swap

A whole-form swap can run an Alpine event handler before the component re-initialises, so a handler that dereferences $refs.input.__x_tippy or calls tippy.show() throws. Always null-guard: $refs.input?.__x_tippy?.hide(), tippy?.show(). The transaction-edit-swap.spec.ts trackErrors() helper fails the test on any pageerror or console.error, which is exactly how a stale-ref throw surfaces.

Let the server value win — don't preserve Alpine state across a server-driven change

When a server change should update a component (e.g. choosing a vendor sets its default account), rebuild that section fresh on the swap so the server-provided value lands without keying tricks. The bug this prevents: "changing the vendor a second time doesn't update the account" because preserved Alpine state shadowed the new server value. If you must preserve a component, key it by value so a change forces re-init: (assoc attrs :key (str id "--" current-value)).

Focus dies if the typed input is inside its own swapped region

The single most important invariant. Amount field → swap a sibling tbody, not the row. Memo → swap nothing. If a caret test (sameNode) fails, the input is in its own swap region — re-target to a sibling/ancestor that excludes it.

Faked cursors breed duplicate render fns

A with-cursor/MapCursor re-root to fake a deep start forces a *-no-cursor* twin. Removing the fake lets you delete the twin. Don't "fix" a faked cursor in place — top-root it and collapse to one render fn. (See render-functions.md.)

Edit Clojure with clojure-mcp tools, not the file editor

clojure_edit / clojure_edit_replace_sexp. If a file won't compile: clj-paren-repair the file, then retry; if still broken, lein cljfmt check. Run tests via clojure-eval / clj-nrepl-eval -p PORT, never lein test (slow, last resort).

Solr/typeahead in tests

Account/vendor search is backed by Solr, unavailable in tests. To drive a typeahead in e2e: type under the 3-char threshold, then inject a result into Alpine state (Alpine.$data(el).elements = [{value, label}]) and click it — the real click handler, tippy.hide(), Alpine reactivity, and the HTMX swap all run as in production. Entity ids come from GET /test-info.


Scorecard exceptions (ratchet violations with a reason)

None yet. Append here if a migration must let a metric regress for a documented reason.