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 <noreply@anthropic.com>
Proves the Phase-6a wizard engine against a real 2-step modal: the Transaction
Rule wizard (edit step + read-only test/preview step) now runs on wizard2 /
wizard-state, fully de-cursored.
What changed
- Wizard machinery removed: deleted the EditModal / TestModal /
TransactionRuleWizard defrecords (mm/ModalWizardStep + LinearModalWizard),
MultiStepFormState, the EDN snapshot, and the step-params[...] prefix. Replaced
with a data-driven `transaction-rule-wizard-config` (two steps + init-fn +
done-fn) driven by the engine.
- De-cursored the whole edit form (82 fc/ refs -> 0): every field reads explicit
data + path->name2; errors via a bound *errors* / ferr. The account row's Alpine
cross-field dispatch wiring (clientId -> accountId -> location) is preserved
verbatim — only the data plumbing moved off the cursor.
- The test step's :render reads :all-data (the engine's get-all), so the
formtools "combine at the end" mechanism feeds the preview table.
- Routes 4 -> 2: open-rule-wizard (new + edit), save-step (every transition via the
engine's `direction` field). The dedicated `navigate` route is deleted.
- decode-rule-form select-keys to the schema's known keys so the engine's nav
fields (wizard-id/current-step/direction) don't leak into the upserted entity.
Scorecard (admin/transaction_rules.clj): fc/ 82->0, mm/ 20->0, defrecords 3->0,
LOC 1000->964, routes 4->2.
Scope note: the de-cursored edit step keeps com/* Hiccup leaf components (not yet
sc/* Selmer); the value here was removing fc/ + mm/ and proving the engine, not
re-templating the conditional/Alpine-cross-field layout. Hiccup-in-render is a
documented partial; the com/ -> sc/ swap is a mechanical follow-up.
Verification: rule spec 4/4 (new + edit dialogs, advance-to-test preview, save);
full Playwright suite 55/55; cljfmt clean. Skill fed: scorecard row + narrative
(engine's first real modal; generalizes for a one-data-step wizard); gotchas
(strip engine nav fields in decode, new-row temp-id, direction-button nav).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bank-account filter rendered "Please select a client" even when a
client was set on the rule. Two causes:
- Inside (fc/with-field :transaction-rule/bank-account ...) the cursor is
rebound to the bank-account field, so (:transaction-rule/client
(fc/field-value)) read the nil bank-account value and the server
rendered the placeholder. The clientId watcher only fires on change, so
when editing (client preset, unchanged) the htmx swap never corrected
it. Read the client from the form root before entering the field.
- The clientId-change swap used innerHTML, nesting a fresh typeahead
inside the stale one and breaking its Alpine refs. Use outerHTML so the
typeahead is replaced in place.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The EditModal step body wrapped all rule fields in a nested
<form id="my-form"> inside the wizard's own #wizard-form. By HTML
form-ownership rules those fields belonged to the inner form, so when
htmx serialized #wizard-form on Next, none of the step-params fields
were sent. The server saw an empty rule, reported "required" for
description/accounts, and re-rendered a blank wizard (losing input).
Replace the nested <form> with a plain <div>; the wizard form already
owns submission, so the inner form and its htmx attributes were
redundant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add vendor-changed HTMX handlers for both bulk code and individual edit
- Pre-populate default account at 100% when vendor is selected and no accounts exist
- Fix render-accounts-section to render from step-params correctly
- Change bulk code vendor-changed from hx-get to hx-post to include form data
- Add routes for vendor-changed endpoints
- Update e2e tests to cover vendor pre-population
- Run lein cljfmt fix across codebase