diff --git a/docs/plans/2026-06-02-001-refactor-ssr-rendering-modernization-plan.md b/docs/plans/2026-06-02-001-refactor-ssr-rendering-modernization-plan.md new file mode 100644 index 00000000..241f120c --- /dev/null +++ b/docs/plans/2026-06-02-001-refactor-ssr-rendering-modernization-plan.md @@ -0,0 +1,502 @@ +# 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"`).