Synthesize three SSR refactor exercises into one low-risk, compounding rollout plan: the render-whole-form HTMX swap doctrine, the critique-wizard architecture simplification, and a Hiccup -> Selmer templating migration. Includes a code-quality ratchet (per-migration scorecard), an explicit test-first strategy with an e2e regression gate, simplest-first phasing, and a self-reinforcing ssr-form-migration skill so each migration makes the next cheaper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
29 KiB
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):
alpine-morphwhole-form morph — worked but required@alpinejs/morph, pluskey/x-datare-init tricks and guards against stale post-morph$refs.hx-selectfragment swap + OOB refresh of the snapshot/totals.- Whole-form
hx-selectswap 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. - 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-<idx>/#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-formbecause 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
<tbody id="account-totals"> (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-idUUID 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/andbulk_code_trial.cljspike 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):
- Field affects nothing else → no request (value rides along, merged on save).
- 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.
- Field touches interdependent state → whole-form
hx-selectswap, zero OOB. - 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
<tbody id="account-totals">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 |
| 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):
- 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'se2e/transaction-edit-swap.spec.tsis the template. This spec is the parity contract the refactor must keep green. - 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. - 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
- 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.
- 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.
- Strangler, not big-bang. New engine (
wizard2), Selmer renderer, and the swap doctrine live alongsidemulti_modal.clj/Hiccup. Migrate one feature at a time behind its own route.multi_modal.cljis deleted only when the last caller is gone. - Behavior parity is proven by e2e, not by reading. Every migrated feature
keeps/gains a Playwright spec (the
render-whole-forme2e/transaction-edit-swap.spec.tsis the template) asserting focus/caret survival and the round-trip. - 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.mdso 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-compoundafter 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
selmerdep. 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-migrationskill 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-selecttomastervia 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:
- Sales Summary Edit (
pos/sales_summaries.clj, ~780) - Invoice Bulk Edit (
invoices.clj, ~700) — has account rows + totals; exercises the targeted-totals-swap pattern from A directly. - 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:
- Transaction Rule (2 steps, ~1005) — simplest real wizard; shakes out the engine + server-side state (Decision 1).
- Invoice Pay (2 steps) — conditional rendering by payment method.
- New Invoice (3 steps) — the critique plan's proven 3-step reference.
- Vendor (5 steps, ~917).
- Client (7 steps, ~1913) — the monster, last, with the fullest skill.
Phase 5 — Cleanup
- Delete
multi_modal.cljand the compatibility shim once the last caller is gone. - Remove
@alpinejs/morphif 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:
- Read the skill. Note which cookbook entries and gotchas apply.
- Classify. Single-step → normal form (no server state). Multi-step → wizard
(
wizard2config + server state). When in doubt, it's a form. - Baseline the scorecard (§4). Record the before-numbers (LOC, cursor-uses, OOB count, route count, complexity proxy) so the after-numbers mean something.
- 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.
- Extract pure render functions from cursor/protocol code — this is where heuristics 1 & 2 (kill form-cursor faking and implicit merges) get paid.
- 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.
- 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.
- Routes: collapse the 4–20 middleware-stacked routes to 2 (open/GET + submit/POST), or +1 for an add-row endpoint. Heuristic 6.
- 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 withclojure-eval. Re-measure the scorecard — no metric may regress. - Commit one reversible feature commit. Message records reused vs. new cookbook entries and the scorecard delta (§7).
- 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)
- Decision 1 — server-side state only for multi-step wizards, none for normal forms? (recommended yes)
- Decision 2 — adopt the zero-OOB whole-form swap doctrine as canon, overriding critique Tasks 5 & 11? (recommended yes)
- Decision 3 — Selmer hybrid (interactive leaves first) vs. full sweep? (recommended hybrid)
- Decision 4 — do A+B+C together per feature on the shared pure-fn seam, rather than three separate sweeps? (recommended yes)
- 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-wizardworktree,docs/superpowers/plans/2025-01-15-wizard-refactor.md(14 tasks) and2025-01-15-wizard-phase-i-trial.md(the isolated trial). Spike code undersrc/clj/auto_ap/ssr/components/wizard_trial/andsrc/clj/auto_ap/ssr/transaction/bulk_code_trial.clj. - Attribute inconsistency evidence:
src/clj/auto_ap/ssr/components/inputs.cljonstaging(e.g.,:x-refvs"x-ref",:x-modelvs"x-model").