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
Six published statements about WebJs are wrong against the code they describe. Five are in blog posts, which is the surface search engines and AI agents read most, and one is a source comment that is the upstream cause of a wrong claim that already had to be corrected in two other places.
A wrong claim on the blog is worse than a wrong line in a reference doc. A reader who tries to satisfy a webjs check rule that never existed, or who expects an esbuild fallback that was deleted, is chasing something the framework has never had.
blog/ai-first-is-plumbing.md names four webjs check rules that do not exist, and states the wrong rule count. Lines 52 to 61 list nine rules. Four are absent from RULES: tests-exist, no-server-imports-in-components (the real rule is no-server-import-in-browser-module, which does something different), no-json-data-files, and light-dom-css-prefix. Line 65 says "We have ~10 rules, not 100"; there are 20. Line 63 says new rules are about 20 lines apiece; the last three added 73, 109 and 165 lines.
blog/light-dom-by-default.md:113 tells the reader webjs check ships a light-dom-css-prefix rule that "flags unprefixed class selectors in vanilla CSS for light-DOM components". It does not. The tag-prefix requirement is prose only (AGENTS.md invariant 7), so the post promises a safety net that will never fire.
blog/stop-ai-agents-breaking-your-code.md:87 names the same phantom light-dom-css-prefix rule. Found by a corpus-wide scan rather than by reading, and it sits in a list whose other four entries are real.
blog/strip-types-not-esbuild.md:62 describes an esbuild fallback that no longer exists. It says the server falls back to esbuild.transform on ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, emitting an inline sourcemap. There is no esbuild in the strip path. packages/server/src/ts-strip.js has exactly two backends, and packages/server/src/dev.js:145 states in source that there is no bundler fallback. The same post's code block at line 55 also shows const TS_CACHE = new Map();, an identifier that does not exist.
packages/server/src/csp.js attributes the style-src inline allowance to Tailwind. Lines 53 and 70 say Tailwind's browser runtime injects a <style> element. That is not why the default needs 'unsafe-inline', and it misleads: an app can drop Tailwind and still need it. The scaffold compiles Tailwind to a static public/tailwind.css, so a scaffolded app has no Tailwind browser runtime at all. This comment is the origin of the same wrong claim that had to be corrected on website/app/docs/deployment/page.ts and in blog/no-build-via-jspm-io.md.
blog/client-router-turbo-drive-style.md states three explicit router non-goals and two are no longer true. Line 76 says the router does not prefetch, but link prefetch now ships on by default with a device-adaptive strategy. Line 80 says each navigation re-fetches the page from scratch, but the X-Webjs-Have header now returns only the divergent fragment, a mechanism the same post already describes at line 55. The middle non-goal (View Transitions off by default) is still accurate.
Implementation plan
Every claim below was re-verified against the current tree before this plan was written. This is a prose-only correction. No behaviour changes, no new API, no test-observable surface moves. The one source file touched is edited in its comments only.
Two shape decisions are settled here rather than left to the implementer. First, the blog stops enumerating the rule set and stops printing a rule count, because a hand-maintained list inside a post is precisely what drifted, so the posts name a few real rules as examples and point at webjs check --rules for the current set. Second, the CSP comment enumerates all four sources of un-nonced style rather than swapping Tailwind for one other single cause, since the single-cause framing is what consumed review rounds previously.
Verified claims
Claim in the corpus
Governing source
Verdict
tests-exist, no-json-data-files, light-dom-css-prefix are check rules
RULES in packages/server/src/check.js
Never existed
no-server-imports-in-components is a check rule
Real name is no-server-import-in-browser-module (check.js:129)
Wrong name, wrong description
The other five rules named in ai-first-is-plumbing.md
check.jsRULES
Still true, keep unchanged
"We have ~10 rules, not 100"
RULES.length is 20, read by importing the module
Wrong
"New rules are about 20 lines apiece"
The last three rules added 73, 109 and 165 lines to check.js
Wrong, off by roughly 4x
The scaffolded hook list in ai-first-is-plumbing.md
packages/cli/templates/
Still true, do not widen the edit into it
esbuild fallback on ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX
packages/server/src/ts-strip.js, plus dev.js:145 stating in source that there is no bundler fallback
Wrong
const TS_CACHE = new Map();
No such identifier. The cache is per-handler state.tsCache; only TS_CACHE_MAX is module-level (dev.js:153, reason at dev.js:2572)
Wrong, and not recorded in the original Problem section
The esbuild loader-hook history at strip-types-not-esbuild.md:50
ts-strip.js header, blog/no-build-via-jspm-io.md
Still true, keep including its existing PR #9 reference
esbuild at website/app/docs/no-build/page.ts:156
Publish-time dist/ bundling of @webjsdev/core, a real and separate thing
Scaffold compiles Tailwind to a static public/tailwind.css (packages/cli/lib/create.js:324), so a scaffolded app has no Tailwind runtime
Wrong, twice over
The framework's own head <style> carries the nonce
packages/server/src/ssr.js:1881 emits <style${n}>
Still true, and is the contrast the new comment turns on
Component styles carry no nonce
packages/core/src/css.js:53 emits a bare <style> and takes no nonce
New fact, needed for the fix
"No prefetching" (client-router-turbo-drive-style.md:76)
packages/core/src/router-client.js:1639, the adaptive prefetch default
Wrong
"No view-transitions API by default" (line 78)
router-client.js:2725, opt-in via <meta name="view-transition" content="same-origin">
Still true, keep
"No nested-route data deduplication" (line 80)
The x-webjs-have short-circuit in packages/server/src/ssr.js
Wrong
Two guards were run corpus-wide and must be re-run after the edits. A scan of every rule-shaped backticked token in blog/ and website/app/docs/ against RULES found exactly the four phantom names above and nothing else, so there is no sixth surprise waiting. And esbuild survives in five legitimate places listed under Landmines.
Rejected
Renaming the client-router-turbo-drive-style slug. It is a published URL and an SEO asset, it is linked from blog/strict-csp-nonce-across-soft-navigation.md:51, and the borrowed-framing concern was already answered by an earlier title change.
Deleting the "What the router does not do" section outright. The post's argument is better for stating a real limit, and one real non-goal survives.
Keeping an enumerated rule list in either blog post and just correcting the names. That list is what drifted, and it will drift again on the next rule.
Printing the corrected count "20 rules". Same failure mode with a one-release fuse.
Adding a test that pins the blog prose to RULES. Prose is not a test surface, and it would red every future rule addition on an unrelated file.
Replacing Tailwind in the CSP comment with one other single cause.
Widening the edit into ai-first-is-plumbing.md's hook list. Every claim there was checked and holds.
Steps
blog/ai-first-is-plumbing.md, "The convention validator". Delete the tests-exist, no-json-data-files and light-dom-css-prefix bullets. Replace the no-server-imports-in-components bullet with no-server-import-in-browser-module, described as what it does, which is that a module genuinely shipping to the browser must not import a server-only file, because the client gets a stub that throws at load. Keep the five accurate bullets byte-identical. Delete the "New rules are about 20 lines apiece" sentence. Replace "We have ~10 rules, not 100" with the same argument stated without a count, plus a pointer to webjs check --rules.
blog/light-dom-by-default.md:113. Replace the clause after "The same applies to other naming-discipline approaches like BEM or scoped class prefixes" so it states the tag-prefix requirement as a convention the author follows, not a rule the checker enforces, which is also a reason Tailwind stays the lower-effort default. Do not claim any tooling backs it.
blog/stop-ai-agents-breaking-your-code.md:87. Replace the light-dom-css-prefix bullet with no-browser-globals-in-render (check.js:119), which fits the section's "failure modes an agent trips over" framing, described as a browser global in the constructor or render() crashing the SSR pass. Keep the surrounding four bullets and the webjs check --rules snippet above them.
blog/strip-types-not-esbuild.md. In the code block at line 54, drop the phantom const TS_CACHE = new Map(); line and keep TS_CACHE_MAX and the entry-shape comment. In the paragraph below it, note that the cache is per request handler rather than module-global, because the cached bytes bake in that handler's elision verdict. Then replace the esbuild paragraph at line 62 entirely: there is no fallback, non-erasable syntax throws at strip time and the server returns a clean 500, and erasable-typescript-only plus no-non-erasable-typescript catch it at edit time. State the one thing that does vary, the backend: Node's built-in module.stripTypeScriptTypes, and amaro on Bun, whose strip-only mode is what the Node built-in wraps, so the output is byte-identical. Leave line 50 alone.
packages/server/src/csp.js, the DEFAULT_DIRECTIVES doc block around line 53. Replace the Tailwind clause with a pointer to app-authored styles that carry no nonce, deferring the detail to the style-src comment.
packages/server/src/csp.js, the inline comment above style-src around line 70. Replace it with the enumerated real reason. WebJs's own head <style> (the @layer webjs-host rule) IS nonce-signed, so the allowance exists for app-authored style: a page or layout's inline <style>, a light-DOM component's <style>, a shadow component's static styles serialized into its declarative shadow root, and every style="..." attribute, the last of which can never carry a nonce because the mechanism applies to elements and not attributes. Name no styling library. Keep the existing closing sentence that style elements are not a script-injection vector.
blog/client-router-turbo-drive-style.md, the "What the router does not do" section at lines 72 to 80. Three non-goals are now one, so rewrite the opening line. Keep the View Transitions item and name the <meta name="view-transition"> opt-in. Replace the prefetching item with what is true now, prefetch on by default with the strategy chosen per device, hover intent where there is a pointer and dwell-gated viewport entry on touch, linking /blog/device-adaptive-link-prefetch. Replace the deduplication item with a back-reference to the X-Webjs-Have mechanism the same post already describes. No maturity language about the old design.
Run the corpus grep from the Acceptance criteria and confirm nothing else states any old version.
Tests
N/A for the prose, because no test layer covers blog copy. website/test/ssr/pre-block-a11y.test.ts renders the shared markdown post body, but from a synthetic inline string rather than a real post file, so no test pins the sentences being edited and none should be added to.
packages/server/test/csp/csp.test.js covers buildCspHeader and DEFAULT_DIRECTIVES behaviour. The change to csp.js is comment-only, so that suite must stay green untouched, which is itself the check that no directive value moved.
Counterfactual: none is possible for a prose fix, which is why the verification table above exists in its place. The substitute is the corpus scan plus a manual re-read of each edited claim against the file named in the table.
Verification
Re-run the phantom-rule scan. Import RULES from packages/server/src/check.js and assert every rule-shaped backticked token surviving in blog/ and website/app/docs/ is in it. Must come back empty. Do not count rules by eye or from CLI output; three rule names are long enough to change the column spacing, which is how a wrong count got published before.
Re-run the esbuild / light-dom-css-prefix / unsafe-inline greps and confirm the five legitimate mentions are intact.
Confirm csp.js is comment-only. Its diff must show no change to any directive string, and packages/server/test/csp/csp.test.js must pass unedited.
Boot the website in prod mode through createRequestHandler (pass the PROJECT ROOT as appDir, since buildRouteTable does join(appDir, 'app')) and GET each edited post plus /blog and /blog/device-adaptive-link-prefetch, asserting 200 and no broken modulepreload hints. Nothing pins post prose, so this render is the only thing standing between a broken fence and production, and website/test/ssr/docs-links.test.ts validates /docs/... links only, so the new /blog/... cross-link is checked here or nowhere.
Re-run the CSP probe empirically rather than trusting a recorded result. Boot a throwaway app with "webjs": { "csp": true } carrying a page-level inline <style>, a style="..." attribute, a light-DOM component with a <style>, and a shadow component with static styles, and no Tailwind anywhere. Expect four <style> tags with exactly one nonce, on the framework's head rule. Keep the scratch app outside the repo with node_modules symlinked in.
Full suites after the review cycle. Browser, e2e and Bun layers are N/A for a prose change with a comment-only source edit, stated as such in the PR body.
The docs site does NOT repeat any of these claims. website/app/docs/conventions/page.ts:34 describes the checker's narrowness without enumerating rules or printing a count, and website/app/docs/troubleshooting/page.ts names only real rules. No light-dom-css-prefix or stale-esbuild reference exists under website/app/docs/. So no docs-site edit is owed, and none should be invented.
AGENTS.md and .agents/skills/webjs/ need no change. Invariant 7 already states the tag prefix as a convention and invariant 10 already states there is no bundler fallback, which is what makes the blog posts wrong rather than the reference docs.
Implementation notes (for the implementing agent)
Landmines.
Every prose file here is under .claude/hooks/block-prose-punctuation.sh. No em-dashes, no space-hyphen-space or space-semicolon-space as pause punctuation, and WebJs capitalized in prose except as a literal code token. The hook scans new content only, so an existing line can be edited freely.
The hook's colon rule is narrower than it looks, and this was checked against the script rather than assumed. It fires only on )</code>:, on a hyphenated <my-tag>:, and on a comment line shaped like foo(): prose. A markdown backtick span holding a plain identifier matches none of them, so ai-first-is-plumbing.md keeps its existing `rule-name`: description list style and the replacement bullet joins it in that form, rather than restyling five correct lines for no reason. The comment-line form DOES bind the csp.js edit, so no new comment line there may be shaped someFn(): prose.
No internal issue or PR numbers in published blog prose. blog/strip-types-not-esbuild.md:50 already carries a PR #9 reference; leaving it is fine since it is existing content, but do not add more.
Do not write a bare webjs dev or webjs start into user-facing prose. webjs check and webjs check --rules are fine and already appear in both posts.
Do not frame Tailwind as something WebJs is coupled to. In the CSP comment it becomes absent entirely, and in light-dom-by-default.md it stays a recommended default rather than a requirement.
Step 5 and 6 touch packages/server/src/**, which trips .claude/hooks/require-docs-with-src.sh and .claude/hooks/require-tests-with-src.sh. The blog edits in the same commit should satisfy the doc gate; WEBJS_NO_DOC_GATE=1 is the documented bypass for a comment-only change with no behaviour change. csp.js is not on the Bun-parity hook's runtime-sensitive list, so no test/bun/** addition is needed for a comment.
The corpus cross-references itself. After editing, grep blog/ and website/app/docs/ for esbuild, light-dom-css-prefix, and unsafe-inline. Five mentions are correct and must not be "fixed": website/app/docs/no-build/page.ts:156 (publish-time bundling of the @webjsdev/core dist), blog/no-build-via-jspm-io.md (esbuild as removed history), blog/full-stack-type-safety-no-build.md:21 and blog/fix-relative-import-paths.md:39 (esbuild as a generic example of a build step), and blog/strict-csp-nonce-across-soft-navigation.md:40 (unsafe-inline inside a real header dump).
Invariants to respect. AGENTS.md invariant 7 (the light-DOM tag prefix is a convention, not a check rule) is the fact steps 2 and 3 turn on. Invariant 10 (erasable TypeScript, no bundler fallback) is the fact step 4 turns on.
Commit shape. One logical unit, one commit, docs: subject, in its own worktree cut from origin/main. A docs: prefix keeps it out of the changelog, which is right for a prose correction.
Acceptance criteria
blog/ai-first-is-plumbing.md no longer names a rule that is absent from RULES, states no rule count, and no longer claims a per-rule line count
blog/light-dom-by-default.md no longer promises a light-dom-css-prefix check rule
blog/stop-ai-agents-breaking-your-code.md no longer names light-dom-css-prefix, and every rule it names is in RULES
blog/strip-types-not-esbuild.md describes the real strip path (built-in stripper plus amaro, no bundler fallback) instead of an esbuild fallback, and its cache snippet names no identifier that does not exist
blog/client-router-turbo-drive-style.md no longer lists prefetching or per-navigation full refetch as router non-goals, keeps the View Transitions non-goal, and links the prefetch post
The packages/server/src/csp.js comment explains the style-src allowance by all four things that lack a nonce, names no styling library, and states that the framework's own head style IS nonce-signed
packages/server/test/csp/csp.test.js still passes with no edits to it, and the csp.js diff touches no directive string
Every rule name surviving anywhere in blog/ is present in RULES, asserted by importing the module rather than by eye
A grep across blog/ and website/app/docs/ confirms no other page states any old version, and the five legitimate esbuild / unsafe-inline mentions are still intact
The website serves each edited post at 200 with no broken modulepreload
Problem
Six published statements about WebJs are wrong against the code they describe. Five are in blog posts, which is the surface search engines and AI agents read most, and one is a source comment that is the upstream cause of a wrong claim that already had to be corrected in two other places.
A wrong claim on the blog is worse than a wrong line in a reference doc. A reader who tries to satisfy a
webjs checkrule that never existed, or who expects an esbuild fallback that was deleted, is chasing something the framework has never had.blog/ai-first-is-plumbing.mdnames fourwebjs checkrules that do not exist, and states the wrong rule count. Lines 52 to 61 list nine rules. Four are absent fromRULES:tests-exist,no-server-imports-in-components(the real rule isno-server-import-in-browser-module, which does something different),no-json-data-files, andlight-dom-css-prefix. Line 65 says "We have ~10 rules, not 100"; there are 20. Line 63 says new rules are about 20 lines apiece; the last three added 73, 109 and 165 lines.blog/light-dom-by-default.md:113tells the readerwebjs checkships alight-dom-css-prefixrule that "flags unprefixed class selectors in vanilla CSS for light-DOM components". It does not. The tag-prefix requirement is prose only (AGENTS.md invariant 7), so the post promises a safety net that will never fire.blog/stop-ai-agents-breaking-your-code.md:87names the same phantomlight-dom-css-prefixrule. Found by a corpus-wide scan rather than by reading, and it sits in a list whose other four entries are real.blog/strip-types-not-esbuild.md:62describes an esbuild fallback that no longer exists. It says the server falls back toesbuild.transformonERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, emitting an inline sourcemap. There is no esbuild in the strip path.packages/server/src/ts-strip.jshas exactly two backends, andpackages/server/src/dev.js:145states in source that there is no bundler fallback. The same post's code block at line 55 also showsconst TS_CACHE = new Map();, an identifier that does not exist.packages/server/src/csp.jsattributes thestyle-srcinline allowance to Tailwind. Lines 53 and 70 say Tailwind's browser runtime injects a<style>element. That is not why the default needs'unsafe-inline', and it misleads: an app can drop Tailwind and still need it. The scaffold compiles Tailwind to a staticpublic/tailwind.css, so a scaffolded app has no Tailwind browser runtime at all. This comment is the origin of the same wrong claim that had to be corrected onwebsite/app/docs/deployment/page.tsand inblog/no-build-via-jspm-io.md.blog/client-router-turbo-drive-style.mdstates three explicit router non-goals and two are no longer true. Line 76 says the router does not prefetch, but link prefetch now ships on by default with a device-adaptive strategy. Line 80 says each navigation re-fetches the page from scratch, but theX-Webjs-Haveheader now returns only the divergent fragment, a mechanism the same post already describes at line 55. The middle non-goal (View Transitions off by default) is still accurate.Implementation plan
Every claim below was re-verified against the current tree before this plan was written. This is a prose-only correction. No behaviour changes, no new API, no test-observable surface moves. The one source file touched is edited in its comments only.
Two shape decisions are settled here rather than left to the implementer. First, the blog stops enumerating the rule set and stops printing a rule count, because a hand-maintained list inside a post is precisely what drifted, so the posts name a few real rules as examples and point at
webjs check --rulesfor the current set. Second, the CSP comment enumerates all four sources of un-nonced style rather than swapping Tailwind for one other single cause, since the single-cause framing is what consumed review rounds previously.Verified claims
tests-exist,no-json-data-files,light-dom-css-prefixare check rulesRULESinpackages/server/src/check.jsno-server-imports-in-componentsis a check ruleno-server-import-in-browser-module(check.js:129)ai-first-is-plumbing.mdcheck.jsRULESRULES.lengthis 20, read by importing the modulecheck.jsai-first-is-plumbing.mdpackages/cli/templates/ERR_UNSUPPORTED_TYPESCRIPT_SYNTAXpackages/server/src/ts-strip.js, plusdev.js:145stating in source that there is no bundler fallbackconst TS_CACHE = new Map();state.tsCache; onlyTS_CACHE_MAXis module-level (dev.js:153, reason atdev.js:2572)strip-types-not-esbuild.md:50ts-strip.jsheader,blog/no-build-via-jspm-io.mdPR #9referencewebsite/app/docs/no-build/page.ts:156dist/bundling of@webjsdev/core, a real and separate thingstyle-src 'unsafe-inline'public/tailwind.css(packages/cli/lib/create.js:324), so a scaffolded app has no Tailwind runtime<style>carries the noncepackages/server/src/ssr.js:1881emits<style${n}>packages/core/src/css.js:53emits a bare<style>and takes no nonceclient-router-turbo-drive-style.md:76)packages/core/src/router-client.js:1639, the adaptive prefetch defaultrouter-client.js:2725, opt-in via<meta name="view-transition" content="same-origin">x-webjs-haveshort-circuit inpackages/server/src/ssr.jsTwo guards were run corpus-wide and must be re-run after the edits. A scan of every rule-shaped backticked token in
blog/andwebsite/app/docs/againstRULESfound exactly the four phantom names above and nothing else, so there is no sixth surprise waiting. Andesbuildsurvives in five legitimate places listed under Landmines.Rejected
client-router-turbo-drive-styleslug. It is a published URL and an SEO asset, it is linked fromblog/strict-csp-nonce-across-soft-navigation.md:51, and the borrowed-framing concern was already answered by an earlier title change.RULES. Prose is not a test surface, and it would red every future rule addition on an unrelated file.ai-first-is-plumbing.md's hook list. Every claim there was checked and holds.Steps
blog/ai-first-is-plumbing.md, "The convention validator". Delete thetests-exist,no-json-data-filesandlight-dom-css-prefixbullets. Replace theno-server-imports-in-componentsbullet withno-server-import-in-browser-module, described as what it does, which is that a module genuinely shipping to the browser must not import a server-only file, because the client gets a stub that throws at load. Keep the five accurate bullets byte-identical. Delete the "New rules are about 20 lines apiece" sentence. Replace "We have ~10 rules, not 100" with the same argument stated without a count, plus a pointer towebjs check --rules.blog/light-dom-by-default.md:113. Replace the clause after "The same applies to other naming-discipline approaches like BEM or scoped class prefixes" so it states the tag-prefix requirement as a convention the author follows, not a rule the checker enforces, which is also a reason Tailwind stays the lower-effort default. Do not claim any tooling backs it.blog/stop-ai-agents-breaking-your-code.md:87. Replace thelight-dom-css-prefixbullet withno-browser-globals-in-render(check.js:119), which fits the section's "failure modes an agent trips over" framing, described as a browser global in the constructor orrender()crashing the SSR pass. Keep the surrounding four bullets and thewebjs check --rulessnippet above them.blog/strip-types-not-esbuild.md. In the code block at line 54, drop the phantomconst TS_CACHE = new Map();line and keepTS_CACHE_MAXand the entry-shape comment. In the paragraph below it, note that the cache is per request handler rather than module-global, because the cached bytes bake in that handler's elision verdict. Then replace the esbuild paragraph at line 62 entirely: there is no fallback, non-erasable syntax throws at strip time and the server returns a clean 500, anderasable-typescript-onlyplusno-non-erasable-typescriptcatch it at edit time. State the one thing that does vary, the backend: Node's built-inmodule.stripTypeScriptTypes, andamaroon Bun, whosestrip-onlymode is what the Node built-in wraps, so the output is byte-identical. Leave line 50 alone.packages/server/src/csp.js, theDEFAULT_DIRECTIVESdoc block around line 53. Replace the Tailwind clause with a pointer to app-authored styles that carry no nonce, deferring the detail to thestyle-srccomment.packages/server/src/csp.js, the inline comment abovestyle-srcaround line 70. Replace it with the enumerated real reason. WebJs's own head<style>(the@layer webjs-hostrule) IS nonce-signed, so the allowance exists for app-authored style: a page or layout's inline<style>, a light-DOM component's<style>, a shadow component'sstatic stylesserialized into its declarative shadow root, and everystyle="..."attribute, the last of which can never carry a nonce because the mechanism applies to elements and not attributes. Name no styling library. Keep the existing closing sentence that style elements are not a script-injection vector.blog/client-router-turbo-drive-style.md, the "What the router does not do" section at lines 72 to 80. Three non-goals are now one, so rewrite the opening line. Keep the View Transitions item and name the<meta name="view-transition">opt-in. Replace the prefetching item with what is true now, prefetch on by default with the strategy chosen per device, hover intent where there is a pointer and dwell-gated viewport entry on touch, linking/blog/device-adaptive-link-prefetch. Replace the deduplication item with a back-reference to theX-Webjs-Havemechanism the same post already describes. No maturity language about the old design.Tests
website/test/ssr/pre-block-a11y.test.tsrenders the shared markdown post body, but from a synthetic inline string rather than a real post file, so no test pins the sentences being edited and none should be added to.packages/server/test/csp/csp.test.jscoversbuildCspHeaderandDEFAULT_DIRECTIVESbehaviour. The change tocsp.jsis comment-only, so that suite must stay green untouched, which is itself the check that no directive value moved.Verification
RULESfrompackages/server/src/check.jsand assert every rule-shaped backticked token surviving inblog/andwebsite/app/docs/is in it. Must come back empty. Do not count rules by eye or from CLI output; three rule names are long enough to change the column spacing, which is how a wrong count got published before.esbuild/light-dom-css-prefix/unsafe-inlinegreps and confirm the five legitimate mentions are intact.csp.jsis comment-only. Its diff must show no change to any directive string, andpackages/server/test/csp/csp.test.jsmust pass unedited.createRequestHandler(pass the PROJECT ROOT asappDir, sincebuildRouteTabledoesjoin(appDir, 'app')) and GET each edited post plus/blogand/blog/device-adaptive-link-prefetch, asserting 200 and no brokenmodulepreloadhints. Nothing pins post prose, so this render is the only thing standing between a broken fence and production, andwebsite/test/ssr/docs-links.test.tsvalidates/docs/...links only, so the new/blog/...cross-link is checked here or nowhere."webjs": { "csp": true }carrying a page-level inline<style>, astyle="..."attribute, a light-DOM component with a<style>, and a shadow component withstatic styles, and no Tailwind anywhere. Expect four<style>tags with exactly one nonce, on the framework's head rule. Keep the scratch app outside the repo withnode_modulessymlinked in.Doc surfaces
blog/ai-first-is-plumbing.md,blog/light-dom-by-default.md,blog/stop-ai-agents-breaking-your-code.md,blog/strip-types-not-esbuild.md,blog/client-router-turbo-drive-style.md.packages/server/src/csp.js.website/app/docs/conventions/page.ts:34describes the checker's narrowness without enumerating rules or printing a count, andwebsite/app/docs/troubleshooting/page.tsnames only real rules. Nolight-dom-css-prefixor stale-esbuild reference exists underwebsite/app/docs/. So no docs-site edit is owed, and none should be invented.AGENTS.mdand.agents/skills/webjs/need no change. Invariant 7 already states the tag prefix as a convention and invariant 10 already states there is no bundler fallback, which is what makes the blog posts wrong rather than the reference docs.Implementation notes (for the implementing agent)
Landmines.
.claude/hooks/block-prose-punctuation.sh. No em-dashes, no space-hyphen-space or space-semicolon-space as pause punctuation, andWebJscapitalized in prose except as a literal code token. The hook scans new content only, so an existing line can be edited freely.)</code>:, on a hyphenated<my-tag>:, and on a comment line shaped likefoo(): prose. A markdown backtick span holding a plain identifier matches none of them, soai-first-is-plumbing.mdkeeps its existing`rule-name`: descriptionlist style and the replacement bullet joins it in that form, rather than restyling five correct lines for no reason. The comment-line form DOES bind thecsp.jsedit, so no new comment line there may be shapedsomeFn(): prose.blog/strip-types-not-esbuild.md:50already carries aPR #9reference; leaving it is fine since it is existing content, but do not add more.webjs devorwebjs startinto user-facing prose.webjs checkandwebjs check --rulesare fine and already appear in both posts.light-dom-by-default.mdit stays a recommended default rather than a requirement.packages/server/src/**, which trips.claude/hooks/require-docs-with-src.shand.claude/hooks/require-tests-with-src.sh. The blog edits in the same commit should satisfy the doc gate;WEBJS_NO_DOC_GATE=1is the documented bypass for a comment-only change with no behaviour change.csp.jsis not on the Bun-parity hook's runtime-sensitive list, so notest/bun/**addition is needed for a comment.blog/andwebsite/app/docs/foresbuild,light-dom-css-prefix, andunsafe-inline. Five mentions are correct and must not be "fixed":website/app/docs/no-build/page.ts:156(publish-time bundling of the@webjsdev/coredist),blog/no-build-via-jspm-io.md(esbuild as removed history),blog/full-stack-type-safety-no-build.md:21andblog/fix-relative-import-paths.md:39(esbuild as a generic example of a build step), andblog/strict-csp-nonce-across-soft-navigation.md:40(unsafe-inlineinside a real header dump).Invariants to respect. AGENTS.md invariant 7 (the light-DOM tag prefix is a convention, not a check rule) is the fact steps 2 and 3 turn on. Invariant 10 (erasable TypeScript, no bundler fallback) is the fact step 4 turns on.
Commit shape. One logical unit, one commit,
docs:subject, in its own worktree cut fromorigin/main. Adocs:prefix keeps it out of the changelog, which is right for a prose correction.Acceptance criteria
blog/ai-first-is-plumbing.mdno longer names a rule that is absent fromRULES, states no rule count, and no longer claims a per-rule line countblog/light-dom-by-default.mdno longer promises alight-dom-css-prefixcheck ruleblog/stop-ai-agents-breaking-your-code.mdno longer nameslight-dom-css-prefix, and every rule it names is inRULESblog/strip-types-not-esbuild.mddescribes the real strip path (built-in stripper plus amaro, no bundler fallback) instead of an esbuild fallback, and its cache snippet names no identifier that does not existblog/client-router-turbo-drive-style.mdno longer lists prefetching or per-navigation full refetch as router non-goals, keeps the View Transitions non-goal, and links the prefetch postpackages/server/src/csp.jscomment explains thestyle-srcallowance by all four things that lack a nonce, names no styling library, and states that the framework's own head style IS nonce-signedpackages/server/test/csp/csp.test.jsstill passes with no edits to it, and thecsp.jsdiff touches no directive stringblog/is present inRULES, asserted by importing the module rather than by eyeblog/andwebsite/app/docs/confirms no other page states any old version, and the five legitimateesbuild/unsafe-inlinementions are still intact