Skip to content

Rspress docsite deep-dive audit: 7 P1s (reference drift, twoslash any hovers, search race, llmsUI 404s), SSG/TypeDoc levers for a 186 s build, 20 % sentence drift #590

Description

@ScriptedAlchemy

Third of the three toolchain audits. #566 (§4 docsite P3s) and #572 (docsite P2: Rspress performance.buildCache written every CI run) are the baseline; nothing they list is restated here. Same format as #572 / #576.

Preamble

Findings

P1 — user-visible breakage or a wrong statement on a reference page

P1 · docs/diagnostics.md:821 · rspress-docs-generator "a source-backed reference must render what the source says" · The AB4834 row has four cells (Code | Severity | Trigger | Recovery) under a three-column header (:785, Code | Severity | Trigger). GFM drops the extra cell, so the recovery hint (Add ".agent-bundle/routes.d.ts" to tsconfig.json include …) is silently absent from both generated reference/diagnostics pages and from GitHub. It is the only ragged row in 29 tables / 240 rows. Change: fold the recovery text into the Trigger cell, as the neighbouring AB48xx rows do. Risk: none.

P1 · docs/diagnostics.md vs website/docs/en/index.mdx:230, guide/distribution/validation.mdx:29,195-201, guide/authoring/mcp.mdx:735, reference/cli.mdx:99, index.mdx:175 · rspress-docs-generator "every user-facing item has a reference entry" · The home page promises "Every diagnostic has a stable AB code documented in the diagnostics reference", but four codes the guides explain with specific semantics have no row: AB4204, AB6005, AB8003, AB8004 (covered only by a one-line family catch-all). Breadth: packages/agent-bundle/src emits 400 distinct ABnnnn literals; docs/diagnostics.md names 213 explicitly (237 with ranges); 169 emitted codes have no explicit or ranged row and 93 of those only a family row — including all 69 AB80xx dev-server codes. Every code does fall under some family row, and the 6 documented codes with no src literal are legitimate (AB8206, AB8233–8235 live in the workbench/example; AB4807, AB4816 are marked retired). Change: add the four rows now; add a check (website check script or pnpm lint) that fails when an AB\d{4} cited in website/docs/**/*.mdx is absent from docs/diagnostics.md explicitly or in-range — it fails on exactly these four today. Risk: low; an "every src literal must be explicit" variant would fail on 169 codes and needs a ratchet.

P1 · website/docs/en/reference/cli.mdx:288 (zh :267) · AGENTS.md "reference prose must match the source" · "2 | An argument-parsing failure from the command-line parser." Five option validators throw a plain TypeError, not InvalidArgumentError: port, trialCount, installHost, installMode, installScope (src/cli.ts:217-246). Commander rethrows anything that is not commander.invalidArgument, and runCli maps only CommanderError to 2 — everything else is one AB5000 diagnostic on stderr and return 1 (src/cli.ts:1156-1161). Only --profile, --allow and doctor --host (cli.ts:251,260,268) exit 2. Change: "2 — a parser-level failure: unknown option, missing argument, or an invalid --profile, --allow, or doctor --host value. An invalid install <host>, --scope, --mode, --port, or --trials value is reported as one AB5000 diagnostic and exits 1." (Or make the five validators throw InvalidArgumentError; the mixed usage in cli.ts:217-268 looks accidental.) Risk: scripts branching on exit 2 for bad install arguments currently see 1 plus a JSON diagnostic.

P1 · website/docs/en/guide/authoring/hooks.mdx:132 (zh :120) · AGENTS.md "reference prose must match the source" · "outcome | continue, deny, or stop." src/adapters/hook-handler.ts:58 says "No event admits outcome: 'stop'", hookResultContract (:62-71) has no stop capability, and the generated wrappers accept the literal in the enum check (hook-contract.ts:895) and then fail it for every event (:902-905, "stop only accepts continue or deny with a reason" / "cannot stop the parent flow"). Change: "outcome | continue or deny. 'stop' is not a legal result on any event: HookHandler<E> excludes it and the wrapper rejects it at run time." Risk: a reader who returns { outcome: 'stop' } gets a tsc error and a hook-time failure.

P1 · cfg:156-167, website/tsconfig.typedoc.json:6-7, .github/workflows/docs.yml:54-61 · rspress-best-practices "validate before deployment" / AGENTS.md "state what the code does" · The twoslash compilerOptions.paths maps only agent-bundle, /config, /test, /eval; the TypeDoc tsconfig maps ten more (@agent-bundle/runtime*, rsc-markdown-stream) and its comment claims twoslash "mirrors" them — it does not. packages/agent-bundle/src/test/{render,matchers,target-capabilities}.ts and eval/assertions.ts import types from @agent-bundle/runtime, whose types is ./dist/index.d.ts; packages/rsc-runtime/dist does not exist in a fresh checkout and docs.yml runs pnpm install → pnpm lint → pnpm docs:site:build with no pnpm build. Twoslash only validates the sample's diagnostics, so the unresolved module becomes any in the published hovers: in the built HTML, 18 of 784 twoslash popups render : anyrenderRoute<…>(target: any, …), invokeMcpTool<…>(tool: any, …), RenderRouteOptionsBase.input?: any, ContractRouteFixture.input?: any — on guide/development/{testing,evaluations,workbench} in both locales (5 + 3 + 1 per locale). Change (minimal): add the ten aliases to the twoslash paths (absolute, like the existing four: '@agent-bundle/runtime': [path.join(repoRoot, 'packages/rsc-runtime/src/index.ts')], /plugin, /flight/server, /lineage, /mount, /notices, /notices/inbox-route, /state, /state/sqlite, 'rsc-markdown-stream'). Better: read tsconfig.typedoc.json once with ts.readConfigFile(file, ts.sys.readFile) (it is JSONC) and feed both consumers from one map — the "never copy a helper" rule. Risk: twoslash type-checks rsc-runtime/src too — slower fences, and TS-7-only syntax there fails under the pinned TS 6.0.3 (the failure mode cfg:122-126 already documents for TypeDoc).

