# 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) ```bash F=src/clj/auto_ap/ssr/.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). | 4 | POS Sales Summary `pos/sales_summaries.clj` | **732** (was 790) | **6** modal | 0 | 0 | **0** | 1✦ | 0✦ | reused `sc/*` lib + `edit-modal`/`transitioner` chrome / added the inline click-to-edit **account-cell** + **manual-items** patterns | ✦ The residual 1 `hx-swap-oob` and the `"hx-..."` string hits all live in the **grid page** code (the `grid-page` render lambdas + the `filters` form + the submit response-header map) — none are in the migrated **modal** render path, which is 100% Selmer. `defrecord` count **0** (all 4 wizard records gone), `fc/` cursor refs 51→**0**, mm coupling 20→**0**, step-params 27→**0** (2 comments). LOC dropped (this wizard held real custom code, unlike bulk-code's thin shell). **Two pre-existing bugs fixed** (per the user's call): the "New Summary Item" add button (was throwing `newRowIndex is not defined`) and the dead totals/balance display. ### 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 the > `with-field-default` shortcut couldn't do; > - **collapsed the 5 manual-coding operation routes into one `edit-form-changed` > dispatcher** (routes ~12→~5; the operations are now pure `apply-*` 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.md` validated). Remaining for *later phases*: drop > the now-thin `mm/ModalWizardStep` protocol wrappers, and the cross-cutting Phase 11 > Selmer sweep of the shared `com/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 in `auto-ap.ssr.components.selmer` (`sc/*`); the modal's own > structure lives under `resources/templates/transaction-edit/`. The `mm` wizard > abstraction (`EditWizard`/`LinksStep` records, `MultiStepFormState`, `step-params[...]` > field names, `wrap-wizard`/`wrap-decode-multi-form-state` middleware) 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**; the `edit-wizard-navigate` route 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-save `com/success-modal` confirmation dialogs (terminal, > shared component — out of the form's render path). See `form-vs-wizard.md` (drop-the- > wizard test), `selmer-conventions.md` (composition mechanics), and `gotchas.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`, > the `step-params[...]` prefix, the old `find *` location swap). Migrated to a plain form by > mirroring Phase 2 — and it was mostly **reuse**: the entire `sc/*` Selmer component library, > `account-typeahead*`/`location-select*`, and the `edit-modal`/`transitioner` chrome were > imported wholesale; the only new shared component was **`sc/select`** (the status dropdown — > `location-select.html` generalized). 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-op `new-account` + > `vendor-changed` routes folded into one `form-changed` op dispatcher), the location swap moved > off `find *` onto explicit `#account-location-` + `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 to `mm/*` 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 single `db/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). > **Phase 4 — POS Sales Summary (first modal with no prior test coverage).** The largest > migration so far and the first that required **building the parity gate first**: the modal > had zero e2e/clj tests and the test server seeded no POS data, so the work began by seeding a > balanced sales summary + writing a 7-test characterization spec (committed separately, ahead > of the rewrite). Then the standard wizard→plain-Selmer migration: `MainStep`/`EditWizard` + > `MultiStepFormState` deleted, the 51 `fc/` cursor refs de-cursored into explicit data + > Selmer, `step-params` dropped, the EDN snapshot replaced by flat `wrap-decode`/`wrap-derive-state` > (with a **db/id-keyed item merge** so the read-only fields the form doesn't post — > ledger-side, amount — survive a re-render). The **inline click-to-edit account cell** (pencil → > typeahead editor → check/cancel) was preserved as three small targeted `.account-cell`-swap > routes (a distinct feature, not folded into the form-changed dispatcher). LOC 790→**732** (net > ↓ — a fat wizard, opposite of bulk-code). > > **Characterize-then-fix.** Writing the gate surfaced two pre-existing bugs: the "New Summary > Item" button threw `newRowIndex is not defined` (dead since forever) and the totals/balance > display was dead code (malformed Hiccup that discarded its labels). The spec first *documented* > them as broken (never assert a bug as working); then, on the user's call, the migration **fixed > both** — add-item is now a whole-form-swap `op=new-item` adding an editable manual row, and a > proper `#summary-totals` block shows running Total + Balanced/Unbalanced (a Rule-4 targeted swap > on manual amount edits). The spec was updated to assert the fixed behavior. New cookbook entries: > the **inline click-to-edit cell** and the **db/id-keyed item merge** for partially-posted rows. > **Phase 5 — Invoice Bulk Edit (cold apply in a 1812-line shared file).** Structurally > Phase 3's bulk-code applied to invoices (selected entities → expense-account rows: > account/location/percentage), so it was near-pure reuse: bulk-code's flat-state plumbing > (ids round-trip, `wrap-bulk-state`, schema/decode) + edit's `account-totals-tbody` for the > live totals. `BulkEditWizard`/`AccountsStep` + `MultiStepFormState` deleted, `step-params` > dropped, the rows de-cursored to Selmer with the explicit-id location swap, bulk-edit > routes 5→**3** (the `new-account` + `total` + `balance` routes folded into one > `form-changed` op dispatcher + the sibling-`` totals swap). **Implemented the dead > TOTAL/BALANCE display** (the wizard had them commented out with a duplicate `id="total"`) > as a `#expense-totals` sibling-`` refreshed by a Rule-4 percentage-keyup swap. > Parity held: invoice-bulk-edit spec 5/5, full suite 50/50. > > **Editing a wizard buried in a large shared file:** the clojure-mcp structural tools > (`clojure_edit` / `replace_sexp`) **reformat the whole file** — here that was a spurious > 650-line whitespace diff that would bury the real change. For a surgical migration inside a > big multi-modal file, use the **text-based Edit tool** instead (the AGENTS.md "absolutely > necessary" carve-out), then `load-file` + `cljfmt` to verify. The resulting diff was fully > contained to the requires + the bulk-edit region. > > **Repeated-row target-selector convention — settled (the Phase 5 exit criterion).** Across > edit / bulk-code / sales-summary / invoice-bulk-edit the convention converged on: **explicit > per-row ids** (`#account-location-`, `#account-row-`) for a cell-local swap > (Rule 2), and a **single stable-id sibling-``** (`#account-totals` / `#expense-totals`) > for running totals (Rule 4) — *not* data-attribute selectors or a `form-path→selector` > helper. Per-row ids are generated from the row index the form already uses for field names > (`path->name2`), so server and markup agree by construction. Whole-form swap (Rule 3) covers > structural changes (add/remove row). This is now the cookbook default; see `swap-doctrine.md`. > **Phase 6 — the wizard engine, and its first real modal (Transaction Rule).** The inflection > phase. (a) **Engine** (`6a`, committed separately): `wizard-state` + `wizard2`, the Django > `formtools` SessionStorage model, REPL-proven before any modal touched it. (b) **First real > modal** (`6b`): the Transaction Rule wizard (edit step + read-only test/preview step) migrated > onto the engine and **fully de-cursored** like Phases 2-5. Scorecard (`admin/transaction_rules.clj`): > `fc/` cursor refs **82 -> 0**, `mm/` coupling **20 -> 0**, defrecords **3 -> 0** (EditModal / > TestModal / TransactionRuleWizard all gone), LOC 1000 -> 964, the 4 wizard routes > (open/navigate/save + per-dialog) collapse to **2** (`open-rule-wizard` for new+edit, > `save-step` for every transition). Parity held: rule spec **4/4**, full suite **55/55**. > > **The engine generalizes even for a one-data-step "wizard".** Transaction Rule is *edit + a > read-only preview of the same entity*, not two independent data steps — so it exercises the > engine's render / navigation / `:all-data`-preview path but not the cross-step *merge* (that > waits for Phase 7's Invoice Pay). The test step's `:render` reads `:all-data` (the engine's > `get-all`), which here is just the edit step's rule — so the formtools "combine at the end" > mechanism is exactly what feeds the preview table. Nav is the engine's `direction` field > (plain submit buttons `name="direction" value="next|back|submit"`), so the per-step > `navigate` route is deleted. > > **Note (scope):** the de-cursored edit step keeps `com/*` Hiccup leaf components rather than > porting to `sc/*` Selmer partials — the modal's value was removing `fc/` + `mm/` and proving > the engine, not re-templating its (conditional, Alpine-cross-field) layout. Hiccup-in-render > (heuristic 9) is therefore a documented partial here; the leaf-component `com/ -> sc/` swap is > a mechanical follow-up. The Alpine cross-field dispatch wiring (clientId -> accountId -> > location) was preserved verbatim — de-cursoring touched only the data plumbing. > **Phase 7 — Invoice Pay: the engine's cross-step merge, finally proven.** The first > *genuine* multi-data-step wizard (every prior one was single-data-step wearing wizard > costume, or edit+preview of one entity). Step 1 `choose-method` collects > `{:bank-account :method}`; step 2 `payment-details` collects > `{:invoices :check-number :handwritten-date :mode}`; the engine's `get-all` **merges the two > independent payloads** for the per-method `pay!` (handwrite-check transacts a pending check; > the others go through `print-checks-internal`). This is the exact mechanism the Phase-6 > reviewer flagged as unproven — now exercised end-to-end (gate 3/3: choose-method renders the > bank-account + methods → handwrite-check advances to details → check number + submit shows > the completion modal). Conditional rendering by method (handwrite shows check-number, print > shows date) lives in step 2's render, reading `:method` from `:all-data`. > > **The whole file falls off the framework.** Invoice Pay was the *last* `mm`/`fc` user in > `invoices.clj` (bulk-edit went in Phase 5), so the migration zeroed the file: `fc/` cursor > refs **0**, `mm/` **0**, `defrecord` **0** (PayWizard + ChoosePaymentMethodModal + > PaymentDetailsStep all gone), `step-params` **0** — and the `multi-modal` / `form-cursor` / > `malli.util` requires were deleted outright. The pay wizard's 3 routes (open / navigate / > submit) collapse to **2** (open = `open-pay-wizard`, every transition = `pay-step`); the > cards post `{bank-account, method, direction:next}` straight to the engine submit-route > instead of a bespoke navigate route. > > **Engine dividends from the review follow-up paid off here.** This migration *used* the > primitives the engine absorbed after Phase 6: `:open-response` (modal wrap, so open is one > handler), `nav-footer` (with the new `:save-label "Pay"`), the auto nav-field stripping (the > flat `{bank-account, method}` decode needs no allowlist), and the Enter guard — so the > consumer is config + render + the per-method `pay!`, not framework plumbing. > > **New constraint discovered:** wizard session data must be EDN-safe (the cookie store has no > clj-time readers) — see `gotchas.md`. The de-cursored amounts grid stores the enriched > invoice list in `:context`, which is fine at gate scale (1 invoice) but is the session-bloat > risk the reviewer named; a leaner context (ids + amounts, re-query in render) is the > follow-up if real payments carry many invoices.