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>
Builds the reusable multi-step wizard engine the plan front-loads in Phase 6, as
two protocol-free namespaces. This replaces the EDN-snapshot-in-a-hidden-field
round-trip for genuine multi-step flows: per-step validated data lives in the Ring
session and is combined only at the end — only an opaque wizard-id rides in the form.
- components/wizard_state.clj — pure session storage (Django formtools SessionStorage
model): create-wizard!, instance, exists?, current-step, context, step-data,
put-step (REPLACE not merge), set-step, get-all (combine at end), forget. State
namespaced by wizard-id at [:wizards <id> ...]; :context holds read-only step inputs
outside :step-data so it never merges into the result. Each fn is session -> session'.
- components/wizard2.clj — the engine: open-wizard, render-wizard, handle-step-submit,
wizard-form. A wizard is a config map (steps with :decode/:validate/:render/:next,
plus :init-fn/:done-fn/:submit-route). Steps' :render get {wizard-id, current-step,
context, all-data, step-data, errors, request}; nav posts a `direction` field
(next/back/submit). Two routes per wizard (open + submit); the engine threads the
session into the response itself — no wrap-wizard / wrap-decode-multi-form-state stack.
REPL-proven lifecycle (before wiring any modal):
1. OPEN -> seeds session state, renders step 1, form leaks NO accumulated data
2. NEXT -> stores {:info {:name "Acme"}}, advances to :terms
3. INVALID -> re-renders the same step with errors, no advance
4. DONE -> done-fn gets combined {:name "Acme" :days 30} (get-all), instance forgotten
5. BACK -> :terms -> :info, no validation
6. EXPIRED -> unknown wizard-id re-opens fresh instead of 500-ing
Inert infrastructure — nothing imports it yet (Transaction Rule migrates onto it next),
so the e2e suite is unaffected. cljfmt clean. Skill: form-vs-wizard.md updated from
aspirational to the realized engine API + the Phase-6 fit note (Transaction Rule
exercises render/nav/preview; the cross-step merge gets its workout in Phase 7+).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrates the Invoice Bulk Edit modal off the wizard to a plain Selmer form,
building on the parity gate. Structurally Phase 3's bulk-code applied to invoices
(selected entities -> expense-account rows), so near-pure reuse of bulk-code's
flat-state plumbing + edit's account-totals-tbody.
What changed
- Wizard removed: deleted BulkEditWizard/AccountsStep records, MultiStepFormState,
the step-params[...] prefix, the EDN snapshot, and all mm/* for this modal.
Replaced with a plain handler + flat wrap-bulk-state (decode straight into
bulk-edit-schema, no snapshot).
- Selection-as-ids round-trip: the non-editable invoice selection is resolved to a
concrete not-locked id vector at open and ridden back in hidden ids[] fields (the
bulk analog of edit's single db/id) -- no filter re-query.
- De-cursored bulk-edit-account-row* to Selmer (sc/*), explicit-id location swap
(#account-location-<index>, replacing the old find * swap), reusing
tx-edit/location-select*.
- 100% Selmer modal render path; the surgical edit was done with the text-based
Edit tool (the clojure-mcp structural tools reformat the whole 1812-line file),
so the diff is contained to the requires + the bulk-edit region.
- Routes 5 -> 3: GET bulk-edit, PUT bulk-edit-submit, POST bulk-edit-form-changed
(one whole-form op dispatcher folding the old new-account route).
Implemented the dead totals
- The wizard's TOTAL/BALANCE percentage rows were commented out (#_(...)) with a
duplicate id="total". Implemented as a #expense-totals sibling-<tbody> refreshed by
a Rule-4 percentage-keyup swap (the new-account + total + balance routes all fold
into form-changed / the sibling-tbody).
Scorecard delta (bulk-edit modal): wizard records 2->0, bulk-edit routes 5->3,
step-params/fc-cursor (modal) ->0, location swap find *-> explicit-id, totals
dead->implemented.
Verification: invoice-bulk-edit spec 5/5 (incl. add-row, save, validation, the
implemented totals); full Playwright suite 50/50; cljfmt clean; diff confined to
the modal region. Skill fed: scorecard row + settled repeated-row target-selector
convention; gotcha (structural tools reformat large files -> use text Edit).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrates the POS Sales Summary edit modal off the wizard to a plain Selmer form,
building on the parity gate committed earlier. Largest migration so far and the
first with no prior test coverage.
What changed
- Wizard removed: deleted MainStep/EditWizard records, MultiStepFormState, the
step-params[...] prefix, the EDN snapshot round-trip, and all mm/* middleware.
Replaced with a plain handler + flat wrap-decode/wrap-derive-state. The 51 fc/
cursor refs are de-cursored into explicit data + Selmer templates.
- db/id-keyed item merge: wrap-derive-state overlays posted items onto the
persisted items by :db/id, so read-only fields the form doesn't post
(ledger-side, amount) survive a re-render and the debit/credit split + totals
stay correct. New manual rows (temp db/id) ride through as-is.
- Inline click-to-edit account cell preserved as three small targeted
.account-cell-swap routes (edit/save/cancel-item-account), ported to Selmer
with the new field-name scheme.
- 100% Selmer modal render path (the remaining Hiccup / hx-swap-oob / "hx-"
strings are all grid-page code — grid render lambdas, the filters form, and the
submit response-header map — not the modal).
- Routes: dropped edit-wizard-navigate + new-summary-item; added form-changed.
Fixes (two pre-existing bugs, per request)
- "New Summary Item" add button (was throwing `newRowIndex is not defined` and
targeting a non-existent `.new-row`) is now a whole-form-swap op=new-item that
adds an editable manual row (category + account typeahead + debit/credit money
inputs + remove).
- The dead totals/balance display (malformed Hiccup that discarded its labels) is
replaced by a proper #summary-totals block showing running Total +
Balanced/Unbalanced, refreshed via a Rule-4 targeted swap on manual amount edits.
Scorecard delta (pos/sales_summaries.clj): LOC 790->732, mm coupling 20->0,
wizard records 4->0, fc/ cursor 51->0, step-params 27->0 (2 comments), modal
routes 8->6. (hx-swap-oob 1 and mixed-hx live in the grid page, not the modal.)
Verification: sales-summary spec 7/7 (incl. the two fixes); full Playwright suite
46/46; cljfmt clean. Skill fed: scorecard row + narrative; gotchas (parity-gate-
first, characterize-then-fix, keyup-trigger tests); cookbook (inline click-to-edit
cell, db/id-keyed item merge).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrates the Transaction Bulk Code modal (a single-step form wearing a full
wizard costume) to a plain Selmer form, cold-applying the ssr-form-migration
skill. Almost entirely reuse of the Phase-2 work: the whole `sc/*` Selmer
component library, `account-typeahead*` / `location-select*`, and the
`edit-modal` / `transitioner` chrome are imported wholesale.
What changed
- Wizard removed: deleted `BulkCodeWizard` / `AccountsStep` records,
`MultiStepFormState`, the `step-params[...]` prefix, and all `mm/*`
middleware. Replaced with a plain handler + flat `wrap-bulk-state` (decode
straight into `bulk-code-schema`, no snapshot round-trip).
- Selection round-trip: the non-editable transaction selection is resolved to
a concrete not-locked id vector at open and ridden back in hidden `ids[]`
fields (the bulk analog of edit's single `db/id`) — no EDN snapshot, no
filter re-query, and more correct (codes exactly the rows the user saw).
- 100% Selmer render path (only the shared terminal `com/success-modal` keeps
Hiccup — heuristic-9 exception). New shared component `sc/select`
(`location-select.html` generalized) for the status dropdown.
- Routes 4 -> 3: GET `bulk-code` (open), POST `bulk-code-submit`, POST
`bulk-code-form-changed` (one whole-form op dispatcher folding the old
`new-account` + `vendor-changed` routes). Location swap moved off `find *`
onto explicit `#account-location-<index>` + `hx-select`.
- Fixed a latent correctness bug surfaced by the migration: the vendor
typeahead needs `:id` (value-keyed `:key`) or its value-bound hidden goes
stale across a whole-form swap and posts blank.
Scorecard delta (transaction/bulk_code.clj): mm coupling 19->0, snapshot
merges 4->0, wizard records 3->0, step-params 10->0, routes 4->3, OOB 0,
Hiccup-in-render ->0 (bar success-modal). LOC 420->506 (documented exception:
the wizard was a thin shell over mm/* defaults, so explicitness moves shared
plumbing into the file). Cookbook: reused the entire Phase-2 sc/* lib + chrome,
added sc/select.
Verification: bulk-code-transactions.spec.ts 13/13; full Playwright suite
39/39; cljfmt clean.
Skill fed: scorecard row + narrative + LOC exception; gotchas (value-bound
typeahead keying, selection-as-ids round-trip); cookbook (sc/select).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Squashed Phase-2 SSR work: migrate the Transaction Edit modal's render path
entirely to Selmer templates (zero Hiccup in the render path), rip out the
multi-step wizard abstraction (EditWizard/LinksStep records, MultiStepFormState,
step-params[...] field names, mm/* middleware) in favor of a plain form with
flat derived state, and promote shared UI components to reusable Selmer partials
under resources/templates/components/. Adds the Selmer interop bridge, the
auto-ap.ssr.components.selmer (sc) wrapper library, and the ssr-form-migration
skill capturing the learnings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New repository-based skill at .claude/skills/invoice-template-creator/:
- SKILL.md: Complete guide for creating invoice parsing templates
- references/examples.md: Common patterns and template examples
- Covers vendor identification, regex patterns, field extraction
- Includes testing strategies and common pitfalls
Updated AGENTS.md with reference to the new skill.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>