# SSR Rendering Modernization — Rollout Plan > **Status:** Planning / for future reference. Nothing here is committed. > **Author:** Bryce + Claude, 2026-06-02 > **Scope:** Three intertwined SSR refactors and a strategy for rolling them out > incrementally and low-risk, where each migration leaves behind a sharper > **skill** so the next migration is cheaper. --- ## 1. Purpose Three separate exercises have each produced evidence for a way to simplify the SSR layer. This document does **not** re-derive them — it sits on top of them and answers a single question: > How do we land all three with minimal blast radius, in an order where each > step de-risks the next, and where the learnings compound into tooling instead > of evaporating? The three workstreams: | # | Workstream | Source | What it is | |---|------------|--------|------------| | A | **HTMX swap doctrine** | `integreat-render-whole-form` worktree | Empirical: how to re-render forms on field edits without losing focus/caret, with the fewest moving parts | | B | **Wizard architecture** | `integreat-critique-wizard` worktree (`docs/superpowers/plans/2025-01-15-wizard-refactor.md` + `-phase-i-trial.md`) | Architectural: replace the `multi_modal.clj` protocol/middleware/EDN-snapshot system with data-driven config + pure render functions | | C | **Hiccup → Selmer** | This conversation | Templating: move HTML rendering from Hiccup data to Jinja-style Selmer templates so Alpine/HTMX attributes are first-class HTML, not keyword/string soup | These are not independent. They all touch the same render functions, and the right sequencing makes them reinforce each other (§3, Decision 4). --- ## 2. What each workstream actually established ### A. HTMX swap doctrine (`render-whole-form`) The branch iterated through **four** approaches on the transaction-edit form and the commit history is the real artifact (read `git log integreat-render-hx-select`): 1. `alpine-morph` whole-form morph — worked but required `@alpinejs/morph`, plus `key`/`x-data` re-init tricks and guards against stale post-morph `$refs`. 2. `hx-select` fragment swap **+ OOB** refresh of the snapshot/totals. 3. **Whole-form `hx-select` swap with ZERO out-of-band swaps** — the snapshot hidden field rides along inside the form, so a whole-form swap keeps interdependent state (accounts + amount-mode + active tab) consistent with no OOB at all. 4. **Precise per-trigger granularity** on top of #3: - **Memo** issues *no request* — it affects nothing else; its value rides along in the form and merges into the snapshot on save. - **Account select** swaps *only* that row's location cell (`#account-location-` / `#simple-account-location`) — selecting an account only changes valid Location options, a truly local effect. - **Structural changes** (vendor, add/remove row, mode toggle, $/% radio) swap the **whole** `#wizard-form` because their state is interdependent and round-trips through the single form-level snapshot. **The invariant that makes it all work:** *the input the user is typing in is never inside the region it triggers a swap of.* Totals were moved into their own `` (new `:footer-tbody` param on `data-grid-`) so an amount edit can swap totals without replacing the amount input. **Secondary learning — Alpine components must survive swaps.** `inputs.clj` had to be hardened: null-guard every `tippy` / `$refs` access (`tippy?.show()`, `$refs.input?.__x_tippy?.hide()`) and **key the typeahead by its server value** (`:key (str id "--" value)`) so a server-driven value change re-initialises Alpine instead of preserving stale `x-data`. **Status:** near-complete, 27/2 e2e passing (the 2 failures are pre-existing and unrelated). This is the most landable of the three. ### B. Wizard architecture (`critique-wizard`) A 14-task plan to retire `multi_modal.clj` (5 protocols, 15+ methods, middleware stacking, EDN-with-custom-readers serialized into hidden fields). Core moves: - **Wizards become data:** `{:steps [{:key :schema :fields :render :next}...] :init-fn :done-fn}`. - **Render functions become pure:** one function that takes an explicit data map, killing the `*-no-cursor*` duplicate-variant problem caused by dynamic bindings (`*form-data*`, `*current*`, `*prefix*`) in form-cursor. - **State moves server-side**, keyed by a `wizard-id` UUID token, replacing the EDN snapshot in the form. - **Reclassification:** 4 of the 9 "wizards" are single-step and shouldn't be wizards at all — they become plain forms (transaction-edit, bulk-code, sales-summary edit, invoice bulk-edit). Inventory (verify line counts at migration time — `edit.clj` is currently 1584, `clients.clj` 1913, `multi_modal.clj` 400): | Name | File | Steps | Target type | |------|------|-------|-------------| | New Invoice | `invoice/new_invoice_wizard.clj` | 3 | wizard | | Transaction Edit | `transaction/edit.clj` | 1 (mode toggle) | **normal form** | | Transaction Bulk Code | `transaction/bulk_code.clj` | 1 | **normal form** | | Sales Summary Edit | `pos/sales_summaries.clj` | 1 | **normal form** | | Invoice Pay | `invoices.clj` | 2 | wizard | | Invoice Bulk Edit | `invoices.clj` | 1 | **normal form** | | Vendor | `admin/vendors.clj` | 5 | wizard | | Client | `admin/clients.clj` | 7 | wizard | | Transaction Rule | `admin/transaction_rules.clj` | 2 | wizard | > The critique plan's `wizard_trial/` and `bulk_code_trial.clj` spike code on the > branch is a Phase-I proof, not production code. Treat it as reference. ### C. Hiccup → Selmer **Rationale (your framing):** Clojure keywords collide conceptually with Alpine attributes, and the codebase is genuinely inconsistent about it. Concrete evidence in `inputs.clj` on `staging`: - The **same** attribute appears keyworded and stringified in one file: `:x-ref "input"` (line 73) vs `"x-ref" "hidden"` (line 83); `:x-model` (line 63) vs `"x-model" "search"` (line 105). - Event handlers must be strings (`"@keydown.down.prevent.stop"`, `"x-tooltip.on.click"`) while structural Alpine attrs are keywords (`:x-init`, `:x-data`, `:x-show`). There is no rule a reader (or an LLM) can rely on. 67 SSR files use this attribute style. In a Jinja-style Selmer template, `@click`, `:class`, `x-data`, `hx-post` are all just plain HTML attributes — no escaping, no keyword/string decision, and the template reads like the HTML that ships. This is the readability win for both humans and LLMs. **Honest cost (must be designed around):** - Hiccup is *data* — composable with `for`/`map`, unit-testable as a tree, trivially manipulated. Selmer templates are files/strings; you lose tree composition and assert on rendered strings instead. (Note: the critique plan's tests already assert via `(re-find #"..." (str html))`, so that style survives the switch.) - The whole component library (`com/data-grid-row`, `com/typeahead`, …) is Hiccup-function composition. Selmer composes via `{% include %}` / `{% block %}` and template inheritance — a different model that needs a deliberate interop bridge during transition. - Selmer is **not yet a dependency** (only `hiccup "2.0.0-alpha2"`). Net-new dep. --- ## 3. Key tensions to resolve before mass migration (DECISIONS) These are the cross-workstream conflicts. Each needs an explicit decision; my recommended default is given but the call is yours. ### Decision 1 — Server-side state vs. embedded snapshot The critique plan (B, Task 1) moves all wizard state server-side behind a `wizard-id`. But `render-whole-form` (A) demonstrated that the EDN snapshot, once you swap the **whole form with zero OOB**, "rides along" cleanly and the messy part was never the snapshot — it was the morph/OOB swapping. So A partly *dissolves* the problem B's Task 1 solves. **Recommended default — split by type, which falls out naturally:** - **Normal forms** (the 4 reclassifications): no snapshot needed at all — plain fields + an entity id. Zero server state. Simplest possible. - **True multi-step wizards** (5): server-side state *is* worth it (drops EDN-with-custom-readers, shrinks payloads). Accept its costs explicitly: server restart loses in-flight state, multi-tab needs distinct ids, abandoned wizards need GC/TTL. Use an atom with a timestamp + sweep, as the spike did. This means **don't** put `multi_modal`'s snapshot machinery on the critical path for the single-step conversions — they get simpler by *deletion*, not by swapping one state mechanism for another. ### Decision 2 — Swap doctrine is now canon, overriding the critique plan's OOB tasks Critique Tasks 5 & 11 advocate HTMX **OOB** for related updates. `render-whole-form` *evolved past that* to zero-OOB whole-form swaps + precise local swaps. Where they conflict, **A wins** — it is the later, e2e-verified learning. **Recommended rule (this becomes the skill's swap section):** 1. Field affects nothing else → **no request** (value rides along, merged on save). 2. Field has a *purely local* effect → **targeted swap of just that cell**, with the cell given a stable id and kept out of the typed input's subtree. 3. Field touches interdependent state → **whole-form `hx-select` swap, zero OOB.** 4. **OOB is for genuinely disjoint DOM regions only.** Bryce's observation from the branch: nearly every OOB he had to fix wasn't a real "separate region" case — it was a target that *should have lived higher in the DOM hierarchy*, where a normal swap of a common ancestor would have covered it. So the rule is: **before reaching for OOB, try to hoist the target to a shared ancestor** so one ordinary swap covers both the trigger's neighborhood and the dependent element. (The totals move into `` is the canonical example — a sibling of the input rows, swapped normally, *not* OOB.) Use OOB only when the dependent element is **truly elsewhere in the page** and cannot reasonably share an ancestor: a global flash/toast, a nav badge/counter, a modal mounted at the document root. **If you're tempted to OOB inside the same feature, that's a signal to restructure the DOM, not to add an OOB.** ### Decision 3 — Selmer scope: full vs. hybrid - **Full:** every SSR file → Selmer. Maximum consistency, maximum risk/effort. - **Hybrid (recommended):** migrate the **Alpine/HTMX-heavy interactive leaves** (forms, typeaheads, wizard steps, data-grid rows) to Selmer first, where the keyword/string pain is concentrated and the LLM-confusion is worst. Leave static layout/structural Hiccup alone until proven worth it. Capture ~80% of the readability benefit for ~20% of the churn, and let the migration's own evidence decide whether to go full later. ### Decision 4 — Sequence Selmer relative to B Do **not** run Selmer as a separate sweep over files you're also restructuring for B. The shared seam is **the pure render function** (B's Task 3): both the wizard engine and Selmer want pure, data-in render units. So: > When a feature is migrated, do all three at once *for that feature*: > extract pure render fn (B) → express it as a Selmer template (C) → wire its > HTMX per the swap doctrine (A). One pass per file, three wins. This is also what makes the compounding skill coherent — every migration exercises the whole playbook. --- ## 4. Code-quality ratchet (heuristics + per-migration scorecard) The rollout only succeeds if quality monotonically *improves*. To keep that honest rather than aspirational, every migration measures a small **scorecard** before and after, and the numbers go in the commit message and a running table in the skill. The rule is a **ratchet**: a migration may not worsen any metric for the feature it touches, and shared work it introduces should *lower future* features' starting numbers. These heuristics are deliberately cheap to measure (mostly `grep -c` / `wc -l` / `clj-kondo`) so the step never gets skipped. | # | Heuristic | What it proves | How to measure (per feature) | |---|-----------|----------------|------------------------------| | 1 | **Form-cursor / dynamic-binding usage → 0** | Whole-form re-render means we never "fake out" a position with the cursor. Pure functions take explicit data. | `grep -cE 'fc/with-field|\*form-data\*|\*current\*|\*prefix\*|-no-cursor'` in the file. Target: trends to 0. | | 2 | **Implicit state merges → 0** | State is either plain form params (normal forms) or explicit keyed step-data (wizards) — never cursor/snapshot merge magic. | Count snapshot-merge / cursor-merge call sites. Target: 0 for normal forms; explicit `update-step!` only for wizards. | | 3 | **Cyclomatic complexity ↓** | Simpler branching in render + handler. | `clj-kondo` complexity, or proxy: count `cond`/`condp`/`case`/nested `if` and max nesting depth per render/handler fn. Target: net decrease. | | 4 | **Lines of code ↓** | The headline simplification. | `wc -l` on the feature's file(s). Target: meaningful net drop (critique est. ~60% across the suite). | | 5 | **Reuse / cross-form similarity ↑** | Similar forms look similar and share render units, instead of each reinventing a row/typeahead/totals block. | Count cookbook components reused (§7) and duplicated-block count across sibling forms. Target: reuse up, duplication down. | | 6 | **Route count → 2 (+1 for add-row)** | The middleware-stacking collapse. | Count routes for the feature. Target: 2 (open/GET + submit/POST), +1 only for an add-row endpoint. | | 7 | **OOB swap count → ~0** | The swap doctrine (Decision 2): OOB only for genuinely disjoint regions. | `grep -c hx-swap-oob` in the feature. Target: 0 unless a justified disjoint-region case is documented. | | 8 | **Attribute consistency** | The Selmer payoff: no keyword/string attr soup. | After Selmer conversion, Alpine/HTMX attrs are plain HTML — 0 `:x-`/`"x-"` mixed encodings in the migrated template. | **Ratchet enforcement:** if any metric regresses for the touched feature, the migration isn't done — either fix it or write down *why* it's an accepted exception in the skill's gotchas. The running scorecard table in the skill lets you see at a glance whether migration N+1 started from a better baseline than N did (it should, because of shared components). > Caveat: these are directional heuristics, not targets to game. Don't shrink LOC > by golfing, and don't inline shared components to drop a route count. The point > is to make "is this actually simpler?" answerable with evidence. --- ## 5. Testing strategy Testing is woven through every phase, not a final gate. Three levels, mapped to what each is good at, consistent with the project's `testing-conventions` skill (test user-observable behavior; assert DB state directly; don't test the means): 1. **Characterization e2e first.** *Before* touching a feature, write/confirm a Playwright spec capturing current user-observable behavior — focus/caret survival across swaps, the field round-trip, validation errors, and the actual save. `render-whole-form`'s `e2e/transaction-edit-swap.spec.ts` is the template. This spec is the parity contract the refactor must keep green. 2. **Pure-function unit checks via REPL.** Once render fns are pure (B), test the *data-prep* functions (what feeds the template) with `clojure-eval` / `clj-nrepl-eval`. Assert on returned data, and on rendered output only via the string-match style the critique tests already use (`(re-find #"accounts\[0\]\[account\]" (str html))`) — this style survives the Selmer switch since Selmer also yields strings. Do **not** write brittle structural assertions on markup. 3. **DB-state assertions for mutations.** Per `testing-conventions`: if a submit writes to Datomic, verify by querying the DB, not by asserting on markup. **Regression gate (the ratchet's safety net):** the full e2e suite must stay at or above its current baseline (**27 passing / 2 known-unrelated failures** from `render-whole-form`) after every migration. A migration that drops a previously passing spec is blocked until restored. This baseline is what makes "behavior parity" checkable rather than asserted. **Test ordering mirrors the rollout (simplest first):** the bulk-code pilot (Phase 2) is where the test recipes get written into the skill (`reference/test-recipes.md`): how to e2e a swap without flaking on focus, how to assert a Selmer render, how to fixture a wizard-id. By the time the 7-step client wizard is reached, the test recipes are mature — so the riskiest migration has the most testing leverage, not the least. --- ## 6. Guiding principles 1. **Compound, don't just complete.** Every migration ends by updating the skill (§7) and recording its scorecard (§4). A migration that didn't teach the skill something, or that can't show its numbers, isn't done. 2. **Lowest-risk-first ordering.** Start with the smallest, simplest, lowest-traffic surfaces; spend the learning on the scary ones last. The ordering is itself a quality strategy: the scorecard heuristics (§4) and test recipes (§5) mature on cheap features so the expensive ones inherit them. 3. **Strangler, not big-bang.** New engine (`wizard2`), Selmer renderer, and the swap doctrine live *alongside* `multi_modal.clj`/Hiccup. Migrate one feature at a time behind its own route. `multi_modal.clj` is deleted only when the last caller is gone. 4. **Behavior parity is proven by e2e, not by reading.** Every migrated feature keeps/gains a Playwright spec (the `render-whole-form` `e2e/transaction-edit-swap.spec.ts` is the template) asserting focus/caret survival and the round-trip. 5. **One reversible commit per feature.** Each migration is independently revertable; never leave a half-migrated wizard on `master`. --- ## 7. The compounding mechanism — a `ssr-form-migration` skill This is the heart of the request. Create one evolving skill that every migration reads first and updates last. **Location:** `.claude/skills/ssr-form-migration/SKILL.md` (matches existing project-skill convention — see `.claude/skills/testing-conventions/SKILL.md`). **Initial structure (seeded from A + B + C):** ``` .claude/skills/ssr-form-migration/ SKILL.md # the playbook (§9): decision tree + step-by-step reference/ swap-doctrine.md # Decision 2 rules, focus invariant, OOB-vs-hoist wizard-engine.md # wizard2 config shape, normal-form-vs-wizard test selmer-conventions.md # attr style, interop bridge, include/block patterns component-cookbook.md # GROWS each migration: typeahead, account-row, # data-grid, money-input, mode-toggle… as Selmer gotchas.md # GROWS: stale $refs, key-by-value, GC of wizard ids… test-recipes.md # how to e2e a swap; how to assert a Selmer render scorecard.md # the §4 heuristics + running table of every # migration's before/after numbers ``` **The contract — every migration's last step is "feed the skill":** - Did you convert a component to Selmer (typeahead, a row, a button group)? → add the before/after to `component-cookbook.md`. The next feature that uses that component now copies instead of rediscovers. - Hit a swap/focus/Alpine surprise? → one entry in `gotchas.md`. - Found a step the playbook didn't cover, or a wrong instruction? → fix `SKILL.md`. - Measured the scorecard (§4)? → append the row to `scorecard.md` so the trend across migrations stays visible. **Success metric for the mechanism:** each successive migration should touch fewer *novel* problems, reuse more cookbook entries, and start from a better scorecard baseline than the last. If migration N+1 isn't faster than N, or its scorecard isn't trending the right way, the skill-update step is being skipped — treat that as a process bug. Track this explicitly in each migration's commit message ("reused: X,Y; new cookbook entries: Z; scorecard: LOC −n, cursor-uses −m, OOB 0"). > Consider `/ce-compound` after each migration to capture the durable learning, > then distill the reusable parts into the skill. --- ## 8. Phased rollout Ordering rationale: **(1)** land the already-finished mechanics work; **(2)** build the shared foundation + skill; **(3)** prove the full playbook on the smallest single-step form; **(4)** walk up the difficulty curve. The progression is strictly simplest → biggest, so the heuristics (§4) and test recipes (§5) mature on cheap features and the riskiest migration (the 7-step client wizard) inherits the most tooling. Week labels are relative effort markers, not commitments. ### Phase 0 — Decisions & foundation - Resolve Decisions 1–4 (§3). Record the answers at the top of the skill. - Add `selmer` dep. Build the **render helper** + **base/layout templates** + the **interop bridge** (render Hiccup → string for inclusion in a Selmer template, and vice versa) so mixed-mode is possible during transition. - Establish the Alpine/HTMX attribute convention in `selmer-conventions.md`. - Scaffold the `ssr-form-migration` skill with the A/B/C reference docs. - **Exit criteria:** a throwaway "hello" Selmer page renders inside the existing layout, and a Hiccup component renders inside a Selmer template (interop proven). ### Phase 1 — Land `render-whole-form` (workstream A, mostly done) - Merge/rebase `integreat-render-hx-select` to `master` via the gitea-tea flow. - This ships the swap doctrine on transaction-edit and the hardened typeahead **in Hiccup** (do not block this on Selmer). - Extract the doctrine into `reference/swap-doctrine.md`. - **Why first:** highest value-to-risk; it's verified and self-contained, and it gives the skill its first concrete, battle-tested section. ### Phase 2 — Pilot the full playbook on the smallest form: **Bulk Code** - `transaction/bulk_code.clj` (~420 lines, single step) → normal form, pure render fns, **Selmer** templates, swap doctrine. No server state needed. - This is critique's own chosen Phase-I subject — small, contained, low traffic. - This is the first time A+B+C run together on one feature. Expect the cookbook to fill up fast (typeahead, account row, money input, submit). - Also the first time the **scorecard** (§4) and **test recipes** (§5) are written down — this pilot sets the baseline numbers everything else is judged against. - **Exit criteria:** e2e parity (suite still ≥27/2); scorecard shows LOC down, cursor-uses → 0, OOB 0, routes = 2; the next migration can copy ≥2 cookbook entries. ### Phase 3 — Remaining single-step "normal form" conversions Lowest-complexity-first; each reuses the Phase-2 cookbook: 1. **Sales Summary Edit** (`pos/sales_summaries.clj`, ~780) 2. **Invoice Bulk Edit** (`invoices.clj`, ~700) — has account rows + totals; exercises the targeted-totals-swap pattern from A directly. 3. **Transaction Edit** (`transaction/edit.clj`, ~1584) — swap mechanics already solved in Phase 1; here we *also* convert it to pure fns + Selmer and drop the wizard wrapper. Largest single-step; do it once the cookbook is rich. ### Phase 4 — True multi-step wizards (build `wizard2` here) Now build the data-driven engine (critique Tasks 1–2) — but only when the first real wizard needs it, smallest-first: 1. **Transaction Rule** (2 steps, ~1005) — simplest real wizard; shakes out the engine + server-side state (Decision 1). 2. **Invoice Pay** (2 steps) — conditional rendering by payment method. 3. **New Invoice** (3 steps) — the critique plan's proven 3-step reference. 4. **Vendor** (5 steps, ~917). 5. **Client** (7 steps, ~1913) — the monster, last, with the fullest skill. ### Phase 5 — Cleanup - Delete `multi_modal.clj` and the compatibility shim once the last caller is gone. - Remove `@alpinejs/morph` if no longer referenced (A already removed it for transaction-edit). - Decide (Decision 3) whether to push Selmer into the remaining static Hiccup, now that the skill makes it cheap. - Final skill pass: promote recurring cookbook entries into shared Selmer partials/components. --- ## 9. Per-migration playbook (the repeatable loop) Every feature migration, regardless of phase, runs this loop. It belongs in `SKILL.md`: 1. **Read the skill.** Note which cookbook entries and gotchas apply. 2. **Classify.** Single-step → normal form (no server state). Multi-step → wizard (`wizard2` config + server state). When in doubt, it's a form. 3. **Baseline the scorecard (§4).** Record the before-numbers (LOC, cursor-uses, OOB count, route count, complexity proxy) so the after-numbers mean something. 4. **Characterize current behavior (test-first).** *Before touching code*, write/ confirm an e2e spec capturing user-observable behavior (focus/caret, round-trip, validation, the actual DB mutation). This is the parity contract. 5. **Extract pure render functions** from cursor/protocol code — this is where heuristics 1 & 2 (kill form-cursor faking and implicit merges) get paid. 6. **Templatize in Selmer** — convert those pure fns to templates; pull repeated bits from the cookbook; add any new ones back. This is where heuristic 5 (reuse) and 8 (attr consistency) get paid. 7. **Wire HTMX per the swap doctrine** (§Decision 2): no-request / targeted / whole-form; focus invariant intact; OOB only for genuinely disjoint regions (else hoist to a common ancestor). Heuristic 7. 8. **Routes:** collapse the 4–20 middleware-stacked routes to 2 (open/GET + submit/POST), or +1 for an add-row endpoint. Heuristic 6. 9. **Verify:** run the feature's e2e spec *and* the full suite (must stay ≥27/2); assert the DB mutation directly per `testing-conventions`; REPL-check pure fns with `clojure-eval`. Re-measure the scorecard — no metric may regress. 10. **Commit** one reversible feature commit. Message records reused vs. new cookbook entries and the scorecard delta (§7). 11. **Feed the skill.** Update cookbook / gotchas / test-recipes / scorecard / SKILL.md. *This step is not optional.* --- ## 10. Risk register | Risk | Mitigation | |------|------------| | Server-restart loses in-flight wizard state | Confine server-side state to true multi-step wizards; normal forms hold no state. Add TTL + sweep; consider Datomic-backed store if a wizard is long-lived. | | Mixed Hiccup/Selmer interop gets messy mid-transition | Build + test the interop bridge in Phase 0 before any feature migration. Strangler keeps both valid. | | Selmer loses Hiccup's structural testability | Lean on e2e + DB-state assertions (already the project's testing-conventions stance); reserve unit tests for the pure data-prep functions that feed templates, not the markup. | | Big files (`clients.clj` 1913, `edit.clj` 1584) hide behavior | They go *last*, after the skill is rich; characterization e2e specs first; split into sub-tasks per step. | | Alpine components break across swaps | Codify A's hardening (null-guarded `tippy?`/`$refs`, key-by-server-value) as a cookbook entry applied to every interactive component. | | Three concurrent changes per file obscure regressions | One feature per reversible commit; e2e parity gate before each merge. | | Skill-update step gets skipped under time pressure | Make "reused/new cookbook + scorecard delta" a required line in the commit message; if migration N+1 isn't faster, flag it. | | Selmer dep churn / unknowns | Spike it in Phase 0 on a throwaway page before betting features on it. | | Heuristics get gamed (LOC golfing, route-count tricks) | They're directional evidence, not targets; pair every scorecard with the e2e parity gate so "simpler" can't mean "broken." Review the scorecard trend, not single numbers. | | Quality regresses silently on a feature | Ratchet rule: no metric may regress for a touched feature; a regression blocks the migration unless an explicit exception is written in `gotchas.md`. | --- ## 11. Open decisions to confirm (owner: Bryce) 1. **Decision 1** — server-side state only for multi-step wizards, none for normal forms? (recommended yes) 2. **Decision 2** — adopt the zero-OOB whole-form swap doctrine as canon, overriding critique Tasks 5 & 11? (recommended yes) 3. **Decision 3** — Selmer hybrid (interactive leaves first) vs. full sweep? (recommended hybrid) 4. **Decision 4** — do A+B+C together per feature on the shared pure-fn seam, rather than three separate sweeps? (recommended yes) 5. **Scope check** — is landing `render-whole-form` (Phase 1) blocked on anything, or can it merge now independent of the rest? --- ## Appendix — source pointers - Swap doctrine commits: `git log integreat-render-hx-select` (worktree `/home/noti/dev/integreat-render-whole-form`). Key files: `transaction/edit.clj`, `components/inputs.clj`, `components/data_grid.clj` (`:footer-tbody`), `e2e/transaction-edit-swap.spec.ts`, `resources/public/js/htmx-disable.js`. - Wizard plan: `integreat-critique-wizard` worktree, `docs/superpowers/plans/2025-01-15-wizard-refactor.md` (14 tasks) and `2025-01-15-wizard-phase-i-trial.md` (the isolated trial). Spike code under `src/clj/auto_ap/ssr/components/wizard_trial/` and `src/clj/auto_ap/ssr/transaction/bulk_code_trial.clj`. - Attribute inconsistency evidence: `src/clj/auto_ap/ssr/components/inputs.clj` on `staging` (e.g., `:x-ref` vs `"x-ref"`, `:x-model` vs `"x-model"`).