P1 · cfg:79-81 (search) + website/plugins/mirror-api-locale.ts · rspress-best-practices "validate before deployment" · The first search query races the index build and shows "No matching results". The per-locale index is 1.71 MB raw / 338 KB gz (952 docs, of which 916 TypeDoc pages contribute 1.21 MB); it is prefetched, but FlexSearch construction takes 1.7–2.3 s after download, and a query typed ≤1.2 s after opening the panel returned nothing or a partial list in 7 of 8 trials (快速开始 0 → 3 on retype, 参与贡献 0 → 1, AB6005 1 → 3). Root cause is in the theme: LocalProvider.init (<core>/theme/components/Search/logic/providers/LocalProvider.js:81-85) calls addAsync for every document without awaiting, so SearchPanel.initSearch flips initStatus to 'inited' (SearchPanel.js:97-98) while indexing is still running, and NoSearchResult renders as soon as the list is empty and initStatus === 'inited' (:271) — no spinner, so the term reads as absent. tokenize: 'full' makes the cost proportional to content size, and this site's index is ~70 % API pages. Change: (a) file upstream (await Promise.all around the addAsync calls, or gate the pending query on completion); (b) site-side, shrink the window: a modifySearchIndexData hook (<shared>:815) in mirror-api-locale.ts that blanks content for /api/ pages (titles and headers stay indexed, so CapabilityStateError is still found by title) — or search: false frontmatter on the mirrored API pages (FrontMatterMeta.search, <shared>:90; honoured at createPageData.js:33) to drop them entirely. Risk: (b) trades API body-text search for a ~3× smaller index; it does not remove the race, only its window.

