You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
All line anchors in this issue were verified against HEAD e5806e24 ("docs: correct the CI route named in seven bun test wrappers (#1345)"). Re-check them with grep -n before editing if HEAD has moved.
Problem
Two doc surfaces give the wrong reason for why a <webjs-suspense>.fallback is read inline at SSR instead of riding the data-webjs-prop-* channel. Both name serializer-safety, which is a true but secondary fact, and neither names the constraint that actually forces the design.
Occurrence 1 (the one the issue was filed for).website/app/docs/suspense/page.ts:94, verified present at HEAD, reads verbatim:
<p>The <code>.fallback</code> is read at SSR as the inline placeholder (never through the <code>data-webjs-prop-*</code> path, since a <code>TemplateResult</code> is not serializer-safe) and must be an unquoted property hole. <code>renderFallback()</code> on a component is a DIFFERENT concern (the client re-fetch loading state, never the first paint); see <a href="/docs/loading-states">Loading States</a>.</p>
Occurrence 2 (not named in the original filing, found by grepping the repo).packages/core/AGENTS.md:38, the webjs-suspense.js row of the module table, carries the same serializer-only parenthetical:
| `webjs-suspense.js` | The `<webjs-suspense>` component-level streaming boundary element (#471). SSR (`render-server.js`) does the work: `injectDSD`'s `processSuspenseElements` pre-pass reads `.fallback` (carried as `data-webjs-fallback` by `renderTemplate`, since a TemplateResult is not serializer-safe) and, in a streaming context, flushes the fallback as `<webjs-suspense id="sN">` while pushing the children to `ctx.pending` for out-of-order streaming ...
Serializer-safety explains why a TemplateResultcannot ride that path. It does not explain why it must not. The load-bearing constraint is TIMING. The data-webjs-prop-* channel applies the property during hydration, which is far too late for a placeholder whose entire job is to be in the first flushed bytes.
The distinction matters because the current wording points at a fixable-looking obstacle. A reader could reasonably conclude that making TemplateResult serializable would unlock the data-webjs-prop-* path, and spend real time on it. It would not. Even a perfectly serializable fallback applied at hydration arrives after the shell has flushed, so there is no placeholder during the window the boundary exists for.
The timing claim, proven against the source
The correction asserts a fact about when each path runs. Both halves are verified in the shipped source, so the corrected sentence is grounded rather than asserted.
The .fallback is consumed during the SSR byte emission.packages/core/src/render-server.js:403 sits inside the kind === 'prop' branch of renderTemplate's attribute scanner and special-cases the boundary before the generic property path can run:
// `<webjs-suspense .fallback=${html`...`}>` (#471). A TemplateResult// is not serializer-safe (the normal data-webjs-prop-* path would// drop it) and a normal custom-element prop applies only at// hydration, too late for the streaming placeholder. So render the// fallback to HTML now and carry it as data-webjs-fallback, which the// injectDSD streaming pre-pass reads as the boundary placeholder.if(currentTag==='webjs-suspense'&&name==='fallback'){constfbHtml=awaitrender(val,ctx);out+=`data-webjs-fallback="${escapeAttr(fbHtml)}"`;
Note that this comment ALREADY gives both reasons, timing included, and gives the timing reason the weight it deserves. It is the canonical statement and needs no change. The two doc surfaces are the drifted copies.
processSuspenseElements (declared at packages/core/src/render-server.js:1243, called from injectDSD at packages/core/src/render-server.js:910) then reads that carrier at packages/core/src/render-server.js:1280 and writes the fallback markup straight into the shell at packages/core/src/render-server.js:1310:
Those bytes are the shell. Nothing client-side is involved, which is why the docblock at packages/core/src/render-server.js:1234 can say first-load streaming needs no client runtime.
The data-webjs-prop-* channel applies at hydration.WebComponent's connectedCallback at packages/core/src/component.js:836 is the only caller of the decoder:
_hydratePropAttrs() at packages/core/src/component.js:904 scans data-webjs-prop-* attributes, decodes each through the wire serializer, assigns the property, and removes the attribute. The if (!isBrowser) return guard on the first line is the proof in one line. That path is browser-only by construction, so it cannot run before the server has flushed anything. The docs site already documents this timing independently at website/app/docs/ssr/page.ts:146 and website/app/docs/ssr/page.ts:161, both of which correctly say the attributes are applied and stripped on connectedCallback. So the suspense page contradicts the SSR page as the repo stands.
What is stale in the original filing
The original body scoped the work to one line in website/app/docs/suspense/page.ts. It missed packages/core/AGENTS.md:38, which repeats the same serializer-only explanation on the framework's own agent-facing surface. Both are in scope now.
The original body said "check whether any test asserts on this paragraph before editing". That check has been done. grep -rn "serializer-safe" test/ website/test/ returns nothing, and no file under test/docs/ or website/test/ssr/ references docs/suspense. There is nothing to update, so the implementer does not need to repeat the search.
Provenance. This correction was written as part of PR #1314, which was closed unmerged as superseded by PR #1317 (#1307). The line is unrelated to that PR's subject and would otherwise have been lost with it, which is the only reason this issue exists. Nothing else from #1314 is worth salvaging.
Design / approach
Settled decision. Lead the parenthetical with the timing reason on every surface that carries it. On the docs page, replace the serializer clause outright with the reviewed wording. On packages/core/AGENTS.md, lead with timing and keep serializer-safety as the trailing secondary fact, matching the ordering the source comment at packages/core/src/render-server.js:403 already uses.
Why this ordering is right. A reason a reader can act on belongs first. Serializer-safety reads as a property of the payload, so it invites the reader to change the payload. Timing is a property of the pipeline, and no change to the fallback value moves a hydration-time write earlier than a server flush. Putting timing first makes the constraint unarguable at a glance, and keeping serializer-safety second on the source-facing surface preserves the fact that the channel would also drop the value, which is real and worth knowing when reading renderTemplate.
Prior art consulted.
React Fizz, ~/Documents/Projects/frameworks/react/packages/react-server/src/ReactFizzServer.js:1447. The comment reads "Each time we enter a suspense boundary, we split out into a new segment for the fallback so that we can later replace that segment with the content", and at line 1464, "The children of the boundary segment is actually the fallback." React makes the fallback the boundary segment's rendered children on the SERVER, emitted into the stream, and replaces the segment later. That is structurally what WebJs does with data-webjs-fallback. In neither framework is the fallback a value the client is handed after boot. It is markup the server already wrote. This is the industry-standard shape and it settles the ordering question.
lit-ssr, ~/Documents/Projects/frameworks/lit/packages/labs/ssr/src/lib/render-value.ts:1099. renderPropertyPart returns undefined for any property that is not a reflected IDL attribute, so lit-ssr emits NOTHING into the HTML for a rich property binding on a custom element. The data-webjs-prop-* channel is WebJs's deliberate divergence from that, and packages/core/src/component.js:836 is where it lands, on the client. Reading lit-ssr confirms there is no server-side prop channel to fall back to. The corrected sentence is therefore not describing a WebJs quirk, it is describing the only ordering the platform allows.
Alternatives considered and rejected.
Keep both reasons on the docs page, timing first. Rejected. The docs-site paragraph is a reader-facing sentence inside a page about using the boundary, not about the renderer's internals. The serializer half is a fact about a channel the reader is being told never to use, so carrying it there adds a clause without adding a decision the reader can make. The reviewed wording in the issue already settled this and the parenthetical stays single-clause.
Drop the parenthetical entirely and just say the property hole must be unquoted. Rejected. The parenthetical exists because a reader who knows about data-webjs-prop-* from website/app/docs/ssr/page.ts will ask why this one is different. Deleting the answer reopens the question the sentence was written to close.
Also rewrite the packages/core/src/render-server.js:403 comment for symmetry. Rejected. That comment is already correct and already leads with the full picture. Editing it would stage a packages/*/src file, which arms the require-docs-with-src.sh and require-tests-with-src.sh gates over a change that alters no behaviour, for no gain.
Leave packages/core/AGENTS.md for a follow-up. Rejected on the explicit instruction below, and on merit. The two sentences drifted from the same source comment, so fixing one and leaving the other guarantees the next reader hits the wrong one.
Implementation plan
No follow-up issues. The implementer files NO follow-up issue for anything this work turns up, and fixes every finding inside this same PR. If a finding is genuinely out of reach of a single PR, it is reported to the user as a note in the PR description, never filed as an issue.
Branch prefix docs/, per AGENTS.md. Cut a worktree first, per the unconditional worktree rule. Note that no node_modules install is needed for the steps below unless step 4 is run.
<p>The <code>.fallback</code> is read at SSR as the inline placeholder (never through the <code>data-webjs-prop-*</code> path, since a <code>TemplateResult</code> is not serializer-safe) and must be an unquoted property hole. <code>renderFallback()</code> on a component is a DIFFERENT concern (the client re-fetch loading state, never the first paint); see <a href="/docs/loading-states">Loading States</a>.</p>
After:
<p>The <code>.fallback</code> is read at SSR as the inline placeholder (never through the <code>data-webjs-prop-*</code> path, because that applies the property at hydration, far too late for a placeholder that has to be in the first flushed bytes) and must be an unquoted property hole. <code>renderFallback()</code> on a component is a DIFFERENT concern (the client re-fetch loading state, never the first paint); see <a href="/docs/loading-states">Loading States</a>.</p>
Only the parenthetical changes. The second sentence, the renderFallback() clause and the Loading States link, is unchanged. The paragraph stays a single line with the same four-space indentation as its neighbours at lines 88 and 93. This is a .ts file inside an html template in the in-repo website app, so the <p> is template text and no escaping changes are needed.
Step 2. Correct the webjs-suspense.js row at packages/core/AGENTS.md:38.
The row is one long table cell. Change only the parenthetical inside it.
Today:
`injectDSD`'s `processSuspenseElements` pre-pass reads `.fallback` (carried as `data-webjs-fallback` by `renderTemplate`, since a TemplateResult is not serializer-safe) and, in a streaming context, flushes the fallback as `<webjs-suspense id="sN">`
After:
`injectDSD`'s `processSuspenseElements` pre-pass reads `.fallback` (carried as `data-webjs-fallback` by `renderTemplate`, because the normal `data-webjs-prop-*` path applies at hydration, far too late for a placeholder that has to be in the first flushed bytes, and a `TemplateResult` would not survive that path anyway) and, in a streaming context, flushes the fallback as `<webjs-suspense id="sN">`
Everything else in the row, including the display:contents note, the swap-path sentence and the SSR-inert marker, is unchanged. Keep it on one line, since it is a markdown table row and a newline would break the table.
Step 3. Confirm no third occurrence was introduced. Run, from the repo root:
The only remaining hit must be packages/core/src/render-server.js:404, the canonical source comment, which stays as it is. Two occurrences exist at HEAD e5806e24 and both are fixed by steps 1 and 2. This grep is the guard against a third appearing between the filing of this issue and the branch.
Step 4 (optional verification, needs a linked worktree).cd website && npx webjs check. The website app is unaffected structurally by a prose edit, so this is a formality rather than a real risk, but it is listed in the acceptance criteria and is cheap once the worktree is linked with npm run worktree:link.
Surfaces deliberately NOT touched, each verified clean at HEAD.
packages/core/src/render-server.js:403. Already correct, already leads with the full picture. See rejected alternative 3.
AGENTS.md:227 and AGENTS.md:272. Both describe <webjs-suspense> and neither mentions the data-webjs-prop-* path or serializer-safety, so neither carries the defect.
.agents/skills/webjs/references/client-router-and-streaming.md:198, .agents/skills/webjs/references/components.md:276, .agents/skills/webjs/references/muscle-memory-gotchas.md:221. All three show the boundary and its fallback without explaining the channel.
packages/cli/templates/gallery/app/features/suspense/page.ts:26. Says only that .fallback is an unquoted property hole per invariant 4, which is true and complete for a gallery demo.
website/app/docs/data-fetching/page.ts:41, website/app/docs/components/page.ts:470, website/app/docs/lifecycle/page.ts:100, packages/mcp/src/mcp-docs.js:401, blog/component-suspense-streaming.md:38, blog/async-render-in-web-components.md:88, examples/blog/app/stream-demo/page.ts:30. All show or describe the boundary without the wrong justification.
website/app/docs/ssr/page.ts:146 and website/app/docs/ssr/page.ts:161. Already state the hydration timing of data-webjs-prop-* correctly. They are the pages this correction brings the suspense page into agreement with.
website/lib/ui/docs-shell.ts:288. Mentions data-webjs-prop-* for an unrelated reason (why the docs nav is slotted rather than passed as a property) and is correct.
Tests
No new test is written, and this is a deliberate decision rather than an omission.
Which layers do not apply and why:
Unit. There is nothing to assert. The change alters two explanatory sentences and no code path, no export, no rendered structure and no attribute. The only test that could exist would assert.match the paragraph against a substring of its own new wording, which pins prose to itself. That has no defect class behind it, breaks on any future rewording, and has no precedent in the repo. Everything under test/docs/ asserts structural or API facts instead (doc-source-consistency.test.mjs checks that imports shown in doc fences resolve against the real exported surface, llms.test.mjs, site-entity-graph.test.mjs, migration-page.test.mjs and the rest follow the same pattern), and nothing there asserts an explanatory sentence.
Browser. No component, no DOM behaviour, no hydration path changes. Nothing to observe in a browser that differs before and after.
e2e. No route, response or navigation changes.
Smoke. The scaffold is untouched. packages/cli/templates/gallery/app/features/suspense/page.ts was checked and carries no wrong explanation, so no scaffold surface moves.
Bun parity. No runtime-sensitive surface is touched. Nothing under packages/*/src is edited at all, so require-bun-parity-with-runtime-src.sh does not arm and no test/bun/** assertion is warranted.
Counterfactual, and why none exists. A useful counterfactual reds when the change is reverted. Reverting this change restores a sentence that is misleading but syntactically and structurally identical, so no mechanical assertion can distinguish the two states without quoting the wording. That is the definition of a prose-pinning test, which the repo does not do.
The existing regression guard that does cover the edit.website/test/ssr/docs-links.test.ts boots a real request handler over the website app, scrapes href= from every file under website/app/docs/, and fetches every internal /docs/... link. It therefore renders /docs/suspense and resolves the <a href="/docs/loading-states"> that lives inside the very paragraph being edited. A malformed html template, a stray backtick (invariant 9) or a broken link introduced by the edit reds that suite. Run it, do not extend it:
cd website && npm test
Report the result. Also confirm the grep in step 3 comes back with only the one expected source-comment hit, which is the closest thing to a verification this change admits.
Docs
This IS a documentation change, so the doc gate is satisfied by construction. Every surface touched, in full:
website/app/docs/suspense/page.ts:94. The docs-site Suspense page, the primary target.
packages/core/AGENTS.md:38. The @webjsdev/core per-package agent-facing module table, the second occurrence.
No other surface needs a change. AGENTS.md, .agents/skills/webjs/SKILL.md, .agents/skills/webjs/references/*.md, packages/cli/templates/**, README.md, the marketing pages under website/, the blog/ posts and docs/ were each grepped for the serializer justification and none carries it. The per-surface verification is enumerated under "Surfaces deliberately NOT touched" in the implementation plan.
The require-docs-with-src.sh gate fires only on staged packages/*/src source. Neither file staged here is under a src/ directory, so the gate does not arm and no bypass env var is needed.
Acceptance criteria
website/app/docs/suspense/page.ts gives the hydration-timing reason and no longer names serializer-safety in that parenthetical
packages/core/AGENTS.md's webjs-suspense.js row leads with the hydration-timing reason, keeping serializer-safety as the trailing secondary fact
The rest of both edited paragraphs is byte-identical to HEAD, including the renderFallback() sentence, the Loading States link, and every other clause of the module-table row
packages/core/src/render-server.js is unmodified
The step 3 grep returns exactly one hit, packages/core/src/render-server.js:404
cd website && npm test passes, including website/test/ssr/docs-links.test.ts
cd website && npx webjs check passes
The prose hook accepts every commit (no em-dash, no space-surrounded hyphen or semicolon used as a pause, WebJs capitalized where it names the project)
No follow-up issue was filed. Anything the work turned up is either fixed in this PR or noted in the PR description
The PR body carries Closes #1324
Out of scope
These are non-goals the implementer must not widen into, not deferred work.
Rewriting the Suspense docs page. Only the one parenthetical at line 94 changes. The surrounding list, the code sample, the "When to Use Suspense" section and the closing paragraph are correct and stay.
Rewriting the packages/core/AGENTS.md module table. Only the one parenthetical in the webjs-suspense.js row changes. Other rows, and the rest of that row, stay.
Touching packages/core/src/render-server.js or packages/core/src/component.js. They are the evidence for the correction, not its target. Editing either would arm the source gates over a zero-behaviour change.
Changing the data-webjs-fallback mechanism, the data-webjs-prop-* channel, or anything about how <webjs-suspense> streams. This issue documents the existing design correctly. It does not revisit it.
Adding a prose-pinning test. Argued in the Tests section. Do not add one to satisfy a habit of shipping a test with every change.
Broadening into a general audit of every data-webjs-prop-* mention across the repo. The greps in this issue already enumerated them and found the rest correct. Re-auditing surfaces already cleared here is not part of the task.
Problem
Two doc surfaces give the wrong reason for why a
<webjs-suspense>.fallbackis read inline at SSR instead of riding thedata-webjs-prop-*channel. Both name serializer-safety, which is a true but secondary fact, and neither names the constraint that actually forces the design.Occurrence 1 (the one the issue was filed for).
website/app/docs/suspense/page.ts:94, verified present at HEAD, reads verbatim:Occurrence 2 (not named in the original filing, found by grepping the repo).
packages/core/AGENTS.md:38, thewebjs-suspense.jsrow of the module table, carries the same serializer-only parenthetical:Serializer-safety explains why a
TemplateResultcannot ride that path. It does not explain why it must not. The load-bearing constraint is TIMING. Thedata-webjs-prop-*channel applies the property during hydration, which is far too late for a placeholder whose entire job is to be in the first flushed bytes.The distinction matters because the current wording points at a fixable-looking obstacle. A reader could reasonably conclude that making
TemplateResultserializable would unlock thedata-webjs-prop-*path, and spend real time on it. It would not. Even a perfectly serializable fallback applied at hydration arrives after the shell has flushed, so there is no placeholder during the window the boundary exists for.The timing claim, proven against the source
The correction asserts a fact about when each path runs. Both halves are verified in the shipped source, so the corrected sentence is grounded rather than asserted.
The
.fallbackis consumed during the SSR byte emission.packages/core/src/render-server.js:403sits inside thekind === 'prop'branch ofrenderTemplate's attribute scanner and special-cases the boundary before the generic property path can run:Note that this comment ALREADY gives both reasons, timing included, and gives the timing reason the weight it deserves. It is the canonical statement and needs no change. The two doc surfaces are the drifted copies.
processSuspenseElements(declared atpackages/core/src/render-server.js:1243, called frominjectDSDatpackages/core/src/render-server.js:910) then reads that carrier atpackages/core/src/render-server.js:1280and writes the fallback markup straight into the shell atpackages/core/src/render-server.js:1310:Those bytes are the shell. Nothing client-side is involved, which is why the docblock at
packages/core/src/render-server.js:1234can say first-load streaming needs no client runtime.The
data-webjs-prop-*channel applies at hydration.WebComponent'sconnectedCallbackatpackages/core/src/component.js:836is the only caller of the decoder:_hydratePropAttrs()atpackages/core/src/component.js:904scansdata-webjs-prop-*attributes, decodes each through the wire serializer, assigns the property, and removes the attribute. Theif (!isBrowser) returnguard on the first line is the proof in one line. That path is browser-only by construction, so it cannot run before the server has flushed anything. The docs site already documents this timing independently atwebsite/app/docs/ssr/page.ts:146andwebsite/app/docs/ssr/page.ts:161, both of which correctly say the attributes are applied and stripped onconnectedCallback. So the suspense page contradicts the SSR page as the repo stands.What is stale in the original filing
website/app/docs/suspense/page.ts. It missedpackages/core/AGENTS.md:38, which repeats the same serializer-only explanation on the framework's own agent-facing surface. Both are in scope now.grep -rn "serializer-safe" test/ website/test/returns nothing, and no file undertest/docs/orwebsite/test/ssr/referencesdocs/suspense. There is nothing to update, so the implementer does not need to repeat the search.Provenance. This correction was written as part of PR #1314, which was closed unmerged as superseded by PR #1317 (#1307). The line is unrelated to that PR's subject and would otherwise have been lost with it, which is the only reason this issue exists. Nothing else from #1314 is worth salvaging.
Design / approach
Settled decision. Lead the parenthetical with the timing reason on every surface that carries it. On the docs page, replace the serializer clause outright with the reviewed wording. On
packages/core/AGENTS.md, lead with timing and keep serializer-safety as the trailing secondary fact, matching the ordering the source comment atpackages/core/src/render-server.js:403already uses.Why this ordering is right. A reason a reader can act on belongs first. Serializer-safety reads as a property of the payload, so it invites the reader to change the payload. Timing is a property of the pipeline, and no change to the fallback value moves a hydration-time write earlier than a server flush. Putting timing first makes the constraint unarguable at a glance, and keeping serializer-safety second on the source-facing surface preserves the fact that the channel would also drop the value, which is real and worth knowing when reading
renderTemplate.Prior art consulted.
~/Documents/Projects/frameworks/react/packages/react-server/src/ReactFizzServer.js:1447. The comment reads "Each time we enter a suspense boundary, we split out into a new segment for the fallback so that we can later replace that segment with the content", and at line 1464, "The children of the boundary segment is actually the fallback." React makes the fallback the boundary segment's rendered children on the SERVER, emitted into the stream, and replaces the segment later. That is structurally what WebJs does withdata-webjs-fallback. In neither framework is the fallback a value the client is handed after boot. It is markup the server already wrote. This is the industry-standard shape and it settles the ordering question.~/Documents/Projects/frameworks/lit/packages/labs/ssr/src/lib/render-value.ts:1099.renderPropertyPartreturnsundefinedfor any property that is not a reflected IDL attribute, so lit-ssr emits NOTHING into the HTML for a rich property binding on a custom element. Thedata-webjs-prop-*channel is WebJs's deliberate divergence from that, andpackages/core/src/component.js:836is where it lands, on the client. Reading lit-ssr confirms there is no server-side prop channel to fall back to. The corrected sentence is therefore not describing a WebJs quirk, it is describing the only ordering the platform allows.Alternatives considered and rejected.
data-webjs-prop-*fromwebsite/app/docs/ssr/page.tswill ask why this one is different. Deleting the answer reopens the question the sentence was written to close.packages/core/src/render-server.js:403comment for symmetry. Rejected. That comment is already correct and already leads with the full picture. Editing it would stage apackages/*/srcfile, which arms therequire-docs-with-src.shandrequire-tests-with-src.shgates over a change that alters no behaviour, for no gain.packages/core/AGENTS.mdfor a follow-up. Rejected on the explicit instruction below, and on merit. The two sentences drifted from the same source comment, so fixing one and leaving the other guarantees the next reader hits the wrong one.Implementation plan
No follow-up issues. The implementer files NO follow-up issue for anything this work turns up, and fixes every finding inside this same PR. If a finding is genuinely out of reach of a single PR, it is reported to the user as a note in the PR description, never filed as an issue.
Branch prefix
docs/, per AGENTS.md. Cut a worktree first, per the unconditional worktree rule. Note that nonode_modulesinstall is needed for the steps below unless step 4 is run.Step 1. Correct
website/app/docs/suspense/page.ts:94.Today:
After:
Only the parenthetical changes. The second sentence, the
renderFallback()clause and the Loading States link, is unchanged. The paragraph stays a single line with the same four-space indentation as its neighbours at lines 88 and 93. This is a.tsfile inside anhtmltemplate in the in-repowebsiteapp, so the<p>is template text and no escaping changes are needed.Step 2. Correct the
webjs-suspense.jsrow atpackages/core/AGENTS.md:38.The row is one long table cell. Change only the parenthetical inside it.
Today:
After:
Everything else in the row, including the
display:contentsnote, the swap-path sentence and the SSR-inert marker, is unchanged. Keep it on one line, since it is a markdown table row and a newline would break the table.Step 3. Confirm no third occurrence was introduced. Run, from the repo root:
The only remaining hit must be
packages/core/src/render-server.js:404, the canonical source comment, which stays as it is. Two occurrences exist at HEADe5806e24and both are fixed by steps 1 and 2. This grep is the guard against a third appearing between the filing of this issue and the branch.Step 4 (optional verification, needs a linked worktree).
cd website && npx webjs check. The website app is unaffected structurally by a prose edit, so this is a formality rather than a real risk, but it is listed in the acceptance criteria and is cheap once the worktree is linked withnpm run worktree:link.Surfaces deliberately NOT touched, each verified clean at HEAD.
packages/core/src/render-server.js:403. Already correct, already leads with the full picture. See rejected alternative 3.AGENTS.md:227andAGENTS.md:272. Both describe<webjs-suspense>and neither mentions thedata-webjs-prop-*path or serializer-safety, so neither carries the defect..agents/skills/webjs/references/client-router-and-streaming.md:198,.agents/skills/webjs/references/components.md:276,.agents/skills/webjs/references/muscle-memory-gotchas.md:221. All three show the boundary and its fallback without explaining the channel.packages/cli/templates/gallery/app/features/suspense/page.ts:26. Says only that.fallbackis an unquoted property hole per invariant 4, which is true and complete for a gallery demo.website/app/docs/data-fetching/page.ts:41,website/app/docs/components/page.ts:470,website/app/docs/lifecycle/page.ts:100,packages/mcp/src/mcp-docs.js:401,blog/component-suspense-streaming.md:38,blog/async-render-in-web-components.md:88,examples/blog/app/stream-demo/page.ts:30. All show or describe the boundary without the wrong justification.website/app/docs/ssr/page.ts:146andwebsite/app/docs/ssr/page.ts:161. Already state the hydration timing ofdata-webjs-prop-*correctly. They are the pages this correction brings the suspense page into agreement with.website/lib/ui/docs-shell.ts:288. Mentionsdata-webjs-prop-*for an unrelated reason (why the docs nav is slotted rather than passed as a property) and is correct.Tests
No new test is written, and this is a deliberate decision rather than an omission.
Which layers do not apply and why:
assert.matchthe paragraph against a substring of its own new wording, which pins prose to itself. That has no defect class behind it, breaks on any future rewording, and has no precedent in the repo. Everything undertest/docs/asserts structural or API facts instead (doc-source-consistency.test.mjschecks that imports shown in doc fences resolve against the real exported surface,llms.test.mjs,site-entity-graph.test.mjs,migration-page.test.mjsand the rest follow the same pattern), and nothing there asserts an explanatory sentence.packages/cli/templates/gallery/app/features/suspense/page.tswas checked and carries no wrong explanation, so no scaffold surface moves.packages/*/srcis edited at all, sorequire-bun-parity-with-runtime-src.shdoes not arm and notest/bun/**assertion is warranted.Counterfactual, and why none exists. A useful counterfactual reds when the change is reverted. Reverting this change restores a sentence that is misleading but syntactically and structurally identical, so no mechanical assertion can distinguish the two states without quoting the wording. That is the definition of a prose-pinning test, which the repo does not do.
The existing regression guard that does cover the edit.
website/test/ssr/docs-links.test.tsboots a real request handler over thewebsiteapp, scrapeshref=from every file underwebsite/app/docs/, and fetches every internal/docs/...link. It therefore renders/docs/suspenseand resolves the<a href="/docs/loading-states">that lives inside the very paragraph being edited. A malformedhtmltemplate, a stray backtick (invariant 9) or a broken link introduced by the edit reds that suite. Run it, do not extend it:Report the result. Also confirm the grep in step 3 comes back with only the one expected source-comment hit, which is the closest thing to a verification this change admits.
Docs
This IS a documentation change, so the doc gate is satisfied by construction. Every surface touched, in full:
website/app/docs/suspense/page.ts:94. The docs-site Suspense page, the primary target.packages/core/AGENTS.md:38. The@webjsdev/coreper-package agent-facing module table, the second occurrence.No other surface needs a change.
AGENTS.md,.agents/skills/webjs/SKILL.md,.agents/skills/webjs/references/*.md,packages/cli/templates/**,README.md, the marketing pages underwebsite/, theblog/posts anddocs/were each grepped for the serializer justification and none carries it. The per-surface verification is enumerated under "Surfaces deliberately NOT touched" in the implementation plan.The
require-docs-with-src.shgate fires only on stagedpackages/*/srcsource. Neither file staged here is under asrc/directory, so the gate does not arm and no bypass env var is needed.Acceptance criteria
website/app/docs/suspense/page.tsgives the hydration-timing reason and no longer names serializer-safety in that parentheticalpackages/core/AGENTS.md'swebjs-suspense.jsrow leads with the hydration-timing reason, keeping serializer-safety as the trailing secondary factrenderFallback()sentence, the Loading States link, and every other clause of the module-table rowpackages/core/src/render-server.jsis unmodifiedpackages/core/src/render-server.js:404cd website && npm testpasses, includingwebsite/test/ssr/docs-links.test.tscd website && npx webjs checkpassesWebJscapitalized where it names the project)Closes #1324Out of scope
These are non-goals the implementer must not widen into, not deferred work.
packages/core/AGENTS.mdmodule table. Only the one parenthetical in thewebjs-suspense.jsrow changes. Other rows, and the rest of that row, stay.packages/core/src/render-server.jsorpackages/core/src/component.js. They are the evidence for the correction, not its target. Editing either would arm the source gates over a zero-behaviour change.data-webjs-fallbackmechanism, thedata-webjs-prop-*channel, or anything about how<webjs-suspense>streams. This issue documents the existing design correctly. It does not revisit it.data-webjs-prop-*mention across the repo. The greps in this issue already enumerated them and found the rest correct. Re-auditing surfaces already cleared here is not part of the task.