Framework Gap — Silent Form Submit¶
📜 Historical snapshot — not current docs
Captured 2026-04-15 during Dazzle's autonomous-improvement cycles. It records the framework as it was then and the gap being worked at the time; it may not describe current behaviour. Start from the documentation home, or see Project Evolution for how these fit together.
Status: SUPERSEDED (cycle 229 investigation) Synthesized: Cycle 224 (framework_gap_analysis) Superseded: Cycle 229 (finding_investigation on EX-039) Contributing cycles: 201, 217, 222, 223 Evidence weight: originally 5 observations; post-investigation, 3 of 5 are substrate artifacts, 1 was a real framework bug already fixed by #774, 1 is a distinct class (#774 cascade, tracked as EX-041)
⚠️ SUPERSEDED — cycle 229 investigation invalidated most of this gap doc¶
Cycle 229's end-to-end investigation of EX-039 revealed that the framework's 422 HTML error-surfacing system already exists and works correctly. At the HTTP layer, POSTing an invalid payload to any entity create endpoint returns a proper 422 Unprocessable Entity response with a [data-dazzle-error] HTML fragment containing "Validation Error" + per-field messages ("device_id: Field required", etc.). The form template's hx-target-422="#form-errors" wiring is correct; HTMX's response-targets extension is loaded at base.html:105; the exception handler at the backend renders the right fragment.
What actually caused the "silent submit" observations was a substrate bug, not a framework gap:
- The subagent's
action_type/action_clickmulti-call pattern launches a fresh Playwright subprocess for each action.storage_statepersists cookies but NOT in-page form state, so values filled in a prioraction_typesubprocess evaporate by the time the nextaction_clicksubprocess fires. - Consequently, every
action_clicksubmitted an empty form. HTML5requiredattributes on the form's device_id/description inputs blocked empty submits entirely — no POST ever reached the server,state_changed=falsewas correctly reported, and no error appeared because no error was requested. - Even in non-required-field cases (EX-018's 1000-char device name, EX-039's -999 negative number), the test values evaporated the same way, so the server saw them as empty and the outcome was the same.
action_observemade it worse: it also launches a fresh subprocess and doespage.goto(last_url), which means any HTMX-swapped error content from an in-place swap is discarded on the next observe. Even if the framework had rendered an error in-place, observing it would have destroyed it.
Framework-side, nothing was broken. The gap this doc proposed to build (framework-default 422 handler re-rendering forms with field errors) already exists. The inject_current_user_refs fix for #774 remains real and correct — it addresses a specific FK auto-population gap that's orthogonal to the error-surfacing question.
Post-investigation evidence classification¶
| Row | Original status | Cycle 229 verdict |
|---|---|---|
| EX-007 (→#774) | Real framework bug | Still real — #774 closed it. Specific cause (missing created_by ref User), fixed in v0.55.33. |
| EX-018 | Concerning | SUSPECTED_FALSE_POSITIVE — substrate artifact; needs re-verification with new form_submit action |
| EX-034 | Notable | SUSPECTED_FALSE_POSITIVE — same substrate class |
| EX-039 | Notable | VERIFIED_FALSE_POSITIVE — cycle 229 reproduced the real mechanism end-to-end |
| EX-041 | Notable | Still real — distinct class. The form exposes ref Tester (User-subtype) without auto-injecting; this is a cascade extension of #774 and should be closed by extending inject_current_user_refs to walk the ref graph. |
So the true residual "silent-form-submit"-like gaps are:
1. EX-041 — inject_current_user_refs cascade to User-subtype entities (Tester → User back-ref). Small, well-scoped framework fix worth 15 min in a future cycle.
2. Client-side validation mirroring (proposed as Fix #3 in the original doc below) — still a latent UX improvement, but no longer motivated by a real defect. Hold until a fresh observation surfaces the need.
Substrate fix shipped in cycle 229¶
Added new form_submit action to src/dazzle/agent/playwright_helper.py: navigates, fills all fields, clicks submit, and harvests error banner text — all in one subprocess lifetime, with a 250ms post-networkidle wait for HTMX to run its swap handler. This unblocks all future subagent form exploration, which was previously unable to observe form validation errors at all. Filed as EX-043 with full trace.
Takeaway for the /ux-cycle loop¶
This gap doc is a useful illustration of why the synthesis cycle must be followed by at least one investigation cycle before a fix is attempted. Cycle 224's gap analysis saw 5 observations pointing at the same symptom class and synthesized them into a framework-wide theme. That was a reasonable inference from the data — but the data itself was poisoned by a substrate bug that faked the symptom. A naïve implementation cycle following cycle 224 would have built the "framework-default 422 handler" that already existed, and then wondered why the observations kept recurring.
Updated /ux-cycle skill heuristic for gap-analysis follow-through: when a gap doc would require new framework infrastructure (vs. a helper swap or single-file edit), spend at least one investigation cycle reproducing the defect end-to-end at the HTTP layer (bypassing any subagent tooling) before committing to the infrastructure build. The "try the real thing" pattern from cycle 228 is the right general discipline.
Original problem statement (retained for historical reference)¶
When a create form submits and the backend rejects the payload for any reason other than a missing created_by ref User, the rejection is completely invisible to the user. The form stays on the same URL, shows no inline validation, no toast, no field highlighting, and no state-changed signal. The user's typed content survives or disappears non-deterministically (at least one subagent run saw a -999 numeric value vanish without a trace).
The v0.55.33 fix for #774 (inject_current_user_refs in src/dazzle_http/runtime/route_generator.py) closed one specific cause — missing FK values for required ref User fields where the current user is a legitimate default. But the broader "silent submit" class is a framework-wide gap in error response surfacing from FastAPI 422 responses back to the HTMX form. The issue recurs across apps, personas, and entity types, with at least four distinct root causes.
Evidence¶
| Row | Cycle | App / Persona | Page | Observation |
|---|---|---|---|---|
| EX-007 (→#774) | 201 | support_tickets/agent | /app/ticket/create |
Empty-submit silent fail. Root cause: missing created_by: ref User FK injection. Fixed in v0.55.33 via inject_current_user_refs. |
| EX-018 | 217 | fieldtest_hub/engineer | /app/device/create |
1000-character device name. Root cause (suspected): server rejects on str(N) max_length validation; the rejection is a 422 that never reaches the form template. |
| EX-034 | 222 | ops_dashboard/admin | /app/system/create |
Typed name, submit silent. Root cause: unverified; either #778 User-bridge recurrence or same 422-not-surfaced mechanism as EX-018. |
| EX-039 | 223 | fieldtest_hub/tester | /app/issuereport/create + /app/testsession/create |
Empty required fields + negative numeric. No inline markers, no toast. Root cause: Pydantic field_required + greater_than_equal validators raise 422 but the form never re-renders with field-level error indicators. |
| EX-041 | 223 | fieldtest_hub/tester | /app/testsession/create |
Required 'Tester' FK rendered as plain text input requiring user to type their own UUID. Root cause: inject_current_user_refs (from #774) only targets ref User; it doesn't cascade to User-subtype entities like Tester. |
Root cause hypothesis¶
Three framework-layer mechanisms are missing or incomplete:
1. Systematic 422 error surfacing for HTMX form submits¶
FastAPI's Request Validation Error handler returns a 422 JSON body by default. Dazzle's HTMX integration at src/dazzle_http/runtime/route_generator.py has partial wiring for hx-target-422 (the HTMX response-targets extension, cite needed) but the mechanism is per-endpoint opt-in, not per-app default. When the opt-in is missing, the HTMX client receives a 422 response with a JSON body it has no template for, and silently does nothing.
Fix direction: Make 422-error-surfacing the framework default. Every generated create/update route handler should register an exception handler that, when the request is HTMX, returns a rendered HTML fragment containing the form re-populated with its submitted values, with per-field error markers on each Pydantic validation error. The fragment targets the form container via hx-reswap: outerHTML or equivalent. This is a single change in create_create_handler / create_update_handler at src/dazzle_http/runtime/route_generator.py that cascades to every entity.
2. inject_current_user_refs cascade through User-subtype entities¶
EX-041 shows the #774 fix doesn't match ref Tester (Tester is a User-subtype entity in fieldtest_hub's DSL). The helper currently matches only literal ref User. When a DSL declares a domain-specific actor entity that has a ref User back-reference (e.g., Tester → user: ref User), the create form should still auto-inject the current user via the transitive relationship.
Fix direction: Extend inject_current_user_refs to follow the entity ref graph one hop — if the required field is ref <Entity> and <Entity> has a unique ref User field, resolve the current user to the matching <Entity> row and inject that instead. Falls back cleanly (if no such row exists, surface a proper error instead of silent failure).
3. Pre-submit validation mirroring¶
Pydantic validation runs server-side on submit. The client-side HTML5 required attribute catches some empty-field cases but not enum placeholders, greater_than_equal, or max_length. The form-field template compiler could emit client-side pattern, minlength, maxlength, min, max attributes matching the Pydantic schema's validators, surfacing most validation failures before submit hits the server at all.
Fix direction: In src/dazzle_page/converters/template_compiler.py, when rendering a form field, consult the Pydantic model's field FieldInfo for min_length/max_length/ge/le metadata and emit matching HTML attributes. This is a pure additive change — doesn't replace server-side validation, just shifts easy rejections left.
Fix sketch (one change addresses the most observations)¶
Primary fix — (1) above. A framework-default 422 handler for HTMX create/update routes that re-renders the form with Pydantic validation errors mapped to field markers. This alone closes EX-018, EX-034 (if not also a #778 recurrence), EX-039, and improves EX-041 from "silent" to "explicit error message even if the fix for the cascade is deferred".
# src/dazzle_http/runtime/route_generator.py — inside create_create_handler
@route.exception_handler(RequestValidationError)
async def _htmx_validation_handler(request: Request, exc: RequestValidationError):
if not _is_htmx_request(request):
return JSONResponse({"detail": exc.errors()}, status_code=422)
# Re-render form with submitted values + per-field errors
field_errors = {e["loc"][-1]: e["msg"] for e in exc.errors() if len(e["loc"]) > 0}
return render_fragment(
"fragments/form_with_errors.html",
form=input_schema,
values=await request.form(),
errors=field_errors,
endpoint_url=str(request.url),
)
Plus a new src/dazzle_page/templates/fragments/form_with_errors.html that re-renders each field with its prior value and any errors[field_name] as an inline error marker.
Blast radius¶
Confirmed affected apps: support_tickets (EX-007), fieldtest_hub (EX-018/039/041), ops_dashboard (EX-034) Likely affected: contact_manager, simple_task (no observations yet because cycles 213/218 didn't stress create forms with deliberately-invalid inputs) Personas: every persona that uses create forms — universal
Every Dazzle app that has a create surface is affected the moment a user types invalid input. That's every Dazzle app.
Open questions¶
- Is the
hx-target-422response-targets wiring currently present in any form? A quickgrep -r "hx-target-422" src/dazzle_page/templates/will show the current state. - Does EX-034 (ops_dashboard/admin system create) have the same mechanism as EX-039, or is it a distinct #778 User-bridge recurrence? Needs a
finding_investigationcycle to reproduce locally and trace. - What's the contract for client-side
pattern=on enum selects? Does the framework already emit HTML5requiredon required fields? Template compiler inspection needed.
Recommended follow-up¶
- Immediately: File a GitHub issue titled "Framework-default 422 error surfacing for HTMX create/update forms" with this gap doc as the body. Tag as
framework-gapand link all 5 contributing EX rows. - Next
finding_investigationcycle: Reproduce EX-034 on ops_dashboard to confirm whether it's a duplicate of EX-039 (suggesting the single fix closes both) or a new #778 recurrence (suggesting the QA auth-bridge gap needs independent attention). - After the primary fix lands: Re-run cycles 217/221/223 against the fixed framework and move the contributing EX rows to
FIXED→v0.55.XXorVERIFIED_FIXEDdepending on the result.