diff --git a/.agents/skills/webjs/SKILL.md b/.agents/skills/webjs/SKILL.md
index fb5a1e10e..5f069802f 100644
--- a/.agents/skills/webjs/SKILL.md
+++ b/.agents/skills/webjs/SKILL.md
@@ -240,7 +240,7 @@ Success is a 303 (PRG); failure re-renders the page at 422 with the result on `a
- Writing `formmethod="get"` or `formenctype="text/plain"` on a button that BINDS an action. Neither can carry that action's body, so the pair contradicts itself and throws. On a button that binds nothing it is a legal native override and is honoured.
- Binding an action whose file declares `export const method = 'GET'`. Form-bound actions strictly require POST (default). Binding a GET action to a form is a 405 at runtime and a `webjs check` error (`form-action-not-a-get-action`).
- Leaving read-only RPC server query actions as default `POST`. Always export `export const method = 'GET'` for RPC data queries so arguments ride URL params, ETags/304 caching work, and CSRF is safely bypassed.
-- Writing `method="get"` on a bound `
`}>`';
- assert.deepEqual(scanHtmlFormScopes(ownForm).submitters, [{ tag: 'button', scope: 'bound', delivers: null, expr: 'del' }]);
- // A hole in CHILD position IS rendered inline by this scan, so it still
- // inherits. This is the pair that keeps the fix from being a blanket opt-out.
- const child = 'html`
${html``}
`';
- assert.deepEqual(scanHtmlFormScopes(child).submitters, [{ tag: 'button', scope: 'unbound', delivers: true, expr: 'del' }]);
-});
-
-test('the unparseable-enctype constant tracks the renderer\'s own set', async () => {
- // The scanner states its enctype rule as a DENYLIST of one because of the
- // invalid-value default, while the renderer refuses the wider allowlist. This
- // pins the relationship rather than asserting the two are equal, so a change
- // to core's set surfaces here instead of drifting silently.
- assert.ok(!PARSEABLE_ENCTYPES.has('text/plain'), 'the renderer cannot parse text/plain either');
- for (const e of PARSEABLE_ENCTYPES) {
- const src = `html\`
\``;
- assert.equal(scanHtmlFormScopes(src).submitters[0].delivers, true, `${e} delivers`);
- }
- assert.deepEqual([...PARSEABLE_ENCTYPES].sort(),
- ['application/x-www-form-urlencoded', 'multipart/form-data'],
- 'if core gains an enctype, revisit the scanner denylist');
-
- // There are now THREE hardcoded copies of the denylist keyword: the scanner's
- // `UNPARSEABLE_FORM_ENCTYPE`, the client guard in `router-client.js`, and this
- // test. Pin the client one too, so the two halves of the feature cannot drift
- // into disagreeing on the same input (which is what the allowlist did).
- const clientSrc = await readFile(new URL('../../../core/src/router-client.js', import.meta.url), 'utf8');
- assert.match(clientSrc, /enctype\.toLowerCase\(\) === 'text\/plain'/,
- 'the client guard uses the same one-keyword denylist, not the renderer allowlist');
- assert.doesNotMatch(clientSrc, /PARSEABLE_ENCTYPES/,
- 'and does not reach for the renderer allowlist again');
-});
-
-test('the start-tag-hole rule matches what the RENDERER actually does', async () => {
- // A differential rather than an assertion about the scanner alone, because the
- // whole 'handed' state is a claim about the renderer's behaviour. Most
- // start-tag holes are values the receiving element places, but
- // `` is rendered INLINE with the enclosing form
- // scope (#471: a custom-element property applies at hydration, too late for a
- // placeholder that must be in the first flushed bytes). Treating that one as
- // handed off would blind the same-scan half to a real render-time throw.
- const { html } = await import('../../../core/src/html.js');
- const { renderToString } = await import('../../../core/src/render-server.js');
- const { setFormActionResolver } = await import('../../../core/src/form-action.js');
- setFormActionResolver(async () => 'abc1234567/publish');
- const publish = async () => ({ success: true });
-
- // Rendered inline, so the renderer judges it against the unbound form and
- // throws. The scanner must see the same thing.
- const fallbackSrc = 'html`
to also be bound/,
- 'the renderer really does refuse it, so the scanner must not be silent',
- );
-
- // The negative half, and it has to assert the MECHANISM rather than "it did
- // not throw". A template carrying a function cannot serialize, so SSR drops
- // the whole property binding and emits nothing for it: the submitter is never
- // rendered and never judged here at all. Asserting only that the render
- // succeeded would stay green for ANY scanner verdict, which is precisely the
- // non-discriminating test this differential exists to avoid.
- const propSrc = 'html`