P1 · cfg:115 (llmsUI: { placement: 'outline' }) + cfg:170-191 (exclude: /api/) · rspress-best-practices "no broken assets" · "Copy Markdown" / "Open in ChatGPT" / "Open in Claude" are broken on all 1,832 /api/** pages. llmsUI renders the outline rows on every page (Outline/index.js:35-39) and the hidden LlmsHint advertises routePathToMdPath(page.routePath); both pluginLlms entries exclude /api/, and include/exclude apply to mdFiles as well as llms.txt (<llms>/index.js:142-148,236-244), so no .md exists for those routes. Live: https://scriptedalchemy.github.io/agent-bundle/api/classes/api.TargetRegistry.md404; the ChatGPT/Claude links embed that URL; getLlmsCopyContent.js:10 copies whatever response.text() returns with no status check (the 404 page — on preview, the home-page HTML). LlmsUI has no route gating (<shared>:292-315: injectLlmsHint, viewOptions, placement only). Change (cheapest, custom-theme level 3): in website/theme/index.tsx re-export LlmsCopyRow, LlmsOpenRow, LlmsHint wrappers that return null when usePageData().page.routePath includes /api/. Alternative that also closes #566 §4's llms-full.txt gap: a third pluginLlms entry { llmsTxt: false, llmsFullTxt: false, mdFiles: { mdxToMd: false }, include: ({ page }) => page.routePath.includes('/api/') } (Options allows false for both, <llms>/index.d.ts:42-50) — at the cost of 1,832 extra SSG-MD renders per build. Risk: none for the wrapper; build time for the alternative.

P2 — real quality / perf / maintainability gain with a concrete change

P2 · cfg:127-144 (pluginTypeDoc), website/docs/{en,zh}/api/_meta.json · rspress-docs-generator "shape the API surface for readers" / rsdoctor-analysis · @rspress/plugin-typedoc bootstraps TypeDoc with router: 'kind' (<typedoc>/index.js:41), which emits 916 pages per locale (13 modules, 151 functions, 21 classes, 499 interfaces, 186 type aliases, 45 variables). Consequences measured on the built site: every one of the 1,832 API routes renders a 921-item sidebar (the six dir groups carry no collapsed, and 2.0.21 defaults collapsed = false, SidebarGroup.js:25), items sort by filename module.Symbol while labels come from the h1, so the module grouping is invisible and two labels collide (Type Alias: AgentBundleConfig ×2, Variable: default ×2); each API HTML page averages 326 KB raw / 17.7 KB gz (authored: 54 KB) because the SSR'd <aside> embeds all 916 sidebar links — collapsed groups render their children with gridTemplateRows: '0fr', not conditionally — so 561 MB of the 602 MB of HTML (93 %) is sidebar, doc_build is 623 MB (the published Pages site sits at ~62 % of GitHub's documented 1 GB limit; the Pages artifact is 36 MB compressed per deploy); the entry chunk static/js/index.*.js (2.23 MB raw / 368 KB gz, loaded on every page) is O(pages): virtual-page-data 1,346 KB (219 KB gz — toc/frontmatter/title for all 1,906 routes) + virtual-routes 488 KB (73 KB gz) + virtual-site-data 287 KB (31 KB gz); 143 API pages have ≤2 body lines. The zh mirror (plugins/mirror-api-locale.ts) doubles all of it: 916 → 1,832 pages ≈ +300 MB HTML, ≈ +40 s single-thread SSG, +916 routes in every chunk above — while the mirrored body stays English. A/B (this audit, link checks disabled, otherwise identical): app.options.setValue('router', 'module') in the existing setup hook → 102 routes instead of 1,906, wall 90.3 s vs 186.3 s (−52 %), peak RSS 4.99 GB vs 8.52 GB (−41 %), doc_build 8.4 MB vs 623 MB, index.*.js 474 KB vs 2.23 MB; the largest module page (api/api.html) is 731 KB. Costs found by the A/B: 43 authored → API links per locale target member URLs (/api/classes/api.TargetRegistry) and must become module anchors; TypeDoc's slugger emits -1-suffixed anchors for duplicate names that Rspress's slugger does not, so checkAnchors fails on the generated pages until the titles are deduplicated; the tracked api/_meta.json (hand-maintained — the plugin only writes it when absent) must be flattened to the 13 module files. Change: (1) today, at zero risk: add "collapsed": true to the six dir entries in both _meta.json files (fixes the 921-item sidebar UX only — the HTML is unchanged); (2) then pick the lever — the module router as measured, an intermediate typedoc-plugin-markdown router, and/or dropping the zh mirror in favour of pointing zh API links at /api/ — and rewrite the 43 links; (3) either way, the requiredToBeDocumented warnings (~65/151 functions, ~250/499 interfaces, ~120/186 type aliases with no JSDoc prose) mean the thin pages stay thin until the source is documented. Risk: (1) none; (2) URL churn for any external links into /api/** (the sitemap advertises 1,832 of them today).

P2 · cfg (no ssg) · rsbuild-best-practices "measure the resolved config before tuning" · SSG is the largest build phase — 77–87 s of the ~186 s — because the default path (<core>/node/ssg/renderPages.js:85-91) renders all 1,906 routes in one thread (SSGConcurrency 32 is async, and rendering is CPU-bound). ssg.experimentalWorker: true (<shared>:1146-1157, default false) spawns a Tinypool of min(ceil(pages/100), cpus) threads (:46-50). Two quiet A/B runs: SSG 77.4 s → 8.7 s / 7.7 s, wall 186.3 s → 101.1 s / 110.8 s (−46 % / −41 %), peak RSS 8.52 → 9.82 / 10.35 GB; output identical (1,907 HTML files both, 7/7 sampled pages byte-identical after content-hash normalisation, HTML total 602,258,970 vs 602,258,375 B). Together with the TypeDoc surface above this is the whole "why does the docsite take three minutes" answer (#572 measured the persistent-cache lever at ~4 %; the warm run here was −3 %). Change: ssg: { experimentalWorker: true }, measured once on the hosted runner before committing to it — ubuntu-latest has 4 vCPU → 4 threads → projected SSG ≈ 20–25 s and wall ≈ −55–65 s, not −85 s. Risk: experimental API; worker-written HTML bypasses Rsbuild's asset table (the file-size table no longer lists HTML — cosmetic); +1.3–1.8 GB RSS to verify on the runner.

P2 · cfg:116-118 (editLink) · rspress-custom-theme "override at the lowest level that works" · useEditLink builds docRepoBaseUrl + page._relativePath unconditionally (<core>/theme/components/EditLink/useEditLink.js:10-11) and the link renders in every outline. en/api/** (916), zh/api/** (916) and the 8 */reference/{hosts,events,notices,diagnostics}.md pages are gitignored (.gitignore:19-25), so 1,840 pages carry an "Edit this page / 编辑此页面" link to a GitHub 404 (verified: …/tree/main/website/docs/en/reference/hosts.md, …/en/api/index.md). 2.0.21 has no per-page switch: EditLink is { docRepoBaseUrl } only (<shared>:31-37) and FrontMatterMeta has no editLink key. Change: in website/theme/index.tsx, wrap EditLink from @rspress/core/theme-original: return null when page._relativePath matches ^(en|zh)/(api/|reference/(hosts|events|notices|diagnostics)\.md$), or — better for the reference pages — point them at their real sources (docs/diagnostics.md, packages/agent-bundle/src/adapters/capabilities/*.json) with a small map. Risk: the regex must track .gitignore.

P2 · cfg:86 (markdown), AGENTS.md "code samples wrap at roughly 90 columns" · rspress-best-practices · Measured at 1440×900 with the outline present (every authored page has one): the code-block container is 690 px, the pre 686 px, 14 px monospace at 8.4 px/char → ~81 characters fit, so the 90-column rule is ~10 columns too wide. 66 of 201 visible code blocks on en authored pages (33 %, 18 of 33 pages) need a horizontal scrollbar (39 % across both locales, 132/336 blocks on 39 pages); every block is white-space: pre with the wrap toggle off by default. Worst: /reference/cli (158-char line, +682 px, 3 of 4 blocks), /guide/authoring/package-entries (139 chars, +530 px, 9/30), /guide/authoring/mcp (124 chars, +396 px, 17/38), /guide/distribution/installation (124, +396), /guide/authoring/hooks (114, +312, 5/9), /guide/development/testing (114, +312, 4/7), /reference/diagnostics (104, +228, 2/2). At the source level 26 fences exceed 90 columns and 11 exceed 100 (reference/cli.mdx:153 = 158; guide/distribution/installation.mdx:165, guide/authoring/mcp.mdx:155 = 124). Change: either markdown: { defaultWrapCode: true } (<shared>:383-387; the copy button still copies raw text) or correct AGENTS.md to "≤ 80 columns" and re-flow the 26 fences above 90 first. Risk: wrapped shell one-liners are less scannable; none for correctness.

P2 · cfg:110-113 (languageParity), website/package.json:11 (check) · AGENTS.md "en page and its zh translation in the same PR" · checkLanguageParity compares only the set of .md/.mdx relative paths across config.locales (checkLanguageParity.js:16-19,36-38,62-67); _meta.json, _nav.json, frontmatter, headings and code are not compared, and it runs in finally after the whole build, so a miss costs a full build. Today nothing is stale (all 33 pairs share the same last commit and commit count; h2/h3, table, link, AB-code and ::: counts match; fences are byte-identical after stripping comments — the only diffs are translated trailing #/// comments in 16 fences across 10 pages), but no gate keeps it that way. Change (minimal): website/scripts/check-locale-drift.mjs, run by check before rspress build, failing when for any en/Xzh/X the fence count differs, fence bodies differ after stripping comment lines and trailing comments, the AB\d{4} sets differ, table-row counts differ, or h2/h3 counts differ by >2; plus a PR-level complement that fails when git diff --name-only origin/main...HEAD touches en/X.mdx without zh/X.mdx. Risk: false positives on a deliberately zh-specific example (none exist today) or translated string literals inside code (all kept English today).

P2 · AGENTS.md:116-117 ("pnpm docs:site:build is the gate"), cfg:98-106, website/package.json:11 · rspress-best-practices "verify broken links before deployment" · The gate does not check the link forms the site itself relies on. <core>/node/mdx/remarkPlugins/link.js:162-174 visits only mdast link/definition nodes and image.js:77,100-105 sees markdown images plus <img> only as mdxJsx nodes (i.e. in .mdx); nothing inspects frontmatter, _nav.json, or raw HTML/JSX in .md. A probe docs root with a dead hero.actions[].link, dead features[].link, missing hero.image, <a href="/dead"> in .md and .mdx, <Link href="/dead">, a dead _nav.json entry, an en page with no zh twin under a directory named capi/ (the parity exclude is a substring match on api/), and a zh page with unrelated content built with exit 0 — while the same probe root confirmed that [x](/abs), [x](./rel), [x](/x.md), definitions, #anchors (self, relative, absolute, cross-locale), markdown images, <img> in .mdx, _meta.json entries and a missing zh file all fail the build as they should. The live docs carry 28 frontmatter links (docs/{en,zh}/index.mdx:16-65) and 10 _nav.json links in exactly the unchecked forms. Second miss: with the persistent Rspack cache that a local build writes to website/node_modules/.cache/rspack/ by default, a dead anchor added to page A that targets unchanged page B passes (B's loader never runs, so the RouteService.onRouteAnchorIds consumer, route/RouteService.js:139-148, never fires) — reproduced: warm exit 0, same content cold exit 1. CI is safe only because the cache is not persisted between runs; a local pnpm docs:site:build re-run is not. Change: a ~50-line post-build script in website/ (run by check after rspress build) that walks doc_build/**/*.html, resolves every href/src under /agent-bundle/ to a file and every #id to an id= in the target — covering frontmatter, nav, sidebar, JSX, generated pages and the cache case uniformly (+2–3 s); plus RSPRESS_PERSISTENT_CACHE=false in the check script itself (extends #572's docs.yml-only proposal; the warm gain is 3 %). The locale-drift check above is the parity half of the same gap. Risk: low.

P2 · website/docs/en/reference/configuration.mdx:24,51-53 (zh :24,50) · AGENTS.md "reference prose must match the source" · (a) "targets | string[] | Adapter-selected." — exactly one adapter is registered as default (.register(portableAdapter, { default: true }), src/adapters/registry.ts:780-786) and resolveTargets returns registry.defaultTargetNames() when config omits targets and no --target was passed (src/config/normalize.ts:1114-1122); no page says so. (b) "defineConfig accepts an AgentBundleConfig … and validateSource enforces the rules on this page" while the table lists routes and evalsAgentBundleConfig (src/core/types.ts:309-328) declares neither; both ride the [key: string]: unknown index signature (types.ts:328, src/routes/graph.ts:336), config/validate.ts never reads evals, and the evals rules at :145-147 are enforced by normalizeEvalConfig (src/eval/config.ts:82-125) only when an eval or the Workbench loads the config, as EVAL_CONFIG_INVALID, not an AB diagnostic. Sibling: :38 marketplace "Adapter-selected." → false (only marketplace === true is carried, normalize.ts:1301). Change: (a) "['portable'] (the only default adapter); --target overrides both." (b) add after :53: "evals and routes are not members of AgentBundleConfig; they are read through its index signature, so tsc does not check them and agent-bundle validate does not report them — the evals rules below fire when an eval runs (EVAL_CONFIG_INVALID)." Risk: a user omitting targets expects host artifacts and gets only artifact/portable; a misspelled top-level key is accepted by both tsc and validateSource.

P2 · website/docs/en/guide/authoring/hooks.mdx:146 (zh :133) · AGENTS.md "reference prose must match the source" · "agentStop … on Codex it cannot add context." The Cursor wrapper also rejects it (hook-contract.ts:906, "Cursor … has no additional-context channel") and the typed contract sets agentStop: { additionalContext: false } for every host (hook-handler.ts:66-67: "Claude carries additionalContext on SubagentStop; Codex and Cursor reject it"). Change: "…and only Claude Code carries additionalContext from it — Codex and Cursor reject it, and the typed contract excludes it everywhere." Risk: none.

P3 — polish

  • cfg (no head), website/docs/public/ · rspress-best-practices "first-class options first" · SSG emits og:title/og:description and en/zh hreflang alternates on 114/114 pages but no rel=canonical, og:url, og:image, og:type, twitter:card, theme-color or x-default; 25 pages have a meta description >160 chars (max 842 on api/type-aliases/*, inherited from TypeDoc bodies); /api/ is titled "agent-bundle - agent-bundle". UserConfig.head (<shared>:1079) accepts per-route functions evaluated at SSG, so canonical + og:url are one entry (withBase/withSiteOrigin/normalizeHref are exported from @rspress/core). Live /agent-bundle/robots.txt404 and plugin-sitemap emits none; sitemap.xml (1,906 <loc>) is therefore unadvertised — add website/docs/public/robots.txt with Sitemap: https://scriptedalchemy.github.io/agent-bundle/sitemap.xml.
  • cfg:83 (cleanUrls: true) · deployment · leaf pages are emitted as x.html, so /agent-bundle/guide/start/quick-start and .html are 200 on Pages while the trailing-slash form …/quick-start/ is 404 on Pages (200 under rspress preview, whose SPA fallback masks it). The site never emits that form (sitemap, hreflang and nav are extensionless), so this only bites shared links; the cheap fix is a redirect snippet in the 404 page (head script: strip a trailing slash when location.pathname is not a directory route).
  • rspress preview · acceptance workflow · the rsbuild static server serves index.html for every extensionless dotted route (/api/classes/api.TargetRegistry), producing React feat(routes): reference MCP App resourceUri from static config and resolve App templates route-relative (#388) #418 hydration errors and a content flash on 30/30 sampled API pages in preview only; the same pages hydrate clean as .html and on Pages. Any browser acceptance against preview therefore shows fake errors and hides the trailing-slash 404 above — preview doc_build with a plain static server (npx serve doc_build) or document it.
  • website/plugins/mirror-api-locale.ts · i18n · 916 zh mirror pages carry <html lang="zh"> and Chinese chrome (目录 / 上一页 / 下一页) around an English body with no notice that the API reference is English-only, and inherit the English site description. Prepend an :::info note in the mirror plugin or set lang/description frontmatter there.
  • cfg:137 · rspress-best-practices · pageTitleTemplates.index: '{projectName} {version}' renders # agent-bundle {version} is empty because includeVersion is not set; drop it or app.options.setValue('includeVersion', true). website/docs/en/_nav.json:22-26: "Type API" lands on the bare generated index (13 bullets named by entry file — index, routes/public, test/browser — no prose, 0/916 API pages have a description); the mapping to agent-bundle, agent-bundle/routes, … exists only in reference/api.mdx — point the nav there or add a "Module page" column to reference/api.mdx:12-29.
  • website/plugins/generated-reference.ts:18-36 · AGENTS.md "look for the helper before writing it" · re-declares loose CapabilityRow/HostCapabilityTable shapes while src/adapters/capability-state.ts:47,94,99 owns EventRouteCapabilityTableEntry, CapabilityTableRow, NoticeDeliveryCapabilityTableEntry; the package does not export them, so a schema change renders stale columns silently. Export them from a public entry and import type in the plugin (changeset + api/** regeneration).
  • build hygiene · (a) the entry chunk hash is non-deterministic: two builds of the same tree produced index.88eed14fa7.js and index.5960409c3a.js, same length 2,227,901 B, first difference at byte 1,837,065 = sidebar key order in virtual-site-data ("/reference" first vs "/contributing" first) — every deploy invalidates the 368 KB gz entry for every returning visitor; upstream @rspress/core auto-nav-sidebar, no repo change. (b) sitemap.xml lastmod = source-file mtime (<sitemap>/index.js:51): 66 URLs carry the checkout time and 1,836 the build time, so every deploy tells crawlers every page changed. (c) Rsbuild's default performance.printFileSize prints a 3,900-line size table per CI log, 4.6–8.9 s after ready built; builderConfig: { performance: { printFileSize: { detail: false } } } removes the noise (rsbuild-best-practices; the seconds are not fully attributed — treat as log hygiene).
  • cfg:92-96, cfg:79-81 · rspress-best-practices "remove unused plugins" · the docs contain 2 [!code highlight] markers and zero [!code ++/--]/[!code focus], so transformerNotationDiff() and transformerNotationFocus() run on every fence for nothing; search.codeBlocks: true restates the default (<shared>:356-359). themeConfig.footer is unset (repo is Apache-2.0).
  • 13 en pages · rspress-description-generator "50–160 chars" · 13/33 en descriptions are 163–196 chars (examples/audiobook-curator 196, reference/limitations 193, examples/mcp-app 194, reference/runtime-environment 179, reference/index 176, guide/distribution/installation 174, reference/security 173, examples/hooks-and-scripts 172, reference/cli 170, guide/development/evaluations 169, guide/development/testing 168, reference/configuration 164, guide/development/workbench 163); all zh ≤111; 0 missing, 0 wrong language, 0 "This page…" openers.
  • shared snippets · the minimal defineConfig fence is byte-identical in 4 en pages (guide/authoring/index.mdx:12, guide/start/project-structure.mdx:69, guide/start/quick-start.mdx:68, reference/configuration.mdx:10) and four paragraphs are duplicated verbatim across pages (reference/security.mdx:19guide/development/evaluations.mdx:196; reference/limitations.mdx:50-51guide/development/testing.mdx:350-351; guide/distribution/installation.mdx:7guide/start/quick-start.mdx:107; guide/distribution/validation.mdx:179guide/authoring/package-entries.mdx:223) — MDX fragment candidates.
  • hooks.mdx:139 · "sessionStart, afterTool, and agentStart cannot deny" is true for HookHandler<E> and the Claude/Codex wrapper, but the Cursor wrapper admits a denying agentStart with a reason (hook-contract.ts:899-901, hook-handler.ts:16) — "the typed contract rejects it; Cursor's runtime tolerates one".
  • theme a11y / contrast (upstream defaults; styles/index.css to override) · appearance switch is a div.rp-switch-appearance with no role/name and not focusable; the GitHub social link has no accessible name; no skip link; home page has no <main>/<h1>; prev/next 12 px labels 1.85:1 light / 3.19:1 dark; active-nav brand #0d8f80 on white 3.99:1; Shiki light string token #31a94d 3.04:1 (body/table/code base ≥11:1 in both modes). Override --rp-c-text-3 and the brand token; add aria-label via themeConfig.socialLinks.
  • .github/workflows/docs.yml:44 · name: Docs site (Node 22.19) is a required context on main (required_status_checks also names Host install proofs (Node 22.19), Examples check (Node 22.19), Release gates (Node 22.19)); bumping Node renames the check and blocks every PR until protection is edited — use a stable name: Docs site. package.json:24 root typecheck covers root/workbench/create-agent-bundle/rsc-markdown-stream but not website/tsconfig.json (config, plugins/**, theme/**, 1,307 lines), which only docs:site:build compiles — so AGENTS.md's "gate before pushing" never typechecks the website locally or in ci.yml Verify; append pnpm --filter @agent-bundle/docs typecheck. website/package.json:24,28: @types/react-dom and agent-bundle: workspace:* have no importer (twoslash resolves agent-bundle* via paths, TypeDoc via tsconfig.typedoc.json); react/react-dom are also unreferenced by website code but @rspress/core aliases to <cwd>/node_modules/react if present, else its own copy — remove both or neither. rstest.integration-tests.ts:8: workspaceTestFileGlob = 'packages/**/tests/**/*.test.ts' means any website/**/*.test.ts would never be collected, and scripts/classify-docs-only.mjs:20 skips Verify for website/**-only PRs — zero tests exist for the ~900-line generated-reference.ts today, so nothing is silently skipped yet; when the first test lands, add website/**/*.test.ts to rstest.unit.config.ts and run it in docs.yml. Actions are major-tag pinned, none by SHA (dependency-review-action runs; Dependabot updates SHA pins). On main, .github/dependabot.yml:7-8: since ci: split Verify into a fast leg + 2 integration shards behind the required gate; pin the browser; retry setup; nightly pools (#576) #583 pnpm/setup@v2 lives only in the composite .github/actions/setup-workspace/action.yml, which the github-actions ecosystem with directory: / does not scan — use directories: ["/", "/.github/actions/*"].

Measurements

(a) Builds (/usr/bin/time -v, website/, Node 22.23, this machine; "quiet" = the sampler saw no other build during the run)

run wall peak RSS CPU notes
cold #1 (doc_build + caches removed) 190.2 s 8.45 GB 173 % overlapped lane 4's build
cold #2, quiet 186.3 s 8.52 GB 184 % baseline
warm (doc_build, node_modules/.cache kept), quiet 184.5 s 8.34 GB 188 % Rspack cache populated (90 MB): −3 % (#572)
A/B twoslash removed 159.9 s 8.09 GB 170 % 12/16 samples saw a foreign build; compile −37.6 s is robust, wall is not
A/B ssg.experimentalWorker: true, quiet 110.8 s 10.3 GB 321 % second run under load: 101.1 s / 9.8 GB
A/B TypeDoc router: 'module' (link checks off) 90.3 s 4.99 GB 201 % 102 routes; doc_build 8.4 MB
Rsdoctor-instrumented build 236.1 s 8.14 GB 174 %
final real-config build (3 foreign builds running) 224.3 s 8.47 GB not a timing sample
tsc -p website/tsconfig.json ×2 2.6 s / 2.5 s 0.3 GB 5 project files + 814 .d.ts; no MDX

#572's 196.6 s / 188.8 s cross-check to 190.2 / 184.5 s here. Phase breakdown from log timestamps (s):

phase cold #2 warm worker (×2) no twoslash
TypeDoc (→ markdown generated) 20.6 17.5 17.1 / 21.4 20.6
plugin + route init (→ build started) 12.5 7.4 7.1 / 9.9 8.6
Rspack web + node compile incl. MDX/twoslash (→ Rendering pages) 67.3 61.9 65.4 / 69.0 29.7
SSG (Pages rendered in) 77.4 87.2 8.7 / 7.7 90.7
emit + size table 6.1 8.5 0.8 / 0.5 7.1
parity + sitemap + llms <0.1 <0.1 <0.1 <0.1

Twoslash is worth its cost: 64 opted-in fences (32 per locale; explicitTrigger defaults to true, so the other 120 ts/tsx/js fences are not type-checked), 26 output pages carry hovers (guide/authoring/mcp 51), compile −37.6 s without it (~20 % of wall). Shiki and twoslash ship no client JS (@shikijs/langs 8.6 MB source, parsedSize 0, tree-shaken); hover HTML is inlined in the route chunks (mcp.mdx route 287 KB / 30 KB gz).

Client JS per page (raw / gz): index.*.js 2,227,901 / 368,666 · search chunk (flexsearch) 226,677 / 74,372, a blocking <script> on every page · lib-react 189,213 / 59,843 · lib-router 35,321 / 12,714 · styles 79,775 / 14,677 CSS · route chunk 4.8–29.5 KB / 1–4.6 KB → ≈ 2.70 MB raw / 520 KB gz per page, identical on home, guide and API pages, plus the 1.71 MB / 338 KB gz search index fetched at requestIdleCallback on every page (SearchPanel.js:177-182), not on focus. Rsdoctor 1.6.3: no E1001/E1002 duplicates; top retained modules by gz are .rspress/virtual-page-data.js 1,345.6 KB / 218.9 KB, virtual-routes.js 487.7 / 73.2, react-dom 174.1 / 54.9, the reference/diagnostics.md routes 257 / 53 each, guide/authoring/mcp.mdx routes 268–287 / 30–34, virtual-site-data.js 286.8 / 30.8 — i.e. the bundle is dominated by the repo's own route count, not by dependencies. Rspack cache after a build: 90 MB in website/node_modules/.cache/rspack/.

(b) Outputdoc_build 623,206,738 B apparent (98 MB on a compressing filesystem), 3,909 files: .html 1,907 = 602.3 MB (sidebar 561.2 MB; API pages avg 326 KB, largest zh/api/modules/api.html 385.6 KB; authored avg 54 KB); .js 1,916 (1,911 async/route-*) = 14.8 MB; search index 2 × 1.71 MB; llms .txt 4 = 1.15 MB (llms-full.txt 565 KB per locale; llms.txt 9.2 KB, 36 URLs, all .md targets 200); llmsUI .md mirrors 74 = 1.12 MB; sitemap.xml 397 KB (1,906 <loc>, 953 /zh/, 1,832 /api/, 0 llms URLs); .css 80 KB; 404.html 11 KB; 0 .map, 3 .LICENSE.txt. Pages artifact 36 MB compressed (last 5 deploys 35.9–37.1 MB). Module-router variant: 8.4 MB total, static/js 2.9 MB, index.*.js 474 KB, largest page api/api.html 731 KB.

(c) Runtime (preview, 1440×900, 114 pages) — HTTP 114/114 → 200; console errors/warnings, pageerror, requestfailed, ≥400 responses: 0 on all 74 authored pages; hydration warnings 0 authored, 30/30 API at extensionless URL under preview only (0/30 via .html); <html lang> en 57 / zh 57 correct; meta description present 114/114, >160 chars 25; canonical 0 / hreflang 114; og:type,title,description 114, og:url,image/twitter:* 0; LCP avg 155 ms / p50 148 / max 444 ms, CLS 0.000 on 114/114; DCL avg 296 / max 653 ms; 14–15 requests per page; cold JS transfer 2.68 MB raw ≈ 520 KB gz + 338 KB gz search index; CDP heap 10.6 / 12.9 / 18.4 / 12.6 MB and DOM nodes 1,148 / 1,977 / 10,719 / 3,182 on home / guide / diagnostics / API; rspress dev (4 pages) 0 errors, first load 4.0–5.6 s. Tables: 278/278 wrapped in div.rp-table-scroll-container; 4 wider than their container (hosts "Install surface" +33 px, events "Canonical event routes" +36 px, ×2 locales) → they scroll; page-level scrollWidth > 1440: 0/114; <wbr> from rehypeTableCellBreaks present (24–48 per hosts table). Search matrix (steady state, 170–250 ms to first result): AB6005 en 3 hits (Artifact validation, MCP servers and MCP Apps), zh 3; AB6011 3 (diagnostics reference); AB4770 0 (correct, does not exist); hooks 61; defineConfig 16; readPluginRoot (fence-only) 1 — codeBlocks works; CapabilityStateError, DevRuntimeMcpAppRunBinding (API-only) found; 脚手架 6, 宿主能力 8 — zh indexed separately (952 docs each); --target 0 (leading punctuation stripped; target 119). Language switcher: same slug both directions for every page type (/reference/hosts/zh/reference/hosts, /api/classes/api.TargetRegistry ↔ zh mirror — English body), 404 → locale home by design. Dark mode toggles html.dark, persists via localStorage['rspress-theme-appearance']. Diagnostics anchors are heading ids: #ab6005 lands, #AB6005 does not.

(d) Live Pages matrix (curl)/guide/start/quick-start 200 · .html 200 · …/quick-start/ 404 · /guide/start 301 → /guide/start/ 200 · /robots.txt 404 · /api/classes/api.TargetRegistry.md 404 (.html 200) · /guide/start/quick-start.md 200 · /sitemap.xml 200 · /404.html 200 with correct /agent-bundle/ asset and home paths.

(e) Accuracy sample — 25 reference sentences vs packages/agent-bundle/src: 5 / 25 = 20 % drift (1 DRIFT, 4 PARTIAL)

# page:line claim source verdict
1 reference/cli.mdx:288 exit 2 = any argument-parsing failure cli.ts:217-246, 1156-1161 PARTIAL (five validators → AB5000, exit 1)
2 reference/cli.mdx:231 --trials positive integer ≤ 100 cli.ts:225-229 TRUE
3 reference/cli.mdx:138 --scope default user; Codex/Cursor user-scoped cli.ts:884; install/install.ts:552,898 TRUE
4 reference/cli.mdx:139 --mode local/marketplace paths (Cursor only) install.ts:1026-1031,909-910; cursor-marketplace.ts:51 TRUE
5 reference/cli.mdx:286 exit 0 incl. --help/--version cli.ts:726,1157-1158 TRUE
6 guide/start/quick-start.mdx:27-28 --template mcp-server --targets … create-agent-bundle/src/options.ts:41-42,113-128 TRUE
7 reference/configuration.mdx:24 targets default "Adapter-selected" registry.ts:780-786; normalize.ts:1114-1122 PARTIAL (['portable'])
8 reference/configuration.mdx:34 runtime.node floor 22.12 core/runtime.ts:4 TRUE
9 reference/configuration.mdx:37 notices retention 7d / 500 / 16777216 config/notice-retention.ts:21-23 TRUE
10 reference/configuration.mdx:145-147 evals include / runsDir / grader / harness defaults eval/config.ts:30-31,62-79,104-107 TRUE
11 reference/configuration.mdx:51-53 "validateSource enforces the rules on this page" (incl. evals, routes) types.ts:309-328; graph.ts:336; validate.ts PARTIAL (untyped, unchecked)
12 guide/start/installation.mdx:12-13 runtime.node raises, never lowers, the floor (AB4602) runtime.ts:4; validate.ts:888-893 TRUE
13 reference/targets-artifacts.mdx:19-23 install surface per target (#555 would change plugin) install/surface.ts:1393-1398 TRUE
14 reference/targets-artifacts.mdx:21 marketplace manifest only when marketplace: true cursor.ts:107; normalize.ts:1301 TRUE
15 guide/start/installation.mdx:40-41,60-62 @agent-bundle/runtime, @rstest/core, react optional peers package.json peerDependenciesMeta TRUE
16 guide/start/installation.mdx:9 Node 22.19+ to run the compiler engines.node >=22.19.0 TRUE
17 guide/authoring/skills.mdx:48 no frontmatter → AB3001; invalid YAML → AB3002 config/skill.ts:106,113 TRUE
18 guide/authoring/skills.mdx:86-87 list targets: in Skill frontmatter → AB3006 skills/parse-ir.ts:264-269 TRUE
19 index.mdx:174-175 portable skipped silently; explicit → AB4204 normalize.ts:526-528; validate.ts:194-199 TRUE
20 reference/cli.mdx:99 AB8003/AB8004 on refusal dev/foreground-server.ts:808-827 TRUE (family-level doc only)
21 guide/authoring/hooks.mdx:132 outcomecontinue, deny, stop hook-handler.ts:58-71; hook-contract.ts:895-905 DRIFT
22 guide/authoring/hooks.mdx:146 agentStop cannot add context "on Codex" hook-contract.ts:906; hook-handler.ts:66-67 PARTIAL (Cursor too)
23 guide/authoring/hooks.mdx:55 timeout omitted → host default normalize.ts:478-487 TRUE
24 guide/authoring/hooks.mdx:77-78 Cursor maps all five matchers; Claude no agent; Codex no agent/file.read capabilities/*.json hooks.matchers TRUE
25 reference/runtime-environment.mdx:42 AGENT_BUNDLE_ENV_FILE delimiter / later wins / none launch-env.ts:28-33,108 TRUE

Sweeps behind the sample: AB codes — 101 distinct on en pages, 0 without an emitter, 0 outside every family row, 4 named only at family level (the P1); CLI flags — 45 parsed, documented-not-parsed 0, parsed-not-documented 0 (after excluding 23 foreign-tool flags that are real in their own parsers); config keys — 19 + 4 host extensions documented, 17 + 4 typed, documented-but-untyped routes, evals, known-but-undocumented 0; unknown top-level keys are rejected nowhere (index signature) and no page claims otherwise.

(f) Dependency / CI currency — see preamble for versions. docs.yml last 80 runs: 62 success, 8 cancelled (PR supersession), 8 action_required (all changeset-release/main bot PRs — repo Actions-approval setting, ci.yml shows the same), 0 failures; build-job median 166 s (n = 9, 130–193 s), deploy 16 s, timeout-minutes: 25 / 10 ample; head-of-main guard and deploy-pages succeeded on every completed push run. pnpm audit: 2 moderate (qs@6.15.3), 0 of 28 paths through @rspress/typedoc/shiki/mdx; 0 open Dependabot alerts. Gate parity: rslint covers website/** in Verify, docs.yml and locally; tsc -p website/tsconfig.json, the TypeDoc + twoslash compile of the package sources under TS 6.0.3, and the Rspress dead-link/anchor/image/parity checks run only in docs.yml; website/** unit tests run nowhere (none exist).

Already compliant

  • Config surface: siteOrigin is a documented UserConfig key (<shared>:1052-1055) consumed by the sitemap fallback, hreflang, llms.txt URLs and absolute-link normalisation; base: '/agent-bundle/' + default outDir match docs.yml:66 and the Pages html_url; lang: 'en' + route.localeRedirect: 'never' emit no redirect script, en at /, zh at /zh/; route.cleanUrls: true (extensionless hrefs, hreflang, sitemap loc and address bar all agree); zh UI strings resolve through DEFAULT_I18N_TEXT (目录, 上一页/下一页, 搜索, 编辑此页面, 复制 Markdown, 切换代码换行) so no themeConfig.locales block is needed; local search builds one index per locale; markdown.shiki lazy: true covers every fence language in use (sh 118, ts 100, text 20, tsx 16, md 2, json 2) and the extra langs only pre-warm grammars for twoslash hovers as the comment says; link.checkDeadLinks: { excludes }, checkAnchors: true, image.checkDeadImages: true match RemarkLinkOptions/RemarkImageOptions; plugin order (TypeDoc configsetupconvertwriteOutput, then mirrorApiLocale, then generatedReference) is sequential with no race in dev; theme/index.tsx uses the first-class afterFeatures slot and re-exports theme-original with no cycle; styles/index.css uses only existing --rp-* tokens and BEM classes (no dead selectors); icon/logo/hero.image are base-prefixed; no deprecated keys (themeConfig.localeRedirect, addRuntimeModules) and no v1 imports; website/tsconfig.json scope is correct; socialLinks.mode: 'link' valid.
  • Content architecture: nav parity (5 entries, same order, zh activeMatch carries /zh/); 10 _meta.json per locale, identical order, translated labels, dir-section-header for the four Guide groups; a missing sidebar target throws at build ("Missing page file"), so the coupling to generated pages is fail-loud; exactly one h1 per page (66/66), 0 heading skips, titles unique per locale; all frontmatter keys valid; 0 orphans (33/33 pages reachable from nav + sidebar per locale; 953 generated pages reachable); no page under 120 words; only the two deliberate cross-locale links (reference/api.mdx); all 13 blob/main/tree/main GitHub targets exist; generated reference plugin fails loudly on malformed input, translates zh headings/prose/info boxes, escapes | < > { }, keeps all 240 diagnostics rows; TypeDoc surface: 916 pages, zh mirror 916/916 byte-identical, 3,149 internal links with 0 broken, 43 authored → API links per locale all resolve, <>{} escaped, \_ absent from titles, no Defined in lines to rot.
  • Runtime: zero console errors/warnings/failed requests on all authored pages; CLS 0 everywhere; every table in a scroll container and no page-level horizontal overflow at 1440; base handled in assets, favicon, 404.html, sitemap, llms, home CTAs, hreflang; language switcher lands on the same slug in both directions; dark mode persists; body/table/code base contrast ≥11:1 both modes; search finds fence-only identifiers, API symbols and CJK phrases; all images have alt; rspress dev loads clean.
  • Gate: dead link/anchor/image checks are build errors, not warnings, in production (link.js:35,74, image.js:33); relative, absolute, .md-suffixed, definition, self/relative/absolute/cross-locale anchor, markdown-image, .mdx <img>, _meta.json and missing-zh-file probes all fail the build as documented; generated TypeDoc and reference pages are written in config hooks before RouteService.create, so anchors into them resolve (cold); checkDeadLinks.excludes correctly whitelists the llms(-full).txt assets; languageParity.exclude: ['api'] has no accidental substring victim in the real tree; explicitTrigger: true keeps twoslash to opted-in fences; Rsdoctor E1001/E1002 clean.
  • Deps / CI: all @rspress/* at latest; no legacy rspress package; Node engines consistent across root, website, Rspress, Rsbuild, typedoc, shiki; concurrency groups correct (docs-main non-cancelling, docs-<n> cancelling for PRs, pages group on deploy); permissions minimal; Pages build_type: workflow, https_enforced, branch policy main only; Dependabot covers the pnpm workspace sub-manifests and workflows; .gitignore covers doc_build/, api/**/*.md, the four generated reference pages.

Caveats

  • Builds ran on a shared 20-thread machine; the sampler flagged foreign builds during cold feat: add RSC plugin framework and audiobook curator example #1 (overlapping lane 4's build) and the no-twoslash A/B (12/16 samples), so those two are upper bounds — the no-twoslash compile-phase delta is measured in-process and robust, its wall figure is not. Single runs, not medians (cold repeat variance 190.2 vs 186.3 s). The 4-vCPU CI projection for experimentalWorker is arithmetic, not measured, and its peak RSS (10.3 GB here) must be re-measured on the hosted runner before adoption.
  • The TypeDoc router: 'module' A/B disabled checkDeadLinks/checkAnchors (the variant fails them for the reasons listed) and flattened api/_meta.json in the scratch worktree only; its numbers are therefore a lower bound on the work, not a drop-in config.
  • Runtime numbers come from rspress preview on localhost (LCP/timing not network-realistic); production behaviour was cross-checked with curl only, not a browser run against Pages. The API sample was 40 of 1,832 pages; all authored pages were crawled. a11y checks are hand-rolled (axe-core is not installed).
  • Search index-build timing was measured once per fresh browser context, 8 trials; the root-cause reading of LocalProvider.init is from the shipped dist, not a debugger.
  • zh "word counts" are CJK character counts; JSDoc-coverage figures are a text heuristic; description lengths are code points.
  • The _meta.json/sidebar collapse behaviour and hook ordering were read from @rspress/core@2.0.21 source and then confirmed on the rendered site (921-item sidebar observed).
  • main moved during the audit; the docs.yml/dependabot.yml P3s were re-checked against main, everything else against b75073b24.

Lanes

  1. Config surface — every cfg key vs <shared>/<core> typings and runtime; produced the twoslash-any P1, the llmsUI and EditLink findings, the head/robots P3s, and the "already compliant" config list.
  2. Content architecture — 33 pages/locale, 10 _meta.json/_nav.json per locale, 916 API pages/locale; produced both diagnostics.md P1s, the sidebar/collapsed and locale-drift P2s, descriptions/snippets/thin-page P3s, orphan/heading/frontmatter/link sweeps.
  3. Build measurement — cold ×2 / warm builds, phase breakdown, Rsdoctor, the experimentalWorker and no-twoslash A/Bs, per-page client JS, and 18 probe docs roots against the dead-link/anchor/image/parity checks; produced the experimentalWorker P2, the gate false-negative P2 (frontmatter/nav/JSX links, warm-cache anchors), the sidebar-is-93 %-of-HTML analysis, and the hash/lastmod/printFileSize P3s.
  4. Runtime quality — headless-Chromium crawl of 114 pages plus search, switcher, dark-mode, 404 and live-Pages matrices; produced the search-race P1, the live 404 evidence for llmsUI/EditLink/robots, the overflow measurements, and the contrast/a11y P3s.
  5. Accuracy sampling — 25 sentences + AB-code / CLI-flag / config-key sweeps; 20 % drift, the cli.mdx:288 and hooks.mdx:132 P1s, the configuration.mdx and hooks.mdx:146 P2s.
  6. Currency / deps / docs.yml — versions vs latest, peer constraints, action pins, last 80 runs, gate parity; no P1/P2, six P3s.
  7. Coordinator — the TypeDoc router: 'module' A/B in a third worktree; verification of every P1 against the built HTML, the config, and live Pages; integration.

Cross-references: #566 (combined survey, §4 docsite), #572 (Rsbuild audit; performance.buildCache P2 not restated), #576 (format).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions