From a2d8517668511f15f2e9dca26d46c2ec91ed3251 Mon Sep 17 00:00:00 2001 From: Bryce Date: Thu, 25 Jun 2026 14:38:07 -0700 Subject: [PATCH] refactor(ssr): wizard2 engine absorbs the per-consumer boilerplate (review follow-up) Adversarial review of Phase 6 found the engine's coupling had relocated rather than dissolved: every wizard consumer had to hand-build a decode allowlist, re-implement the open-handler modal wrap, mint temp ids for added rows, and hand-roll the nav buttons + Enter guard. The engine had the information to prevent all four. Now it does: - handle-step-submit strips its own nav fields (wizard-id/current-step/direction) from form-params before calling a step's :decode -- no per-consumer allowlist, and they can no longer leak into the saved entity (the Phase-6 "500 on save" class of bug is structurally impossible). - open-wizard takes an :open-response config fn and owns the create!/render/wrap/thread flow, so modal wizards route through (partial wizard2/open-wizard config) directly. - wizard2/blank-row supplies a temp :db/id (+ :new?) so an added row passes schema validation and the step actually advances. - wizard2/nav-footer emits the direction buttons (Back/advance/Save), marks the primary, and wizard-form guards Enter to trigger the primary button. Consumer (transaction_rules.clj) gets correspondingly leaner: deleted rule-form-keys + the decode allowlist, rule-nav, and the hand-rolled open-rule-wizard; new/edit routes are now (partial wizard2/open-wizard config). A new wizard is now just a config map + the step :render fns. LOC 964 -> 932, and the deleted code was exactly the cross-consumer boilerplate, not modal-specific logic. Verification: rule spec 4/4; full suite 55/55; cljfmt clean. Skill gotchas updated from "three traps" to "use the engine's primitives" (the engine now absorbs them). Co-Authored-By: Claude Opus 4.8 --- .../ssr-form-migration/reference/gotchas.md | 40 +++++++----- .../auto_ap/ssr/admin/transaction_rules.clj | 62 +++++-------------- src/clj/auto_ap/ssr/components/wizard2.clj | 54 +++++++++++++--- 3 files changed, 86 insertions(+), 70 deletions(-) diff --git a/.claude/skills/ssr-form-migration/reference/gotchas.md b/.claude/skills/ssr-form-migration/reference/gotchas.md index 8e62f904..ff1334f3 100644 --- a/.claude/skills/ssr-form-migration/reference/gotchas.md +++ b/.claude/skills/ssr-form-migration/reference/gotchas.md @@ -265,23 +265,31 @@ carve-out. Verify with `load-file` (compile) + `lein cljfmt check`, not by eyeba diff is contained with `git diff -U0 | grep '^@@'` — the hunks should cluster only where you edited (requires + the modal region), nothing else. -## Wiring a modal onto the wizard2 engine — three traps that cost a debug cycle each +## Wiring a modal onto the wizard2 engine — use the engine's primitives, don't re-roll them -1. **Strip the engine's nav fields in the step `:decode`.** The posted form carries - `wizard-id` / `current-step` / `direction` alongside the real fields. If the step schema is - an open `:map` (most are), `mc/decode` keeps them, they ride into `get-all`, and the save's - `:upsert-entity` dies with `:db.error/not-an-entity ... :current-step`. Fix: `select-keys` - the decode to the schema's known top-level keys (the same allowlist trick as the flat-form - migrations). Symptom is a **500 on save**, not a validation message. -2. **New repeated-row needs a temp `:db/id` or the step can't advance.** If the row schema - requires `[:db/id [:or entity-id temp-id]]`, an added row with no id fails per-step - validation, so the engine re-renders the *same* step instead of advancing — looks like "the - Next/Test button does nothing." Give new rows `(str (java.util.UUID/randomUUID))`. -3. **Nav is a `direction` field, and Back/Save are both submit buttons.** The footer buttons - are plain `