Migrates the Transaction Bulk Code modal (a single-step form wearing a full wizard costume) to a plain Selmer form, cold-applying the ssr-form-migration skill. Almost entirely reuse of the Phase-2 work: the whole `sc/*` Selmer component library, `account-typeahead*` / `location-select*`, and the `edit-modal` / `transitioner` chrome are imported wholesale. What changed - Wizard removed: deleted `BulkCodeWizard` / `AccountsStep` records, `MultiStepFormState`, the `step-params[...]` prefix, and all `mm/*` middleware. Replaced with a plain handler + flat `wrap-bulk-state` (decode straight into `bulk-code-schema`, no snapshot round-trip). - Selection round-trip: the non-editable transaction selection is resolved to a concrete not-locked id vector at open and ridden back in hidden `ids[]` fields (the bulk analog of edit's single `db/id`) — no EDN snapshot, no filter re-query, and more correct (codes exactly the rows the user saw). - 100% Selmer render path (only the shared terminal `com/success-modal` keeps Hiccup — heuristic-9 exception). New shared component `sc/select` (`location-select.html` generalized) for the status dropdown. - Routes 4 -> 3: GET `bulk-code` (open), POST `bulk-code-submit`, POST `bulk-code-form-changed` (one whole-form op dispatcher folding the old `new-account` + `vendor-changed` routes). Location swap moved off `find *` onto explicit `#account-location-<index>` + `hx-select`. - Fixed a latent correctness bug surfaced by the migration: the vendor typeahead needs `:id` (value-keyed `:key`) or its value-bound hidden goes stale across a whole-form swap and posts blank. Scorecard delta (transaction/bulk_code.clj): mm coupling 19->0, snapshot merges 4->0, wizard records 3->0, step-params 10->0, routes 4->3, OOB 0, Hiccup-in-render ->0 (bar success-modal). LOC 420->506 (documented exception: the wizard was a thin shell over mm/* defaults, so explicitness moves shared plumbing into the file). Cookbook: reused the entire Phase-2 sc/* lib + chrome, added sc/select. Verification: bulk-code-transactions.spec.ts 13/13; full Playwright suite 39/39; cljfmt clean. Skill fed: scorecard row + narrative + LOC exception; gotchas (value-bound typeahead keying, selection-as-ids round-trip); cookbook (sc/select). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.2 KiB
Quality scorecard (the ratchet)
Cheap to measure (grep -c, wc -l, clj-kondo), recorded before/after each
migration in the commit message and in the results table below. No metric may regress
for the touched modal without a written exception in gotchas.md. These are directional
evidence, not targets to game — always paired with the e2e parity gate.
Heuristics
| # | Heuristic | Measure | Target |
|---|---|---|---|
| 1 | Faked cursor positions (not cursors themselves) | grep -cE 'with-cursor|MapCursor\.' re-roots + grep -c 'defn.*-no-cursor' |
→ 0 (top-rooted cursors are fine) |
| 2 | Implicit state merges (snapshot/cursor) | count merge sites | → 0 (forms); explicit put-step only (wizards) |
| 3 | Branching complexity | clj-kondo, or count cond/condp/case/nested if + max depth |
net ↓ |
| 4 | Lines of code | wc -l on the modal's file(s) |
net ↓ |
| 5 | Reuse / cross-form similarity | cookbook components reused; duplicated-block count | reuse ↑, dup ↓ |
| 6 | Route count | count routes for the modal | → 2 (+1 for add-row) |
| 7 | OOB swaps | grep -c hx-swap-oob |
→ 0 unless a justified disjoint-region case is documented |
| 8 | Attribute consistency | mixed :x-/"x-" encodings in migrated template |
→ 0 |
How to measure (copy/paste)
F=src/clj/auto_ap/ssr/<modal>.clj
echo "LOC $(wc -l < $F)"
echo "no-cursor twins $(grep -c 'defn.*-no-cursor' $F)"
echo "faked-cursor roots $(grep -cE 'with-cursor|MapCursor\.' $F)"
echo "snapshot merges $(grep -c ':multi-form-state :snapshot' $F)"
echo "branch forms $(grep -cE '\(cond |\(condp |\(case |\(when-not ' $F)"
echo "hx-swap-oob $(grep -c 'hx-swap-oob' $F)"
echo "mixed string hx- $(grep -cE '\"hx-[a-z]' $F)"
# route count: count this modal's entries in src/cljc/auto_ap/routes/*.cljc
Results
Each migration appends one row (after-numbers), referencing the before in the diff.
| Phase | Modal | LOC | Routes | no-cursor twins | faked roots | snapshot merges | OOB | mixed hx- | cookbook reused / added |
|---|---|---|---|---|---|---|---|---|---|
| 1 (baseline) | Transaction Edit transaction/edit.clj |
1608 | ~12 | 1 | 2 | ~75 | 0 | 8 | — / seeded 7 entries |
| 2 | Transaction Edit transaction/edit.clj |
1593 | ~5 | 0 | 0 | 0 round-trip | 0 | 8 (shared) | location-select / 1 Selmer |
| 2-final | Transaction Edit (full Selmer + wizard removed) | 1548 | 5 | 0 | 0 | 0 | 0 | 0 | full sc/* lib / ~30 partials |
| 3 | Transaction Bulk Code transaction/bulk_code.clj |
506 (was 420 — see exception) | 3 | 0 | 0 | 0 | 0 | 0† | reused all of Phase-2's sc/* lib + account-typeahead*/location-select* + edit-modal/transitioner chrome / added sc/select |
† The one "hx-..." string hit is a response-header map ({"hx-trigger" "refreshTable, reset-selection"}), not a mixed attribute encoding. mm coupling 19→0, wizard records 3→0, step-params 10→0 (the 2 hits are comments), Hiccup-in-render → 0 except the shared com/success-modal (heuristic-9 exception, as in Phase 2).
New heuristics introduced at 2-final (full Selmer)
| # | Heuristic | Measure | Target |
|---|---|---|---|
| 9 | Hiccup HTML tags in the render path | grep -cE '\[:(div|span|p|a|button|input|h[1-6]|ul|li|label|select|option|t(able|head|body|r|d|h)|form|svg|template)' over the modal's render fns |
→ 0 (success-modal confirmation dialogs may keep the shared Hiccup component) |
| 10 | mm wizard coupling | grep -c 'mm/' the modal file + grep -c 'defrecord.*Wizard|ModalWizardStep' |
→ 0 for a single-step modal |
Phase 2 progress. Achieved with parity held (swap spec 6/6, transaction-edit spec 8/8, full suite 38 pass / 1 unrelated fail / 0 skip, up from 30/2/7):
- deleted the dead
*-no-cursor*twin (no-cursor 1→0);- de-faked the simple-mode cursor (faked roots 2→0) via explicit data + explicit field names (
account-field-name) + explicit error lookup — the render-fn rewrite thewith-field-defaultshortcut couldn't do;- collapsed the 5 manual-coding operation routes into one
edit-form-changeddispatcher (routes ~12→~5; the operations are now pureapply-*fns);- fixed a real production bug (
:mode→ 500 on every advanced manual save);- greened
transaction-edit.spec.ts(8/8) and matured the skill.Phase 2 complete. The wizard→plain-form rewrite removed the snapshot round-trip (heuristic 2 → 0) and the first interactive component (
location-select) is migrated to a Selmer template (selmer-conventions.mdvalidated). Remaining for later phases: drop the now-thinmm/ModalWizardStepprotocol wrappers, and the cross-cutting Phase 11 Selmer sweep of the sharedcom/typeahead/com/select/com/button-group-button(those shared call sites hold the last 8 mixed@/:-attr offenders; they clear when the shared components move to Selmer — not a single-modal task, per Open decision 2).
Phase 2-final — full Selmer + wizard removed. Every component the modal renders through was ported to a Selmer partial under
resources/templates/components/with a thin Clojure wrapper inauto-ap.ssr.components.selmer(sc/*); the modal's own structure lives underresources/templates/transaction-edit/. Themmwizard abstraction (EditWizard/LinksSteprecords,MultiStepFormState,step-params[...]field names,wrap-wizard/wrap-decode-multi-form-statemiddleware) was deleted — there was only ever one step, so it was pure overhead. Result: heuristic 8 (mixed hx-) and 9 (Hiccup in render) and 10 (mm coupling) all → 0; theedit-wizard-navigateroute is gone (routes 5). Parity held: swap spec 6/6, transaction-edit spec 8/8, full suite 38 pass / 1 pre-existing unrelated fail (serial, fresh seed). The only Hiccup left in the file is the post-savecom/success-modalconfirmation dialogs (terminal, shared component — out of the form's render path). Seeform-vs-wizard.md(drop-the- wizard test),selmer-conventions.md(composition mechanics), andgotchas.md(stray-field decode leak; jetty reload staleness).
Phase 3 — Transaction Bulk Code (first cold apply of the mature skill). Single-step form wearing a full wizard costume (
BulkCodeWizard/AccountsStep,MultiStepFormState, thestep-params[...]prefix, the oldfind *location swap). Migrated to a plain form by mirroring Phase 2 — and it was mostly reuse: the entiresc/*Selmer component library,account-typeahead*/location-select*, and theedit-modal/transitionerchrome were imported wholesale; the only new shared component wassc/select(the status dropdown —location-select.htmlgeneralized). Parity held: bulk-code spec 13/13, full suite 39/39 (up from the Phase-2 baseline of 38–39). mm coupling 19→0, snapshot merges 4→0, wizard records 3→0, routes 4→3 (open / submit /form-changed— the per-opnew-account+vendor-changedroutes folded into oneform-changedop dispatcher), the location swap moved offfind *onto explicit#account-location-<index>+hx-select.The one regression — LOC 420→506 (documented exception, see
gotchas.md). Unlike edit (whose wizard held real custom code), bulk-code's wizard was a thin shell that delegated almost everything tomm/*defaults (default-render-step,default-render-wizard,submit-handler,open-wizard-handler). Ripping the wizard out moves that previously-shared plumbing into the file as explicit render/decode/submit/handler code. The trade is intended: every other heuristic improved and the modal is now self-contained and wizard-free. New patterns added to the cookbook: the selection-as-ids[]round-trip (resolve the non-editable selection to a concrete id vector at open, ride it in hidden fields — the bulk analog of edit's singledb/id), and the:id-keyed vendor typeahead (a value-bound hidden must be keyed or its posted value goes stale across a whole-form swap).