Skip to content

fix: drop submitter-needs-bound-form, which contradicts the self-sufficient bound submitter #1384

Description

@vivek7405

Problem

main has been red since 7683c1ba (#1314) on three jobs: Conventions (webjs check), Unit + integration (node --test), and Bun runtime smoke + test matrix. Two of those are required contexts on main, so every open PR is currently unmergeable, including #1383, which touches only gallery/ and cannot go green on its own.

#1314 added a webjs check rule, submitter-needs-bound-form, plus a renderer differential test. Both were written against renderer behaviour that does not exist. #1307 deliberately made a bound submitter self-sufficient, and AGENTS.md invariant 12 states the rule explicitly:

A bound submitter is SELF-SUFFICIENT and asks nothing of the form around it. [...] the renderer refuses only a SAME-ELEMENT contradiction, which has no correct fallback, and never a cross-element rule, which always has one.

The rule's own source comment (packages/server/src/check.js L1454) asserts the opposite:

A submitter bound in a component whose host form is unbound posts nowhere: the form defaults to GET, the identity rides the query string, and the page re-renders with the action never having run.

That is false. Measured against packages/core/src/render-server.js at 034d6a0e:

<form><button formaction=${publish}>x</button></form>
  -> <form><button name="__webjs_action" value="…"
             formmethod="post" formenctype="multipart/form-data">x</button></form>

<form method="get"><button formaction=${publish}>x</button></form>
  -> <form method="get"><button name="__webjs_action" value="…"
             formmethod="post" formenctype="multipart/form-data">x</button></form>

<button formaction=${publish}>Publish</button>          (a component's own template)
  -> <button name="__webjs_action" value="…"
             formmethod="post" formenctype="multipart/form-data">Publish</button>

The renderer stamps formmethod="post" on the button in every shape the rule flags, and per HTML a submitter's formmethod overrides the form's method. The submission is a POST, the identity rides the body, and the action runs. There is no GET, no query string, and no silent failure.

Consequences on main today:

  1. webjs check fails on examples/blog with 2 violations. One of them is modules/feedback/components/publish-button.ts, which is feat: make a bound form submitter self-sufficient, and honour the authored enctype #1307's own fixture demonstrating self-sufficiency, and whose doc comment says in as many words that it "submits correctly inside /feedback/triage-split's unbound, method-less form, with JavaScript on or off". The rule flags the very fixture that proves it wrong.
  2. packages/server/test/scanner/html-form-scopes.test.js:233 ("the start-tag-hole rule matches what the RENDERER actually does") fails with Missing expected rejection, asserting /requires the enclosing <form> to also be bound/. That error string exists nowhere in packages/core/src or packages/server/src. The same single test file is the sole genuine failure in the Bun matrix.

Design / approach

Delete the rule and the false assertion, keeping everything in #1314 that is independently correct.

The rule cannot be repaired by narrowing it, because there is no shape left for it to flag: the renderer makes every bound submitter deliver. Its existence also re-litigates a decision #1307 settled with reasons that still hold (a component renders its template in a separate pass with no view of the host page, so the cross-element question is unanswerable at render time and the fix was to stop asking it).

Explicitly out of scope (these are correct and must not be touched):

  • The runtime diagnostics in packages/server/src/form-dispatch.js (WEBJS_FORM_SUBMITTED_AS_GET L354, WEBJS_FORM_ACTION_MISSING L300). These detect a request shape server-side and are reachable regardless of how the request was produced (hand-written HTML, a stale cached page, a crafted request). test/bun/form-action-dispatch.mjs covers them and passes.
  • scanHtmlFormScopes() in packages/server/src/js-scan.js L933 and the passing tests in html-form-scopes.test.js. The scanner's bound/unbound/handed scopes and the enctype denylist test are sound. Only the one renderer differential at L233 encodes the false premise.
  • website/app/docs/deployment/page.ts L202, which already describes the correct mechanism ("A bound submitter carries its own formmethod="post", so what reaches this is an explicit formmethod="get" or method="get" the author wrote").

Implementation notes (for the implementing agent)

Where to edit.

  • In packages/server/src/check.js, remove the rule end to end: the RULES entry at L147 (name: 'submitter-needs-bound-form'), the call site and its comment block at L1454, the checkSubmitterNeedsBoundForm() implementation (L1466 through roughly L1900, including the two violations.push sites at L1878 and L1894), and the cross-reference comment at L1412. Drop the scanHtmlFormScopes import at L12 only if nothing else in the file still uses it (L1730, L1737, L1758 and L1762 are all inside the function being removed).
  • Delete packages/server/test/check/submitter-needs-bound-form.test.js. Note L83 asserts RULES lists the rule, so it fails by construction once the rule is gone.
  • In packages/server/test/scanner/html-form-scopes.test.js, remove only the await assert.rejects(...) block inside the test at L233 (the webjs-suspense .fallback half asserting /requires the enclosing <form> to also be bound/). Keep the scanner deepEqual assertions around it, which pass and still pin real behaviour. Re-title the test if it no longer describes a differential.

Doc surfaces carrying the same false mechanism. All of these state that the enclosing form is what supplies method="post", which the button now supplies itself.

  • .agents/skills/webjs/references/data-and-actions.md L149
  • .agents/skills/webjs/references/muscle-memory-gotchas.md L160
  • website/app/docs/troubleshooting/page.ts L60
  • website/app/docs/progressive-enhancement/page.ts, which mentions the rule, so audit it for the same claim
  • The scaffold copies under packages/cli/templates/.agents/skills/webjs/references/ mirror the two skill references above and must move with them, or the scaffold-sync drift test reds
  • Leave website/app/docs/troubleshooting/page.ts L70 alone. It covers an explicit formmethod="get" on a plain submitter in a bound form, which is a real and different case.

Landmines.

Invariants to respect.

  • AGENTS.md invariant 12, the bound-submitter refusal list. This fix restores agreement with it. Do not add a cross-element rule back in any form.
  • examples/blog's publish-button.ts and app/feedback/triage-split/page.ts are feat: make a bound form submitter self-sufficient, and honour the authored enctype #1307's demonstration fixtures. Do not "fix" the blog to satisfy the rule, which would invert cause and effect and delete the proof that the feature works.

Test surfaces.

  • Unit: packages/server/test/check/** and packages/server/test/scanner/**.
  • Bun parity: the same scanner file is the only genuine Bun-matrix failure, so it clears with the unit fix. packages/server/src is runtime-sensitive, so node scripts/run-bun-tests.js must be run and reported.
  • No browser or e2e layer applies (no routing, DOM, or client-router surface changes).
  • A counterfactual is not meaningful for a deletion. Prove the negative directly instead: ( cd examples/blog && npx webjs check ) passes, and the rendered /feedback/triage-split publish button still carries formmethod="post".

Acceptance criteria

  • ( cd examples/blog && npx webjs check ) passes, with publish-button.ts and triage-split/page.ts unmodified
  • gallery and website still pass webjs check
  • node --test packages/server/test/** green, including html-form-scopes.test.js
  • node scripts/run-bun-tests.js reports zero genuine failures
  • The rendered /feedback/triage-split publish button still carries formmethod="post" and formenctype="multipart/form-data"
  • Every doc surface listed above no longer claims the enclosing form supplies method="post", and the scaffold template copies match their skill originals
  • main is green, unblocking feat(gallery): brand the gallery shell like the website #1383 and the other open PRs

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions