Forms consolidation: one shared core for SSR and SPA, proven by a conformance kit #35

Merged
mprihoda merged 62 commits from iw-support-form-effort into main 2026-07-12 19:16:52 +00:00
mprihoda commented 2026-07-12 17:58:30 +00:00 (Migrated from github.com)

What this delivers

The core-hardening-spa-refold slice of the forms-consolidation effort (project-management/issues/iw-support-form-effort-3/): every form walker — UIFormBuilder, SSR HTML renderer, XML renderer, FormRJsonEncoder, DeclaredValidation, LiveHtmlInterpreter — now evaluates conditions, validation, required-ness, field-type dispatch and repeated-row expansion through one shared core, and disagreement is a failing test instead of a production surprise.

  • Shared core: pure Condition.eval, Repeated.instances/template (total — rows are never silently dropped), Field.required, declared Validation vocabulary with Validation.check/Validation.rule, ValidationRuleRegistry for Rule(id, params) bindings, FieldKind closed dispatch index, FormData as the typed value currency, TypedForm[A] (the FormSchema salvage).
  • Both variants refolded onto it: the SSR GET/POST loop and the Laminar custom element render the same proof form identically; czech-support extracted (FC-D3); portaly.forms renamed to works.iterative.forms.
  • Conformance kit (forms/scenarios): a corpus with one sample per FieldKind/Validation/Condition case — Mirror-derived case counts make a new enum case red until covered — plus per-walker agreement suites and a browser pair (ssrVocab/spaVocab) pinning the per-kind control tables and ButtonIntent semantics in Gherkin. 23 Playwright scenarios green.

Drift the kit killed (each red-green)

  • FormRJsonEncoder crashed on forms encoding to no data, and on repeated rows via an exact-match template lookup bypassing the shared fallback.
  • The SSR builder dropped template-less repeated rows out of the POST loop — user data loss (the SPA kept them). Repeated.Instance.segment is now an Option; every walker faces the unrenderable case explicitly.
  • The live interpreter hardcoded enums required, never required dates, ignored declared validations and the form-key message nesting; UIFormBuilder ignored a declared Required for the star decoration.
  • FieldTypeResolver.empty rendered email/date/number as plain text inputs while SSR typed them — the defaults now share one control table.

Migration flags for client upgrades (cmi-portaly, medeca-modul-poptavky)

  • Package rename portaly.formsworks.iterative.forms (compile-time only; verify akka-persistence journals for serialized FQNs before replay).
  • Pattern-match arity changed on Enum/Date/Button; UIButton carries an intent; custom UIForm walkers need a UIRepeatedGroup arm.
  • FieldType string conversion retired — wrap in FieldType(...).
  • FieldFactory.render gained the declared-rule parameter; optional Selects no longer validate blank values.
  • Form-key-prefixed message keys now resolve in live forms (previously unreachable).
  • FieldTypeResolver.empty is kind-aware (cmi uses it in two internal demo scenarios); LiveHtmlInterpreter gained a defaulted trailing ruleRegistry parameter.
  • FormRJsonEncoder stays on FormR until medeca rebinds (FC-D2 sequencing).

Known gaps, deliberately deferred (tracked in the tracker)

  • SPA displays reading ctx.state see removed-row values (FormV never forgets) — browser-verified evidence for the FormR→FormData SPA-internals move, next slice.
  • Checkbox-typed Field value semantics are a design corner in both variants (SPA renders it as text — Laminar forbids the value controller on checkbox inputs; bool Enums are the checkbox story). Pinned as the one documented divergence in the conformance feature.
  • ButtonIntent is not yet exposed to the SPA ButtonHandler — YAGNI until a client needs it; current routing pinned.
  • Morph swaps, FormBundle/PDF, file upload → later slices.

Verification

./mill __.compile (zero warnings, clean build), ./mill __.test (full suite), ./mill formsScenarios.jvm.e2e (23 browser scenarios, ~21s), ./mill __.checkFormat. Scenarios server: PORT=8391 ./mill formsScenarios.jvm.run — pages at /ssrForm, /spaForm, /ssrVocab, /spaVocab.

🤖 Generated with Claude Code

## What this delivers The core-hardening-spa-refold slice of the forms-consolidation effort (`project-management/issues/iw-support-form-effort-3/`): every form walker — UIFormBuilder, SSR HTML renderer, XML renderer, FormRJsonEncoder, DeclaredValidation, LiveHtmlInterpreter — now evaluates conditions, validation, required-ness, field-type dispatch and repeated-row expansion through one shared core, and disagreement is a failing test instead of a production surprise. - **Shared core**: pure `Condition.eval`, `Repeated.instances`/`template` (total — rows are never silently dropped), `Field.required`, declared `Validation` vocabulary with `Validation.check`/`Validation.rule`, `ValidationRuleRegistry` for `Rule(id, params)` bindings, `FieldKind` closed dispatch index, `FormData` as the typed value currency, `TypedForm[A]` (the FormSchema salvage). - **Both variants refolded onto it**: the SSR GET/POST loop and the Laminar custom element render the same proof form identically; `czech-support` extracted (FC-D3); `portaly.forms` renamed to `works.iterative.forms`. - **Conformance kit** (`forms/scenarios`): a corpus with one sample per FieldKind/Validation/Condition case — Mirror-derived case counts make a new enum case red until covered — plus per-walker agreement suites and a browser pair (`ssrVocab`/`spaVocab`) pinning the per-kind control tables and ButtonIntent semantics in Gherkin. 23 Playwright scenarios green. ## Drift the kit killed (each red-green) - FormRJsonEncoder crashed on forms encoding to no data, and on repeated rows via an exact-match template lookup bypassing the shared fallback. - The SSR builder dropped template-less repeated rows out of the POST loop — user data loss (the SPA kept them). `Repeated.Instance.segment` is now an `Option`; every walker faces the unrenderable case explicitly. - The live interpreter hardcoded enums required, never required dates, ignored declared validations and the form-key message nesting; `UIFormBuilder` ignored a declared `Required` for the star decoration. - `FieldTypeResolver.empty` rendered email/date/number as plain text inputs while SSR typed them — the defaults now share one control table. ## Migration flags for client upgrades (cmi-portaly, medeca-modul-poptavky) - Package rename `portaly.forms` → `works.iterative.forms` (compile-time only; verify akka-persistence journals for serialized FQNs before replay). - Pattern-match arity changed on `Enum`/`Date`/`Button`; `UIButton` carries an intent; custom UIForm walkers need a `UIRepeatedGroup` arm. - `FieldType` string conversion retired — wrap in `FieldType(...)`. - `FieldFactory.render` gained the declared-rule parameter; optional Selects no longer validate blank values. - Form-key-prefixed message keys now resolve in live forms (previously unreachable). - `FieldTypeResolver.empty` is kind-aware (cmi uses it in two internal demo scenarios); `LiveHtmlInterpreter` gained a defaulted trailing `ruleRegistry` parameter. - `FormRJsonEncoder` stays on `FormR` until medeca rebinds (FC-D2 sequencing). ## Known gaps, deliberately deferred (tracked in the tracker) - SPA displays reading `ctx.state` see removed-row values (`FormV` never forgets) — browser-verified evidence for the FormR→FormData SPA-internals move, next slice. - Checkbox-typed `Field` value semantics are a design corner in both variants (SPA renders it as text — Laminar forbids the value controller on checkbox inputs; bool `Enum`s are the checkbox story). Pinned as the one documented divergence in the conformance feature. - `ButtonIntent` is not yet exposed to the SPA `ButtonHandler` — YAGNI until a client needs it; current routing pinned. - Morph swaps, FormBundle/PDF, file upload → later slices. ## Verification `./mill __.compile` (zero warnings, clean build), `./mill __.test` (full suite), `./mill formsScenarios.jvm.e2e` (23 browser scenarios, ~21s), `./mill __.checkFormat`. Scenarios server: `PORT=8391 ./mill formsScenarios.jvm.run` — pages at `/ssrForm`, `/spaForm`, `/ssrVocab`, `/spaVocab`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
mprihoda commented 2026-07-12 19:09:08 +00:00 (Migrated from github.com)

Two updates since opening: 3b7e14a1 fixes the pre-existing one-liner where LiveHtmlInterpreter.aroundTitle's copy constructor silently reset formMods, and 67fb0cfe merges current main (workflow-kernel extraction #34) — the three effort-doc add/add conflicts resolved to this branch's versions, which are strict supersets of the day-one framing the kernel branch carried. Full gate re-run on the merged tree: clean compile zero warnings, all tests, 23 browser scenarios green.

🤖 Generated with Claude Code

Two updates since opening: `3b7e14a1` fixes the pre-existing one-liner where `LiveHtmlInterpreter.aroundTitle`'s copy constructor silently reset `formMods`, and `67fb0cfe` merges current main (workflow-kernel extraction #34) — the three effort-doc add/add conflicts resolved to this branch's versions, which are strict supersets of the day-one framing the kernel branch carried. Full gate re-run on the merged tree: clean compile zero warnings, all tests, 23 browser scenarios green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
mph referenced this pull request from a commit 2026-07-13 10:21:58 +00:00
mph referenced this pull request from a commit 2026-07-24 07:33:37 +00:00
mph referenced this pull request from a commit 2026-07-28 18:29:57 +00:00
Sign in to join this conversation.
No description provided.