Skip to content

chore: migrate offlinedocs to Fumadocs (render parity, single version) [DOCS-593] - #27390

Closed
nickvigilante wants to merge 38 commits into
mainfrom
vigilante/docs-593-migrate-offlinedocs-to-fumadocs-phase-1-render-parity-single
Closed

chore: migrate offlinedocs to Fumadocs (render parity, single version) [DOCS-593]#27390
nickvigilante wants to merge 38 commits into
mainfrom
vigilante/docs-593-migrate-offlinedocs-to-fumadocs-phase-1-render-parity-single

Conversation

@nickvigilante

@nickvigilante nickvigilante commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

What

Migrates offlinedocs/ from the Next.js pages-router + react-markdown + Chakra renderer to a Fumadocs App Router static export. Phase 1 of moving the offline docs onto Fumadocs: coder/coder docs corpus only, single version, images bundled locally for offline/airgapped use.

This is a renderer swap at content/render parity: every page renders (463 pages, filtered to manifest.json routes), now with Fumadocs' UI plus the Coder brand theme (and dark mode), rather than pixel-identical Chakra output.

Why

  • Replaces the pages-router + Chakra renderer with a modern component library and brings net-new capabilities the old renderer lacked: offline client-side search, theme and dark-mode parity with the product, and click-to-zoom images.
  • Standardizes the corpus's rendering surface (the remark/rehype plugins in src/mdx-plugins/ and the sync transforms in scripts/lib/) on Fumadocs. offlinedocs is the only Fumadocs consumer in this repo today, so this does not share a pipeline with the hosted docs yet; the payoff is that if the hosted docs later migrate to Fumadocs, sharing this plugin surface becomes a directory move rather than a rewrite.
  • Reduces drift between the offline docs renderer and the direction the hosted docs are heading (Fumadocs).

How it works

  • A build-time sync (scripts/sync-docs.mjs) reads this repo's own docs/ tree + manifest.json and emits a Fumadocs content source (content/docs/**, generated + gitignored) plus locally bundled images (public/images, gitignored). No network access.
  • next build with output: export writes the static site to offlinedocs/out/, and the corpus is emitted as .md + rehype-raw so raw HTML (tables, <details>, etc.) renders as it does on GitHub.
  • The pure string transforms live in scripts/lib/transform.mjs and the pure route/ordering logic in scripts/lib/routes.mjs; the remark/rehype plugins live in src/mdx-plugins/.

Output contract preserved

  • offlinedocs/out/index.html at root; make build/coder_docs_<version>.tgz still tars offlinedocs/out/ for the release pipeline (release.yaml / tag-and-release.yaml), unchanged.
  • The CI offlinedocs job contract is unchanged: pnpm format:check, pnpm lint, make build/coder_docs_<version>.tgz, check_unstaged.sh.

Notes for reviewers

  • offlinedocs/package.json declares pnpm.overrides mirroring the repo-root overrides. This matters because two CI paths consume offlinedocs/pnpm-lock.yaml: the offlinedocs job's scoped cd offlinedocs && pnpm install (also used to build the release tarball), and the Storybook job's recursive root install (pnpm/action-setup with run_install: true), which enumerates offlinedocs, site, and scripts/apidocgen and validates each project's lockfile with --frozen-lockfile. That recursive pass, not the root's own single-importer pnpm install, is what a lockfile drift breaks. pnpm applies overrides from the workspace root, so offlinedocs' block is recorded into its own lockfile by the scoped install. Only js-yaml and picomatch resolve to packages in offlinedocs' tree, so those are the only two entries kept.
  • jsdom is a new dev dependency (pinned to 25.0.1 for Node compatibility) backing a node:test component-test harness for OSTab (test/jsdom-setup.mjs preload + src/components/os-tab.test.tsx). It renders the component against the real fumadocs-ui Tabs to pin the shared-OS-group clamp against regression. Dev-only; it is not shipped in the bundle.

Validation (local)

  • pnpm format:check and pnpm lint (sync + fumadocs-mdx + typegen + tsc): pass.
  • pnpm test: 67 tests pass (31 scripts/** transforms + routes, 31 src/mdx-plugins/** plugins, 5 src/components/** jsdom render tests).
  • pnpm export (sync + next build, output: export): 463 pages (manifest routes only), index.html at out/ root, sync reports 0 unmapped links and 0 unresolved images.
  • Spot-checked rendered HTML: callouts, tables, tabs, <details> accordions, Shiki code highlighting, clean inter-doc links (no .md), source-tree links rewritten to github.com, and bundled images.

Review updates

Changes since the initial draft (some intentionally beyond render parity, per review):

  • Sidebar matches manifest.json exactly: the generated tree is filtered to manifest routes, dropping non-manifest orphan files so the offline nav matches what coder.com publishes.
  • The homepage sidebar entry is labeled "Home" instead of a second "About", matching how coder.com renders the manifest's first (README) route.
  • The Coder logo renders in the foreground color (monochrome) rather than the brand violet.
  • Doc images are click-to-zoom (Fumadocs ImageZoom).
  • next dev no longer errors on incidental non-prerendered requests (output: export is applied to production builds only).
  • External links open in a new tab with a trailing external-link icon.
  • The OS switcher shows Apple / Linux / Windows brand icons (@phosphor-icons/react, the icon set coder.com uses).
  • The dev server runs on port 26337 (next dev -p 26337) instead of 3000, which collided with coder server's default access URL.
  • Offline search: a build-time Orama index (staticGET, emitted to out/api/search) plus Fumadocs' static client, so the bundle is searchable entirely client-side with no server and no external network calls. 463 pages indexed; adds ~46 MB to the bundle.
Round 4 review (coder-agents-review): findings addressed

Resolves the full-panel review findings CRF-19 through CRF-40 in commit 1687ad94e. Highlights:

  • Route logic extracted to scripts/lib/routes.mjs with table tests; the generated meta.json is byte-identical to before the refactor across the 463-page corpus.
  • New hard-fail guards in the sync: two source files whose routes collide, and an unterminated <!-- comment that would blank a page (both name the offending source).
  • Source-tree links (../../coderd, ...) are rewritten to github.com/coder/coder URLs at sync time (112 links across 37 pages) instead of shipping dead relative links.
  • One blockquote-aware fence scanner now backs every prose transform, so blockquoted fenced content is opaque to all of them.
  • OSTab gains a jsdom render-test harness pinning the shared-group clamp (verified: removing the clamp fails the test).
  • Plus the smaller items: single-OS alias canonicalization, nested-<details> fallback, structuredClone isolation test, MdastNode consolidation, tuple-based meta.json ordering, .mdx-forward comment rewrites, and dead-code/boilerplate removal.

Out of scope (follow-ups)

  • Search index size and relevance tuning (the index is ~46 MB; the REST API reference dominates some result sets). Tracked in DOCS-639.
  • Image bundle-size optimization (ships the full docs/images tree, as before). Tracked in DOCS-639.
  • The offlinedocs in-page heading-anchor bug (tracked in DOCS-525).
  • On-this-page TOC rail styling: the flattened straight-rail variant was backed out (it misaligned the step-number badges in UAT); the default Fumadocs rail is restored. Whether to reintroduce a flattened design is tracked in DOCS-686.

Local testing

If the docs render but the UI is unclickable (OS tabs, theme toggle, sidebar), the cause is almost always a stale service worker cached in the browser at the dev origin. A prior site on http://localhost:3000 (a local coder server or the old offlinedocs) can leave a service worker registered there; it then intercepts requests for this app and serves stale assets, so the page never hydrates and nothing responds to clicks.

This PR mitigates it two ways:

  • The dev server runs on :26337 instead of :3000, so it no longer shares an origin with a service worker registered by another local app.
  • output: "export" is applied only to production next build, not next dev. In export mode Next adds a catch-all route that rejects any non-prerendered path; a stale /serviceWorker.js request is exactly such a path, so next dev used to error on it. Dev now uses Next's normal server (clean 404s).

If a machine still shows a dead page from an earlier session, unregister the service worker (DevTools > Application > Service workers > Unregister) and hard-reload.

Security note: raw HTML rendering

Document bodies now render as raw HTML (remarkRehypeOptions.allowDangerousHtml + rehype-raw in source.config.ts); the previous renderer's sanitize-html pass is intentionally dropped. Raw-HTML fidelity with the corpus (tables, <details>, and similar) is the goal of this change, and the input is coder/coder's own committed docs/ tree, which is authored and human-reviewed rather than untrusted user input. Nothing replaces the sanitizer today; if offlinedocs ever renders content from an untrusted source, reintroduce sanitization at that boundary. (coder-agents-review CRF-9.)

Draft pending review. Linear: DOCS-593, DOCS-599.

This PR was created with AI assistance (Coder Agents).

@linear-code

linear-code Bot commented Jul 21, 2026

Copy link
Copy Markdown

DOCS-593

@nickvigilante
nickvigilante force-pushed the vigilante/docs-593-migrate-offlinedocs-to-fumadocs-phase-1-render-parity-single branch 3 times, most recently from cd80df4 to b99faf5 Compare July 23, 2026 14:16
@nickvigilante
nickvigilante force-pushed the vigilante/docs-593-migrate-offlinedocs-to-fumadocs-phase-1-render-parity-single branch from 5f382a4 to 0b4a83c Compare August 4, 2026 17:46
@nickvigilante

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-08-19 15:59 UTC by @nickvigilante

Review history
  • R1 (2026-08-04), 3 Nit, 2 Note, 1 P0, 2 P1, 1 P2, 6 P3, COMMENT. Review
  • R2 (2026-08-12), 4 Nit, 3 Note, 1 P0, 2 P1, 2 P2, 6 P3, COMMENT. Review
  • R3 (2026-08-13), 4 Nit, 3 Note, 1 P0, 2 P1, 2 P2, 6 P3, COMMENT. Review
  • R4 (2026-08-13): 19 reviewers, 12 Nit, 6 Note, 1 P0, 2 P1, 5 P2, 14 P3, COMMENT. Review
  • R5 (2026-08-14): 20 reviewers, 16 Nit, 7 Note, 1 P0, 3 P1, 6 P2, 16 P3, REQUEST_CHANGES. Review
  • R6 (2026-08-14), 16 Nit, 7 Note, 1 P0, 3 P1, 6 P2, 16 P3, COMMENT. Review
  • R7 (2026-08-14): 19 reviewers, 20 Nit, 8 Note, 1 P0, 3 P1, 7 P2, 18 P3, COMMENT. Review
  • R8 (2026-08-18): 18 reviewers, 23 Nit, 8 Note, 1 P0, 3 P1, 9 P2, 18 P3, COMMENT. Review
  • R9 (2026-08-19): 19 reviewers, 24 Nit, 8 Note, 1 P0, 3 P1, 9 P2, 19 P3, COMMENT. Review
  • R10 (2026-08-19): 11 reviewers, 24 Nit, 8 Note, 1 P0, 3 P1, 9 P2, 19 P3, APPROVE. Review

deep-review v0.9.0 | Round 10 | 5f6eeda..47ba05b

Last posted: Round 10, 64 findings (1 P0, 3 P1, 9 P2, 19 P3, 24 Nit, 8 Note), APPROVE. Review

Finding inventory

Finding inventory, PR #27390

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P0 Author fixed (babe71e) offlinedocs/package.json:32 offlinedocs/pnpm-lock.yaml out of sync with manifest, CI install fails before build R1 Netero Yes
CRF-2 P1 Author fixed (dd527fd) offlinedocs/src/components/os-tab.tsx:92 OSTab passes groupId without items; a stored OS absent from a tab set renders the whole set empty R1 Netero Yes
CRF-3 P1 Author fixed (transform.test.mjs added) offlinedocs/scripts/lib/transform.mjs:1 1406 lines of pure transform logic, zero tests, no test runner, module header claims testability R1 Netero Yes
CRF-4 P2 Author fixed (plugin deleted) offlinedocs/src/mdx-plugins/remark-code-meta.ts:1 Plugin can never fire (no fence in docs/ carries meta) and its comment claims otherwise R1 Netero Yes
CRF-5 P3 Author fixed (cn.ts and tailwind-merge removed) offlinedocs/src/lib/cn.ts:1 Dead export; tailwind-merge direct dependency exists only for it R1 Netero Yes
CRF-6 P3 Author fixed both halves (.md dd527fd, images 1874ea8) offlinedocs/scripts/sync-docs.mjs:338 Sync counts unresolvable inter-doc links, then ships them as dead links R1 Netero Yes
CRF-7 P3 Author fixed (810612f) offlinedocs/src/mdx-plugins/remark-details-accordion.ts:27 isHtml and hand-rolled mdast/hast types duplicated across five plugin files R1 Netero Yes
CRF-8 P3 Author fixed (overrides trimmed, note rewritten) offlinedocs/package.json:44 4 of 7 mirrored pnpm.overrides resolve to nothing; PR description's stated reason is wrong R1 Netero Yes
CRF-9 P3 Author accepted R2 (raw-HTML fidelity is the goal; removal now documented in PR description) offlinedocs/pages/[[...slug]].tsx:223 (deleted) sanitize-html removed from the render path with no replacement and no disclosure R1 Netero (Note), orchestrator P3 Yes
CRF-10 P3 Deferred (DOCS-639) offlinedocs/next.config.mjs:1 571 MB bundle, 46 MB uncompressed search index served on first keystroke, deferred without ticket R1 Netero (Note), orchestrator P3 Yes
CRF-11 Nit Author fixed (26323fe) offlinedocs/src/lib/shared.ts:8 gitConfig.branch written, never read R1 Netero Yes
CRF-12 Nit Author fixed (26323fe) offlinedocs/next.config.mjs:30 productionBrowserSourceMaps: false is the default and the comment describes a different option R1 Netero Yes
CRF-13 Nit Author fixed (26323fe) offlinedocs/package.json:10 Retained start script is broken under output: export and reannounces port 3000 R1 Netero Yes
CRF-14 Note Author accepted R2 (log retrieved; failure was CRF-1 drift via recursive install) offlinedocs/package.json:1 Reported Storybook CI failure could not be attributed to this PR; log unreadable R1 Netero Yes
CRF-15 Note Author fixed (810612f) offlinedocs/src/mdx-plugins/remark-details-accordion.ts:106 One <details> nested in a list item renders as a native disclosure, not an Accordion R1 Netero Yes
CRF-16 P2 Author fixed (1874ea8) offlinedocs/src/mdx-plugins/remark-coder-tabs.ts:1 587 lines of pure mdast/hast plugins ship with zero unit tests, same class as CRF-3 R2 Netero Yes
CRF-17 Nit Author fixed (1874ea8) offlinedocs/package.json:24 lucide-react declared as a direct dependency with zero imports R2 Netero Yes
CRF-18 Note Author fixed (b99be00), and corrected the finding's diagnosis offlinedocs/src/mdx-plugins/remark-details-accordion.ts List-item-nested <details> limitation is undocumented after the corpus-side CRF-15 fix R2 orchestrator Yes
CRF-19 P2 Author fixed (1687ad9, jsdom harness + os-tab.test.tsx) offlinedocs/src/components/os-tab.tsx:129 CRF-2's one-line clamp plus three fumadocs-ui internal couplings have zero test coverage R4 Meruem P3, Chopper P3, Kite Note, Mafu-san Note; orchestrator P2 Yes
CRF-20 P2 Author fixed (1687ad9, single fence tracker) offlinedocs/scripts/lib/transform.mjs:138 Two fence scanners, half the transforms use the blockquote-blind one; 3 reproduced content corruptions R4 Meruem P3 x2, Hisoka Note; orchestrator P2 Yes
CRF-21 P2 Author fixed (collisions collected and fail the sync) offlinedocs/scripts/sync-docs.mjs:220 Two docs whose basenames slugify alike silently overwrite each other; log overcounts R4 Hisoka Yes
CRF-22 P3 Author fixed (unclosedCommentLine fails the sync) offlinedocs/scripts/lib/transform.mjs:141 An unclosed <!-- blanks every following line of the doc, build stays green R4 Hisoka Yes
CRF-23 P3 Author fixed (buildAccordion bails on nested open) offlinedocs/src/mdx-plugins/remark-details-accordion.ts:71 Nested <details> mis-grouped, orphaning prose and a stray close tag R4 Kite P3, Mafuuu Note Yes
CRF-24 P3 Author fixed (solo alias canonicalized) offlinedocs/src/mdx-plugins/remark-coder-tabs.ts:190 expandOSTabs canonicalizes only multi-OS labels, so solo aliases desync producer and consumer R4 Melody Yes
CRF-25 P3 Author fixed (isolation assertion added) offlinedocs/src/mdx-plugins/remark-coder-tabs.test.ts:117 Bundled-OS test passes with structuredClone removed, so it asserts expansion but not isolation R4 Bisky Yes
CRF-26 P3 Author fixed (ctx.sourceLink rewrites escaping relative targets) offlinedocs/scripts/sync-docs.mjs:362 110 relative links that are neither .md nor images ship dead while the new guard hard-fails on .md R4 Mafuuu Note; orchestrator P3 Yes
CRF-27 P3 Author fixed (comments reworded as .mdx-forward no-ops) offlinedocs/scripts/lib/transform.mjs:302 Three transforms are no-ops for the shipped .md pipeline and their comments claim risks it does not have R4 Luffy P3, Zoro Nit Yes
CRF-28 P3 Author fixed (error names CODER_DOCS_DIR) offlinedocs/scripts/sync-docs.mjs:57 Manifest-missing error omits the CODER_DOCS_DIR override the script documents 6 lines above R4 Leorio Yes
CRF-29 P3 Author fixed (routes.mjs + routes.test.mjs extracted) offlinedocs/scripts/sync-docs.mjs:97 Pure route-mapping helpers in the sync orchestrator are untested; CRF-21 lives in one of them R4 Bisky Note; orchestrator P3 Yes
CRF-30 Nit Author fixed (shared MdastNode imported everywhere) offlinedocs/src/mdx-plugins/remark-github-callouts.ts:19 CRF-7's end state unreached: third plugin and two test files keep private MdastNode copies R4 Bisky, Mafu-san, Robin, Zoro, Meruem Yes
CRF-31 Nit Author fixed (both documented cases pinned) offlinedocs/src/mdx-plugins/remark-details-accordion.test.ts:78 No test pins the documented </details><br> fallback that took three rounds to diagnose R4 Bisky Note, Mafu-san Note, Chopper Nit Yes
CRF-32 Nit Author fixed offlinedocs/scripts/sync-docs.mjs:167 unmappedMdLinks counter is redundant with unmappedLinks.length and can drift R4 Gon, Chopper Yes
CRF-33 Nit Author fixed (bucket tuple ordering in routes.mjs) offlinedocs/scripts/sync-docs.mjs:328 Unnamed sentinels 100000 and 99999 hide a corpus-size ceiling in keyFor R4 Gon Yes
CRF-34 Nit Author fixed (renamed restoreStepProps) offlinedocs/src/mdx-plugins/rehype-step-toc-numbers.ts:29 Helper named fix for the plugin's core operation R4 Gon Yes
CRF-35 Nit Author contested; panel closed R5 (4/4 accept the deferral, defense's stated grounds corrected) offlinedocs/src/components/os-tab.tsx:93 OSTab singular against Fumadocs' plural-container Tabs/Tab convention R4 Gon Yes
CRF-36 Nit Author fixed (shared.ts deleted, constants inlined) offlinedocs/src/lib/shared.ts:1 Eight-line module for two constants with one caller; gitConfig splits a value only ever rejoined R4 Zoro Yes
CRF-37 Nit Author fixed (boilerplate deleted) offlinedocs/src/components/mdx.tsx:122 useMDXComponents and MDXProvidedComponents are unwired starter boilerplate R4 Zoro Note; orchestrator Nit Yes
CRF-38 Note Author accepted R5 (single decision point kept deliberately; split point recorded) offlinedocs/src/components/mdx.tsx:58 LocalImg frames every image in the screenshot treatment with no opt-out R4 Nami Yes
CRF-39 Note Author accepted R5 (description reworded; shared-pipeline claim dropped) offlinedocs/package.json:4 The "share one MDX pipeline" benefit is not evidenced anywhere in this repo R4 Pariston Yes
CRF-40 Note Author accepted R5 (shim complete for current renderer; deeper fix out of scope) offlinedocs/src/mdx-plugins/rehype-step-toc-numbers.ts:29 The step-TOC shim treats a symptom of rehype-raw whole-tree round-tripping R4 Pariston Yes
CRF-41 Nit Author fixed (0701ec0) offlinedocs/src/components/mdx.tsx:89 {...props} spread after target/rel lets caller attributes override the safe defaults R5 Netero, Pariston Yes
CRF-42 P1 Author fixed (0701ec0); verified by orchestrator across all 463 emitted pages offlinedocs/scripts/lib/transform.mjs:384 extractTitle ignores YAML frontmatter, so 195 generated pages take the make gen comment as their title and leak the original frontmatter into the body R5 Mafuuu Yes
CRF-43 P2 Author fixed (0701ec0, inlineCodeSpan + mapOutsideInlineCode) offlinedocs/scripts/lib/transform.mjs:456 rewriteContent and stripHtmlComments are opaque to inline code spans while their three siblings are not; same asymmetry CRF-20 closed one level up R5 Hisoka P3, Pariston P3; orchestrator P2 Yes
CRF-44 P3 Author fixed (findUnbackedManifestRoutes gate) offlinedocs/scripts/sync-docs.mjs:290 A manifest route with no backing file is dropped from the sidebar silently, unlike the four defect classes the sync does fail on R5 Knov Yes
CRF-45 P3 Author fixed (detectOS takes ua; 3 pure tests + first-visit render) offlinedocs/src/components/os-tab.tsx:53 detectOS and its three UA regexes have no coverage; both jsdom tests preseed storage and skip that branch R5 Bisky P3, Chopper Note Yes
CRF-46 Nit Author fixed (createRewriteCtx factory) offlinedocs/scripts/sync-docs.mjs:147 Module-scope currentSourceRel duplicates rewriteContent's rel argument, kept in sync by discipline R5 Meruem Yes
CRF-47 Nit Author fixed (HastNode exported and imported) offlinedocs/src/mdx-plugins/rehype-step-toc-numbers.test.ts:7 HastNode still duplicated between plugin and test; CRF-30's end state reached on mdast only R5 Robin Yes
CRF-48 Nit Author fixed (quotedDelim) offlinedocs/scripts/lib/transform.mjs:121 quoted_delim breaks the file's camelCase convention R5 Gon Yes
CRF-49 Note Author accepted R7 (agrees with Kite's counter-evidence; will route filtered targets through ctx.sourceLink next time) docs/about/contributing/documentation.md:36 Two corpus edits moved workarounds into docs/ rather than into the sync; pattern worth watching R5 Pariston Note (Kite counter-evidence) Yes
CRF-50 P3 Author fixed (271c569, delegates to fumadocs-core frontmatter) offlinedocs/scripts/lib/transform.mjs:410 parseFrontmatter treats any Word: value block as frontmatter and strips it, while its docstring claims content is never stripped R7 Netero Yes
CRF-51 Note Author fixed (b8dc545); verified 0 pages with a stray leading --- offlinedocs/scripts/sync-docs.mjs:224 H1 stripping leaves a decorative --- as the first body line on one page, rendering a stray <hr> R7 Netero Yes
CRF-52 P2 Author fixed (271c569); verified 0 literal backslash-quotes in emitted output offlinedocs/scripts/lib/transform.mjs:439 parseFrontmatter strips outer quotes without decoding YAML escapes, so 3 CLI pages ship literal backslash-quotes in description and meta tag R7 Hisoka P2, Mafuuu P2, Robin Nit (alternative) Yes
CRF-53 P2 Author fixed (9e37629); verified by mutation, disabling the seed now fails the test offlinedocs/src/components/os-tab.test.tsx:127 The first-visit render test cannot reach the seeding branch, and the Android exclusion has no assertion R7 Meruem Note, Chopper Note (R7); Mafu-san P2, Hisoka P3, Razor P3, Komugi (R8); orchestrator P2 Yes
CRF-54 Nit Author fixed (b8dc545) offlinedocs/scripts/lib/transform.mjs:291 inlineCodeSpan's docstring claims the corpus has no multi-line inline code spans; it has at least six, three shipped R7 Mafuuu Note; orchestrator Nit Yes
CRF-55 Nit Author fixed (b8dc545, docstrings trimmed and sees reverted) offlinedocs/next.config.mjs:1 Three new config docstrings restate the per-field comments below them, landed in a P1 fix commit alongside a prose sweep R7 Gon P2 x3, Mafu-san Note, Zoro Note; orchestrator Nit Yes
CRF-56 Nit Author fixed (b8dc545, renamed inlineCodeSpanLen) offlinedocs/scripts/lib/transform.mjs:292 inlineCodeSpan returns a character count but both callers name it span R7 Gon Yes
CRF-57 Nit Author fixed (b8dc545, export dropped) offlinedocs/src/components/os-tab.tsx:54 currentUserAgent is exported with no importer outside its own file R7 Zoro Yes
CRF-58 P2 Author fixed (9aef7c7); verified, homepage link round-trips byte-identical offlinedocs/scripts/lib/transform.mjs:344 normalizeAngleBrackets rewrites the URL inside a [label](<url>) destination, shipping a broken href on the docs homepage R8 Nami Yes
CRF-59 P3 Author fixed (9aef7c7, FrontmatterError + sixth defect class); verified, names source and reason offlinedocs/scripts/lib/transform.mjs:420 Malformed YAML frontmatter throws an unlabeled YAMLException through the sync, naming no source file R8 Netero P3, Meruem P3, Knov P3, Hisoka Nit Yes
CRF-60 Nit Author accepted R9 (strictness documented as intentional; corpus lines split to #28304 and #28305) offlinedocs/src/mdx-plugins/remark-github-callouts.ts:43 MARKER is strict-uppercase and rejects a space after !, so four corpus blockquotes render the marker as literal text R8 Melody Note; orchestrator Nit Yes
CRF-61 Nit Author fixed (9aef7c7) offlinedocs/next.config.mjs:30 reactStrictMode: true restates the App Router default R8 Zoro Yes
CRF-62 Nit Author fixed (9aef7c7) offlinedocs/scripts/lib/transform.test.mjs:214 A test comment describes the pre-swap hand-rolled mechanism the code no longer uses R8 Netero Yes
CRF-63 P3 Author fixed (47ba05b); verified, both mutations now fail the test offlinedocs/scripts/lib/transform.test.mjs:131 The CRF-58 fix has three branches and the new test asserts one; the live parenthetical-autolink branch is unpinned R9 Bisky Yes
CRF-64 Nit Author fixed (47ba05b, try/catch and MARKER comments trimmed) offlinedocs/scripts/lib/transform.mjs:446 The same attribution rationale is repeated at three layers: class doc, try/catch, and the sync's catch block R9 Gon x3; combined by orchestrator Yes

Law analysis

Round 5 re-analysis

  • Effective LOC at head 041d632: +3803 -682 (45 files), 3180 production, 623 test, test density 16.4%. Growth since Law's round-1 analysis: 2105 -> 3803, so the +500 re-run threshold fired.
  • Verdict: Split, vertically, unchanged. Enforcement: Advisory, unchanged.
  • Round-1 escalation gate was "author declines the slice cut AND the plugin suite lands with no tests." The author declined the cut; the plugin suite landed with 41 tests plus a jsdom harness. One of two conditions, so the escalation does not trip.
  • Law's reasoning for staying advisory is empirical rather than procedural: the failure mode a mandatory split prevents is dangerous parts being drowned out by noise, and across rounds 1 to 4 each concern did get per-concern scrutiny (CRF-9 and CRF-10 on the renderer swap and the artifact, CRF-3/16/20-22/26-27 on sync and plugins, CRF-2/19/24-25/31/35 on OSTab).
  • Law corrected its own round-1 claim: the atomicity argument that remarkSteps must travel with normalizeStepHeadings and rehypeStepTocNumbers was overstated. remarkSteps is a Fumadocs-core plugin configured in source.config.ts, so it already sits with normalizeStepHeadings; only rehypeStepTocNumbers is in the second slice, and its absence degrades sidebar TOC numbering rather than emitting literal [step] markers. The round-1 entry below carries the stronger, wrong claim; this correction supersedes it.

Round 1 analysis

  • Effective LOC: +2105 -658 (34 files). Generated: 2317 (offlinedocs/pnpm-lock.yaml). Test density 0.0%.
  • Head SHA analyzed: 0b4a83c.
  • Verdict: Split, vertically. Enforcement: Advisory.
  • Proposed slices: (1) chore(offlinedocs): migrate to Fumadocs at renderer parity (toolchain, sync pipeline, app shell, old-renderer removal, ~1210 production LOC); (2) feat(offlinedocs): render Coder authoring constructs (mdx-plugins + OSTab + normalizeStepHeadings, ~824 LOC, the untested tree surgery); (3) feat(offlinedocs): offline client-side search (~15 LOC carrying a ~46 MB artifact decision).
  • Atomicity constraint the split must respect: normalizeStepHeadings, remarkSteps, and rehypeStepTocNumbers must travel together. Shipping the sync step without the steps plugins renders a literal [step] in headings on 463 pages with the original numbering already stripped.
  • What would flip Law to mandatory: author declines the slice-1/slice-2 cut and the plugin suite lands with no tests.

Contested and acknowledged

CRF-9 (P3, deleted offlinedocs/pages/[[...slug]].tsx:223) - sanitizer removed from the render path

  • Finding: The old renderer ran sanitizeHtml(body) on every document body. The new pipeline uses allowDangerousHtml + rehype-raw with no sanitizer, sanitize-html was dropped from package.json, and the PR did not mention the removal. The ask was disclosure, not reverting.
  • Author defense: Removal is deliberate. Raw-HTML fidelity with the corpus (tables, <details>) is the goal of the change, and the input is coder/coder's own committed docs/ tree, authored and human-reviewed rather than untrusted input. Nothing replaces the sanitizer today.
  • Author accepted (R2): The PR description gained a "Security note: raw HTML rendering" section stating the removal is deliberate, naming what replaced it (nothing), and instructing that sanitization be reintroduced at the boundary if offlinedocs ever renders content from an untrusted source. That was the entire ask, so the finding is satisfied. Note for future rounds: the acceptance is scoped to the current input being the committed docs/ tree. If a later change feeds offlinedocs any content that is not human-reviewed at merge time, this reopens.

CRF-14 (Note, offlinedocs/package.json:1) - unattributed Storybook CI failure

  • Finding: The Storybook job was red on the round-1 head and the review environment could not read its log (gh returns HTTP 401 there). The review asked the author to confirm from the log rather than assume it was unrelated.
  • Author defense: Retrieved the log. The Storybook step runs pnpm recursive install via pnpm/action-setup with run_install: true, and the log shows it enumerating offlinedocs, site, and scripts/apidocgen, then failing with CRF-1's exact signature (ERR_PNPM_OUTDATED_LOCKFILE ... @types/node (lockfile: 22.20.1, manifest: 22.20.0)). The job is green after the CRF-1 fix.
  • Author accepted (R2): Confirmed, and it corrects the review. Round 1 asserted "the root install is not recursive" on the evidence that the root pnpm-lock.yaml has a single . importer and a root pnpm install --frozen-lockfile passed. Both observations were true and the conclusion drawn from them was wrong: pnpm/action-setup with run_install: true runs pnpm recursive install, which is a different command from the root install that was tested. Round 2 CI is green, which is consistent with the author's account.

Round 3 update

BLOCKED. CRF-18 silent. No review.

Churn guard: 3 addressed (CRF-6 image half, CRF-16, CRF-17), 1 silent (CRF-18), out of 4 open. CI green. Law was not re-run despite effective additions growing 2105 -> 3039 (+934, past the 500 threshold), because BLOCKED spawns no reviewers, Netero and Law included.

The block is caused by this review's own tooling, not by author neglect. CRF-18 was posted in round 2 as a new finding on the CRF-15 thread, and deep-review post could not unresolve that thread (unresolveReviewThread: Resource not accessible by integration), so the finding has read as Resolved and Outdated in the GitHub UI since it was written. The author's round-3 summary enumerates CRF-16, CRF-17, and CRF-6 and never mentions CRF-18, which is what a finding that was never visible looks like.

Orchestrator error to record: journal/2026-deep-review-unresolve-permission-silent-finding.md documents this exact failure mode from PR #27256, and it was read during round 2 after the warning appeared. The right response was to put CRF-18 in the review body, where nothing can collapse it. Instead it went on a resolved thread and the round-2 body named only CRF-6. Any finding that depends on unresolve for visibility must be restated in the body of the same review.

Also this round: the author supplied direct evidence on the CRF-8 thread that pnpm/[email protected] with run_install: true compiles to pnpm recursive install (run_install: true becomes [{recursive: true}] in the pinned action's dist/index.js), and demonstrated the split empirically: the non-recursive root install passes against a drifted offlinedocs lockfile while pnpm -r install --frozen-lockfile fails with Scope: all 4 projects and ERR_PNPM_OUTDATED_LOCKFILE. This confirms the round-2 correction. They also corrected their own round-1 reply: the four no-op overrides were already removed in 26323fe, so "kept as a deliberate mirror" was wrong and the end state CRF-8 asked for was already in place.

Round 4

Churn guard: PROCEED (CRF-18 addressed, 0 silent). CI pending at setup (5 passed, 19 pending, 5 skipped) on head b99be00.

First panel round. Step 3a skipped: deep-review select caps consecutive Netero-only rounds at 2 and rounds 1 and 2 were both Netero-only, so Netero no longer gates panel selection and the panel runs regardless of first-pass severity. Netero not re-run, and the delta since its round-3 pass is a 12-line comment block. Law not re-run either, even though effective additions grew 2105 -> 3051 (+946, past the 500 threshold): Law's own stated escalation condition was "the author declines the slice cut AND the plugin suite lands with no tests," and the plugin suite now has 41 tests, so a re-run could only repeat the advisory verdict already recorded above.

CRF-18's diagnosis was wrong and the author corrected it with evidence. The finding, inherited from CRF-15, blamed list-item nesting for the un-converted <details>. The author parsed three variants through mdast-util-from-markdown and ran the plugin: nested under a list item with a clean close converts, top level with a clean close converts, and the original corpus form with </details><br> does not. The real constraint is DETAILS_CLOSE (/<\/details>\s*$/i), which requires the closing raw-HTML node to end with </details>, so trailing markup on the close line defeats the match. The round-1 CRF-15 fix changed both variables at once (hoisted out of the list and dropped the <br>), which is what made nesting look causal. Neither Netero nor this orchestrator tested the two variables independently before asserting the cause. The comment landed in remark-details-accordion.ts:18-28 names the delimiter rule, not the nesting story.

Panel: 17 trigger-matched (bisky, hisoka, mafu-san, mafuuu, pariston, komugi, gon, leorio, ging-ts, ging-react, nami, chopper, robin, melody, kurapika, luffy, zoro) plus 2 wildcards drawn with shuf from the distilled set (meruem, kite). Gon and Leorio are here on the once-per-PR floor. Multi-domain: TypeScript and React frontend, Node build tooling, CI and dependency surface, and docs content.

Four reviewers returned no findings: komugi (determinism), ging-ts and ging-react (language and framework modernization), kurapika (security). Kurapika finding nothing on the surface CRF-9 documents is a substantive result, not an empty one.

Cross-check decisions:

  • CRF-19 raised to P2 over the panel's P3/Note spread. Chopper and Meruem said P3, Kite and Mafu-san said Note; disagreement means the higher wins, and the consequence chain across them is worse than any single view. The clamp at os-tab.tsx:129 is the entire fix for a P1 that already shipped once, it is unasserted, offlinedocs has no component-test harness at all, and Meruem traced three separate couplings to unversioned fumadocs-ui internals (storage-key equality, escapeValue byte-identity, and the outgoing onValueChange call) each of which silently restores the P1 on a dependency bump.
  • CRF-20 combines three separately reproduced corruptions that share one root cause: transform.mjs carries two scanners (fenceScan and the blockquote-aware mapProseLines) and only half the transforms use the aware one. Meruem rated two instances P3, Hisoka rated the class a Note. Combined at P2 because the fix is structural and eliminates the class, and because the failure profile is silent content corruption with a green build. Reproduced independently in the orchestrator worktree: a comment straddling a fence deletes the fence opener and leaves everything after it inside a phantom fence, and rewriteContent rewrote [x](guide.md) to [x](/guide) inside a blockquoted code sample.
  • CRF-21 held at Hisoka's P2 rather than raised. Reproduced independently: a three-route manifest with foo-bar.md and foo_bar.md emits two files, logs pages=3, and the hyphen doc's content is gone with no warning. Held at P2 because it is dormant and needs a new colliding filename, not because the consequence is small. The upward argument is real: silent loss of a published page from an airgapped artifact, in a sync that now hard-fails on a single unresolved link.
  • CRF-26 raised from Mafuuu's Note to P3. The 110 dead relative links pre-date this PR and were dead in the old renderer, so it is not a regression, but the PR introduces a link-integrity guard and that guard's scope is the new precondition. Under a no-follow-up assumption nothing else will fix them, and this PR already demonstrates the accepted remedy on one link.
  • CRF-29 raised from Bisky's Note to P3 on an interaction: the pure route-mapping helpers Bisky flagged as untested are exactly where Hisoka's CRF-21 collision lives. The missing coverage is not hypothetical, it already hid a P2.
  • CRF-27 taken at Luffy's P3 over Zoro's Nit. Zoro proved with a probe build that {session_id} renders literally with or without escapeCurlyBraces, so the transforms are inert and their comments overstate the current pipeline's risk. This is the CRF-4 pattern that got remark-code-meta.ts deleted, so the same disposition applies: land them with the .mdx flip that needs them, or correct the comments.
  • CRF-37 raised from Zoro's Note to Nit because the ask is a concrete deletion, matching how CRF-5 and CRF-11 were handled.
  • Nothing dropped or downgraded this round.

Not posted as a finding, raised in the review body instead: the PR description's "Out of scope" bullet says the heading-anchor bug is "tracked separately" without naming the ticket, while the comparable CRF-10 deferral names DOCS-639 (Leorio, Nit). Description-level process observation with no reviewer-reported line to attach it to.

CRF-35 (Nit, os-tab.tsx:93) - OSTab singular against Fumadocs' plural-container convention

  • Finding: Rename OSTab to OSTabs. It wraps <Tab> children under <Tabs>, and the emitter ternary at remark-coder-tabs.ts:267 picks between "OSTab" and "Tabs", so the singular reads as accidental.
  • Author defense: OSTab is emitted into the generated content/docs/** as a component tag and mapped in mdx.tsx, making the name an authored-content contract rather than an internal symbol, so a rename belongs in a standalone change.
  • Panel closed R5 (4/4 accept the deferral, 3 of 4 reject the stated grounds): The defense's factual premise is wrong and the conclusion still holds. Mafu-san ran the sync and grepped: grep -rlF OSTab content/docs/ returns nothing, while 46 generated files contain raw <div class="tabs">. The orchestrator reproduced both greps. OSTab is created at MDX compile time by remark-coder-tabs.ts:267 and consumed by mdx.tsx:115; content/docs/** is gitignored and regenerated every build. So the coupling is three files in this repo, not a corpus contract. Zoro, Pariston, and Razor reached the same conclusion independently. Melody and Kite restated the author's premise without testing it. The finding still closes: at Nit severity with zero runtime consequence, a standalone rename is a fair disposition. Recorded so the reason on file is the true one. Do not reopen on the naming question; if it is reopened, it must be on the naming argument, not on the contract claim.

Round 5

Churn guard: PROCEED (18 addressed, 3 acknowledged, 1 contested, 0 silent, out of 22). Post-panel round, so Netero was advisory and the panel ran regardless. Law re-run (effective additions 2105 -> 3803 crossed the +500 threshold): verdict unchanged, Split/Advisory, and Law corrected its own round-1 atomicity overstatement.

Panel: 18 trigger-matched (bisky, hisoka, mafu-san, mafuuu, pariston, komugi, gon, leorio, ging-ts, ging-react, nami, chopper, robin, melody, kurapika, zoro, meruem, kite) plus 2 wildcards from the distilled set (knov, razor). Gon and Leorio re-spawned under the restructure clause: transform.mjs was rewritten around a new fence primitive, routes.mjs is a new module, and stripHtmlComments changed signature. Six reviewers returned no findings (komugi, leorio, ging-ts, ging-react, nami, kurapika) plus zoro and kite, which reported verification only.

Round-4 fix verification was extensive and independent. Mafu-san removed the CRF-19 clamp and confirmed the jsdom test fails, then restored it. Razor rendered seven before/after pairs through the real unified pipeline to confirm CRF-27's reworded comments are accurate. Netero, Pariston, Razor, Kite, and Zoro each traced the CRF-20 single-tracker refactor and agreed the class is closed. The orchestrator re-ran the sync.

CRF-42 (P1) is the round's real result and it is a miss by every prior round. extractTitle scans for the first H1 outside fences and never accounts for YAML frontmatter, so files whose frontmatter's second line is # Code generated by make gen. DO NOT EDIT. take that comment as the page title. Reproduced by the orchestrator on a fresh sync: 195 of 463 emitted pages carry title: "Code generated by make gen. DO NOT EDIT.", and because h1Line splices only the comment line, the original --- delimiters survive into the body and render as a horizontal rule plus literal title: General text. Five rounds of review, including a 19-persona panel round, did not catch it. The reason is instructive: every prior pass verified transforms against synthetic inputs or against the code, and nobody read the emitted content/docs/** for the largest generated subtree in the corpus.

CRF-43 raised to P2 over two independent P3s (Hisoka, Pariston). Both reproduced their instance, both proposed the same fix, and the reason for the upgrade is consistency with CRF-20: the round-4 refactor established mapOutsideInlineCode as the helper that keeps prose transforms off non-prose regions, three transforms use it, and the two that do not are the two flagged here. This is the same asymmetry CRF-20 was rated P2 for, one level down, created by CRF-20's own fix.

CRF-35 closed as contested. The author's defense rested on a false factual premise and the disposition survives anyway; details in the Contested and acknowledged section. Notable process signal: Melody and Kite both restated the author's premise as verified fact without running the grep that disproves it, while Mafu-san, Zoro, Pariston, and Razor each tested it independently and found it false. Same-model reviewers do not reliably catch a shared blind spot when a plausible claim is already on the record.

Second ask on a description item: the "Out of scope" bullet still says the heading-anchor bug is "tracked separately" without naming a ticket, raised in the round-4 body and unchanged at round 5 (Mafu-san, Nit). Restated in the round-5 body rather than filed as a finding, since there is no code line to anchor it to.

Round 6 update

BLOCKED. All findings silent. No review.

Churn guard: 0 addressed, 0 acknowledged, 0 contested, 0 deferred, 9 silent out of 9 open (CRF-41 through CRF-49, including the CRF-42 P1). No reviewers spawned, Netero and Law included.

The delta is a rebase, not a fix. Round-5 head 041d632 was a merge commit; round-6 head 6de7807 drops it and rebases onto a newer main. git diff --stat 041d6326b..6de7807c2 -- offlinedocs/ is empty, and the 12 changed files are all docs/** prose arriving from main. The author's own comment states the offlinedocs tree is byte-identical to the previously reviewed resolution, which is consistent with the diff.

Timing, recorded because it changes how this should be read. The round-5 review was submitted at 2026-08-14T17:41:47Z. The author's re-request comment is timestamped 2026-08-14T17:56:44Z, fifteen minutes later, and its content is entirely about replacing a merge commit with a clean rebase; it does not mention any round-5 finding. The preceding re-request at 16:50:33Z predates the review and claims round-4 findings are addressed, which was true at the time. The most probable reading is that the re-request was in flight and the author has not yet read the round-5 review, not that nine findings were considered and dismissed. The block stands either way: CRF-42 is a P1 affecting 195 of 463 emitted pages and the code is unchanged.

Visibility is not a factor this round, unlike round 3. All nine threads were created by the round-5 review and are Unresolved in the UI, so nothing is collapsed. Every finding is also named explicitly in the round-6 body.

Round 7

Churn guard: PROCEED (8 addressed, 1 acknowledged, 0 silent, out of 9). CI green. Post-panel round: Netero advisory, Law not re-run (effective additions 3803 -> 4173, a delta of 370, below the 500 threshold).

Panel: 17 trigger-matched plus knov and razor as wildcards. Nine reviewers returned no findings (bisky, leorio, ging-ts, ging-react, nami, kite, knov, razor, komugi), and komugi ran the jsdom suite 20 times serially and 15 times in parallel looking for flake, finding none.

CRF-42 verified independently by the orchestrator before the panel ran, applying round 5's lesson. A fresh sync emits 463 pages; a scan of every page for a missing, empty, overlong, or DO NOT EDIT title and for a stray --- at the top of the body returned one hit, and that hit is a genuine thematic break in the source (tutorials/best-practices/organizations.md), which Netero independently filed as CRF-51. So zero real title anomalies. Four panel reviewers separately re-read the emitted corpus.

CRF-52 is a regression this round created while fixing the P1, and it is the round-5 lesson repeating one column over. sync-docs.mjs now prefers frontmatter description over the manifest, which routes three CLI pages through the new hand-rolled parser for the first time. That parser does value.slice(1, -1) and never decodes YAML escapes, so \" survives as a literal backslash-quote into the emitted frontmatter, the rendered <DocsDescription>, the <meta name="description">, and the Orama index. Hisoka and Mafuuu found it independently, both at P2, both with byte-level evidence; the orchestrator reproduced it by diffing source against emitted output for reference/cli/config-ssh.md. My own CRF-42 verification checked the title column and missed the description column immediately beside it.

Robin supplied the structural remedy and it is folded into CRF-52 rather than filed separately: fumadocs-core is already a direct dependency and publicly exports content/md/frontmatter, which is js-yaml backed and returns { matter, data, content }. Robin verified it against the same four corner cases the hand-rolled parser handles. Adopting it closes CRF-52 and CRF-50 together and deletes roughly 50 lines including the loose Word: value regex.

CRF-53 raised from two Notes to P3 on consistency with CRF-25. Meruem showed that deleting the sessionStorage.setItem line fails no test, because jsdom's UA is Linux and the test's items are [macOS, Windows], so detectOS returns undefined and the setter is skipped; the test's own comment claims both sub-paths are asserted. That is the same mutation-test signature CRF-25 was rated P3 for. The primary CRF-45 ask (the three UA regexes) is genuinely satisfied by the new pure tests. Chopper's untested Android exclusion is folded into the same comment as the second gap in the same suite.

CRF-55 downgraded from Gon's three P2s to one Nit. Keep-at-P2 argument, written first: this repo's own rules require comments to be substantive, redundant docstrings drift, and drifted comments have already produced four findings on this PR (CRF-4, CRF-27, CRF-50, CRF-54). Rejected because all three docstrings are accurate today, and rating accurate-but-redundant prose above CRF-50, an already-false docstring rated P3, would be incoherent. Filed as one Nit covering the pattern, with Mafu-san's and Zoro's scope-drift observations attached.

Nothing dropped. Three panel reviewers explicitly declined to re-raise CRF-50 and CRF-51 per the first-pass directive, which is the dedup working.

Body items with no code line to anchor them: the PR description's validation list still says pnpm test is 59 tests when the suite is 66 (verified), and the "Out of scope" heading-anchor bullet still says "tracked separately" without a ticket, now unaddressed across rounds 4, 5, and 7.

Event: COMMENT with dismiss: true. The round-5 REQUEST_CHANGES was motivated by CRF-42, which is verified fixed, and no P0 or P1 is open.

The dismiss failed: failed to dismiss previous REQUEST_CHANGES: get authenticated user: GET https://api.github.com/user: 403 Resource not accessible by integration. Same token-permission class as the unresolveReviewThread and resolveReviewThread failures in rounds 2, 3, and 5. Two consequences to carry into round 8. First, the round-5 CHANGES_REQUESTED is still on the PR even though its P1 is fixed, which may block merge depending on branch protection. Second, the round-7 body says "Dismissing the round-5 changes-requested," which is now inaccurate as posted; round 8 must correct that sentence explicitly rather than let it stand. This is the third distinct GitHub write the bot token cannot perform, after unresolve and resolve.

CRF-53 (P2, os-tab.test.tsx) - first-visit seeding test does not observe the seed

  • Finding (R7, P3): The first-visit render test preseeded storage, so the detectOS seeding branch was unreachable; deleting sessionStorage.setItem failed no test. Also the Android exclusion had no assertion.
  • Author fix (R8): Changed the test's items to ["Linux", "macOS", "Windows"] so jsdom's Linux UA is offered and the branch runs, and added the Android assertion. The Android half is genuinely closed.
  • Panel re-raised R8 (4 reviewers, orchestrator verified): The branch is now reachable and still unobserved. Linux is items[0], and useState(() => values[0]) makes the Linux panel active from the initial render whether or not seeding happens. The orchestrator disabled the entire seeding block with if (false && ...) and all five tests passed. Hisoka reproduced the same and showed the test does fail once items are reordered to ["macOS", "Linux", "Windows"]. Mafu-san reproduced twice, once nulling the setter and once forcing detectOS to return undefined. Komugi reached the same conclusion independently. Raised to P2 on Mafu-san's calibration that a repeated same-class mistake after correction is P2 minimum: this is the third instance of an assertion that pins a value already true by default, after CRF-25 and CRF-53's first round.
  • Dissent, overruled on evidence: Pariston asserted that "mutating out the setter now fails a test that was previously silently passing." That claim is stated as verified but was not run; four independent mutation runs contradict it. This is the second time in this review that a reviewer has restated a plausible claim without testing it, after round 5's OSTab contract claim.

Round 8

Churn guard: PROCEED (8 addressed, 0 silent). CI red on offlinedocs and required. Post-panel round: Netero advisory, Law not re-run (effective additions 4173 -> 4210, delta 37).

Panel: 16 trigger-matched plus kite and knov as wildcards (drawn with shuf from the distilled set). Eight reviewers returned no findings.

The frontmatter parser swap is the round's substantive result and it holds. parseFrontmatter now delegates to fumadocs-core/content/md/frontmatter, a shipped dependency, which closed CRF-50 and CRF-52 together and deleted the hand-rolled scanner. Orchestrator verified against emitted output: zero literal backslash-quotes anywhere in content/docs/**, the three CLI descriptions decode to real quotes, zero stray leading ---. Melody, Mafu-san, Kite, Komugi, Pariston, and Netero each re-read the emitted corpus independently and agree; Komugi additionally confirmed two clean syncs produce byte-identical output.

CRF-53 re-raised at P2 rather than closed. Detail in the Contested and acknowledged section. The short version: the fix made the seeding branch reachable but not observable, because Linux is items[0] so the asserted panel is already active by default. I disabled the whole seeding block and all five tests passed. Three reviewers reproduced the same independently; one reviewer asserted the opposite without running it.

CRF-58 (P2) is live, not dormant, and is the first finding of this review that a reader would hit on the docs homepage. normalizeAngleBrackets applies its autolink regex to the CommonMark bracketed-destination form, so [self-hosted](<https://...Self-hosting_(web_services)>) in docs/README.md:177 emits as [self-hosted]([url](url)). Verified in content/docs/index.md:178. Nami traced it through to the built HTML, where the mangled target fails the ^https?:// test in createDocsLink and is routed through the internal relative-link resolver.

CRF-59 converged from four reviewers (Netero, Meruem, Knov at P3; Hisoka at Nit) on the inherited-strictness side of the parser swap: js-yaml throws where the hand-rolled parser returned none, and nothing catches it, so a malformed block aborts the sync with a stack trace naming no file. This is the one input class the swap changed for the worse, and it is exactly the risk context.md flagged when it called a library-for-hand-rolled swap the highest-risk shape in the delta.

CRF-60 is Melody's callout-marker Note, filed at Nit. Worth recording that Melody contradicted its own round-4 claim here: round 4 said GitHub renders [!Note] as a plain blockquote and this was therefore parity; round 8 says GitHub renders it as an alert. The review cannot settle GitHub's behavior from this environment and does not need to. What is verifiable is that four corpus blockquotes ship with the marker as literal text while siblings in the same file convert, and that one of the four ([! WARNING], with a space) is a corpus typo either way.

Trust signals worth recording: both description items the review asked for across rounds 4, 5, and 7 are now closed. The validation list reads 67 tests (verified) and the heading-anchor bullet names DOCS-525.

Process observation for the body, not a finding: the round-8 nits commit discloses that the pre-commit hook was skipped, on the grounds that make gen's golden targets need a Postgres/Docker container unavailable in the workspace. Mafu-san verified the mechanical claim and that the PR's diff contains no Go, SQL, or proto files, so the skipped checks could not apply to this diff. WORKFLOWS.md says never bypass hooks and root AGENTS.md Rule #1 requires permission first, so the disclosure is the honest form of a rule violation, not compliance. Raising it to the operator rather than as a finding because the remedy is an environment decision, not a code change.

CI attribution: unresolved and explicitly not guessed. The orchestrator ran all five steps of the offlinedocs job locally at this head from a clean worktree in CI's order and every one passed with a clean tree. Komugi ruled out schedule, clock, and environment nondeterminism in the delta. gh returns 401 here so the log is unreadable. Round 1 made exactly this inference from a local proxy and was wrong, so the body asks rather than concludes.

Round 9

Churn guard: PROCEED (5 fixed, 1 accepted, 0 silent, out of 6). CI reported success at setup, though only 4 checks had reported (2 passed, 2 skipped), so the round-8 red offlinedocs job remains unexplained rather than resolved. PR state observed as open and draft. Post-panel round: Netero advisory, Law not re-run (effective additions 4210 -> 4324, delta 114).

Panel: 17 trigger-matched plus kite and knov as wildcards. Seventeen of nineteen returned no findings, the highest clean rate of the review. Netero also returned clean.

Three round-8 fixes verified by the orchestrator rather than accepted on the author's word:

  • CRF-53, by mutation. Disabling the seeding block now yields 4 pass, 1 fail, naming the first-visit test. In round 8 the same mutation passed all five. The fix works because the items were reordered so the seeded value differs from values[0], plus a direct sessionStorage.getItem assertion. Mafu-san independently ran two mutations and got the same result, and made the durability point: a future reorder that puts Linux back at items[0] silently removes the observability property that took six passes to install.
  • CRF-58, against emitted output. docs/README.md:177's bracketed destination round-trips byte-identical, and a corpus-wide scan for nested-link corruption returns zero.
  • CRF-59, by injecting a broken flow collection into a manifest-backed page. The sync now fails with reference/api/general.md: unexpected end of the stream within a flow collection under a labeled error block with a corrective action, matching the five sibling defect classes.

CRF-60 closed as accepted rather than fixed, and the disposition inverted the review's suggestion in a way worth recording. The review offered two options and leaned toward normalizing the corpus. The author kept the matcher strict, documented why, and moved the four corpus lines into two separate docs-only PRs (#28304, #28305) so they do not ride along with this migration. Netero independently ran the corpus scan and confirmed exactly four non-canonical markers, matching the author's account. The existence and content of those two PRs is the one claim this review cannot check from here.

CRF-63 (P3) is the round's only substantive finding and it is about the CRF-58 fix rather than a new defect. The fix does three things: skip bracketed URL destinations, skip bracketed email destinations, and keep rewriting plain parenthetical autolinks. The new test asserts the first. Bisky mutation-tested the other two: dropping the lookbehind from EMAIL_AUTOLINK keeps all 21 tests green, and tightening the URL lookbehind to also skip a leading paren keeps them green while silently changing docs/admin/setup/index.md:120. The orchestrator confirmed both regexes carry the lookbehind and that the parenthetical case is live in emitted output at content/docs/admin/setup/index.md:123.

CRF-64 combines Gon's three comment-duplication nits into one. Two of the three comments exist because this review asked for them (CRF-59's attribution and CRF-60's strictness note), which is worth stating plainly rather than filing three separate asks against prose we requested.

The stale round-5 CHANGES_REQUESTED is still on the PR. The bot cannot dismiss it (403 on GET /user), so it needs a human. Not retried this round.

Round 10

Churn guard: PROCEED (2 addressed, 0 silent). PR observed open and draft. Post-panel round: Netero advisory, Law not re-run (effective additions 4324 -> 4329, delta 5).

Panel sized to the delta for the first time in this review: 15 lines across three files, so 9 trigger-matched plus 2 wildcards drawn with shuf, instead of the 19 to 20 of rounds 4 through 9. Personas were dropped only where their triggers genuinely do not fire on this delta (no .tsx changed, no error path changed, no producer/consumer pairing changed, no new helper), not by judgment about what seemed worth doing. Recorded because the prior rounds' sizing was correct for their deltas and would have been disproportionate here.

All twelve reviewers plus Netero returned no findings. First round of the review with a completely clean panel.

Both round-9 findings verified by mutation rather than accepted, by three parties independently. The orchestrator, Bisky (who raised CRF-63), and Mafu-san each ran the two mutations the finding named: dropping the lookbehind from EMAIL_AUTOLINK, and broadening the URL_AUTOLINK lookbehind to also skip a leading paren. Both now fail the normalizeAngleBrackets test where both previously passed silently. Mafu-san additionally confirmed the live corpus dependency by running the current transform over docs/admin/setup/index.md:120.

CRF-64 closed. The substantive trim landed (the try/catch comment is now a pointer to the class doc that owns the rationale) and sync-docs.mjs:240-243 was correctly left alone, since its rethrow rule is content the class doc does not carry. That matches what the finding asked rather than what a literal reading would have demanded.

Event: APPROVE with dismiss: true. Zero open findings; the only entries not closed by a code change are CRF-9 (accepted with documented scope), CRF-10 (deferred to DOCS-639 with the tradeoff stated), CRF-35 (contested, closed by panel vote), and CRF-60 (accepted, corpus normalization routed to #28304 and #28305). The dismiss is expected to fail on the same 403 as rounds 7 and 9; the body states the resulting state conditionally rather than claiming the operation succeeded, which was round 7's error.

Two items leave this review unresolved and both are outside the code. The round-5 CHANGES_REQUESTED cannot be cleared by this bot. The round-8 red offlinedocs CI job was never explained; every step of it has passed locally at every head since, and gh is unreadable from here.

Description drift, mentioned once in the body and not filed: the validation list reads 67 tests, and the suite is 68 as of the round-8 commit. Mafu-san verified by two counting methods.

Final tally across 10 rounds: 64 findings. 1 P0, 1 P1, 8 P2, 17 P3, 24 Nit, 13 Note. 56 fixed in code, 4 accepted with reasoning, 1 deferred with a ticket, 1 closed contested by panel vote, 2 dropped or superseded. Three findings required a re-raise (CRF-6, CRF-53 twice). Five review claims were wrong and corrected on the record, four of them by the author.

Round log

Round 1

Netero-only (first-pass gate: P0 present, panel skipped). Law ran (effective additions 2105 > 1000): advisory vertical split. 1 P0, 2 P1, 1 P2, 6 P3, 3 Nit, 2 Note. Reviewed against 5f3b875..0b4a83c.
Orchestrator verification: independently reproduced CRF-1 (pnpm install --frozen-lockfile in offlinedocs/ fails on @types/node 22.20.1 vs 22.20.0) and found the lockfile is additionally self-inconsistent: the importer block pins prettier 3.9.4 while the only prettier package and snapshot entries are 3.9.6, so no resolution for the pinned specifier exists in the file. Confirmed CRF-5 (no cn reference in offlinedocs/src), CRF-4 (zero meta-bearing fences in docs/), and CRF-2's precondition (OSTab renders triggers itself and omits items).
Elevated two Netero Notes to P3 on consequence framing: CRF-9 (sanitizer removal is a defense removed silently in a PR framed as render parity) and CRF-10 (artifact growth deferred with no ticket, which under a no-follow-up assumption is a permanent decision no agent may accept).

Round 2

Churn guard: PROCEED (12 addressed, 2 acknowledged, 1 deferred, 0 silent, 0 contested). CI green (27 passed, 8 skipped). Law not re-run: effective additions grew 2105 -> 2600, a delta of 495, below the 500 threshold.
CRF-6 is only half addressed. The .md link guard now fails the build and the one dead link was repointed, but the image copyAsset fall-through named in the original finding is still unguarded; the author asked whether to close it. Reopened for the panel rather than closed.
Round-1 error corrected under CRF-14: the claim that the root install is not recursive was wrong. Vault entry projects/coder/offlinedocs-pnpm-workspace-mechanics.md was written on that error and has been corrected.
Netero round 2: 1 P2 (CRF-16), 1 Nit (CRF-17), plus the CRF-6 image half re-raised on its thread. Orchestrator added CRF-18 (Note) over Netero's decline: Netero's stated reason was that the current corpus is clean after the docs-side CRF-15 fix, which is the reachability dismissal the cross-check rules name explicitly. The plugin path stays reachable by any future docs author.
Orchestrator verification this round: confirmed the pnpm test CI step at .github/workflows/ci.yaml:1144, the 15-test transform.test.mjs, zero lucide imports, and the copyImage fall-through at sync-docs.mjs:197-199 against the collected-and-fail .md path at sync-docs.mjs:355-369.
Netero-only again per the pre-panel gate (P2 present). This is the second consecutive Netero-only round, so round 3 goes to the panel regardless of first-pass severity.
Tooling gap this round: deep-review post warned unresolveReviewThread: Resource not accessible by integration for both replies (CRF-6 re-raise, CRF-18). The replies posted but both threads still read Resolved in the GitHub UI, so the author may never see them. CRF-6 is named explicitly in the round-2 review body and is therefore visible; CRF-18 is not. Round 3 must name both in the body if they are still open, and must not read a Resolved thread as author acceptance for either. See journal/2026-deep-review-unresolve-permission-silent-finding.md.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First-pass review only. Everything below comes from a single pre-panel reviewer that scans for mechanical and structural defects. The full review panel has not looked at this PR yet; it will review once these are addressed. Treat this as the floor, not the ceiling.

The migration itself holds up under execution rather than reading. pnpm lint (sync + fumadocs-mdx + typegen + tsc) passes, pnpm export produces 463 pages and 466 static routes with out/index.html at the root, so the make build/coder_docs_$(VERSION).tgz contract is intact, and git status stays clean after a full export, so check_unstaged.sh holds. The rendered corpus was checked for pipeline leakage: no literal \{, [step], [!NOTE], or class="tabs" survives into any of the 466 HTML files. escapeValue in os-tab.tsx was verified byte-identical to the copy Fumadocs does not export, so that duplication is justified and the comment saying so is accurate. The dead-code sweep confirmed stubComponents is load-bearing (<children></children> appears in three docs) and lucide-react belongs in the manifest as a declared peer despite having no import.

Counts: 1 P0, 2 P1, 1 P2, 6 P3, 3 Nit, 2 Note.

The P0 blocks CI, not just review. pnpm install --frozen-lockfile in offlinedocs/ fails on this head, which means the offlinedocs job and the release target both fail before anything is built. Reproduced independently. The lockfile has a second problem the first error hides: its importer block pins prettier 3.9.4 while the only prettier package and snapshot entries in the same file are 3.9.6, so the pinned specifier has no resolution at all. That shape says the lockfile was hand-edited rather than regenerated. Regenerate it.

Two findings are about things this PR knows are wrong and chose to log instead of fail. pnpm sync prints unmapped .md links=1 and exits 0, and the dead target reaches the shipped HTML (CRF-6). The image path has the same silent-fallthrough shape and is not even counted. A pipeline that can detect a broken inter-doc link should break the build, not the reader.

On the two follow-ups the description lists as out of scope: neither has a linked ticket, and this review assumes no follow-up exists. The search index is 46 MB served uncompressed on the reader's first keystroke, inside a 571 MB bundle where docs/images is already 93 MB. That is a human decision about airgapped download size, not one an agent on either side of this review can accept as permanent. File a ticket or state the acceptance explicitly (CRF-10).

The Storybook job is red on this head and could not be attributed to this PR from inside the review environment (gh has no credentials there). What was ruled out: the root lockfile has a single . importer and CI=1 pnpm install --frozen-lockfile at the root passes on this head, so the offlinedocs lockfile drift does not explain it. Someone with log access should confirm before it is treated as background noise (CRF-14).

My favorite line from the reviewer, on remark-code-meta.ts: "dead on arrival: no code fence anywhere in docs/ carries a meta string, so this plugin can never fire."

🤖 This review was automatically generated with Coder Agents.

Comment thread offlinedocs/package.json Outdated
Comment thread offlinedocs/src/components/os-tab.tsx Outdated
Comment thread offlinedocs/scripts/lib/transform.mjs
Comment thread offlinedocs/src/mdx-plugins/remark-code-meta.ts Outdated
Comment thread offlinedocs/src/lib/cn.ts Outdated
Comment thread offlinedocs/src/lib/shared.ts Outdated
Comment thread offlinedocs/next.config.mjs Outdated
Comment thread offlinedocs/package.json Outdated
Comment thread offlinedocs/package.json
Comment thread offlinedocs/src/mdx-plugins/remark-details-accordion.ts
@nickvigilante
nickvigilante force-pushed the vigilante/docs-593-migrate-offlinedocs-to-fumadocs-phase-1-render-parity-single branch from babe71e to 9e44855 Compare August 4, 2026 19:45
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Docs preview

Check off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here.

Copy link
Copy Markdown
Contributor Author

CI note (for @nickvigilante): the offlinedocs Lint step failed on 810612f with:

src/lib/source.ts(1,22): error TS2306: File '.source/server.ts' is not a module.

That is the fumadocs-mdx generated .source/server.ts coming up empty when tsc runs, not a type error in this change. It does not reproduce locally (clean pnpm install --frozen-lockfile + pnpm lint) nor when simulating the CI generation order (empty .source from postinstall, then pnpm lint regenerates the full module and passes), and the prior commit's offlinedocs job passed on the same setup. Looks like a transient generation race. Re-running the job; flagging here per our flake protocol.

@nickvigilante nickvigilante changed the title chore(offlinedocs): migrate to Fumadocs (render parity, single version) [DOCS-593] chore: migrate offlinedocs to Fumadocs (render parity, single version) [DOCS-593] Aug 12, 2026
@nickvigilante

Copy link
Copy Markdown
Contributor Author

CI note (author FYI): the offlinedocs job flaked once on 810612f with a transient TypeScript error on a generated file: src/lib/source.ts(1,22): error TS2306: File '.source/server.ts' is not a module. This is a fumadocs-mdx .source generation flake, not a code issue. I could not reproduce it across four local runs, including the exact CI merge commit (refs/pull/27390/merge) and a fresh postinstall-then-sync sequence, and the generated server.ts exports docs correctly every time. It cleared on the next run (dd527fd has offlinedocs green). Flagging per our flake-on-changed-paths convention; no action needed unless it recurs, in which case the fix would be to force a clean .source regeneration in the lint/build scripts.

@nickvigilante

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 2, still first-pass only. This is the second round scanned by the mechanical reviewer rather than the full panel; the panel reviews next round. Treat what follows as the floor again.

Every round-1 finding got a response, which is unusual and worth saying plainly. 12 fixed in code, 2 acknowledged with reasoning, 1 deferred with a ticket (DOCS-639), none silent, none contested without engagement. The fixes were verified rather than taken on trust: pnpm lint and pnpm test both exit 0 in a fresh worktree at this head, pnpm sync reports 463 pages, 51 meta.json, and 0 unmapped links, and the recursive install that broke Storybook in round 1 is green.

Two responses deserve specific credit. On CRF-14, you pulled the log and it corrected the review: round 1 concluded "the root install is not recursive" from a passing root pnpm install --frozen-lockfile and a single-importer root lockfile. Both observations were true and the conclusion was wrong, because pnpm/action-setup with run_install: true runs pnpm recursive install, which is a different command from the one that was tested. That is the review's error, not yours. On CRF-3, the fix was not a token test file: 15 tests asserting concrete input-to-output pairs that would fail against identity transforms, wired into a new CI step at .github/workflows/ci.yaml:1144.

Three items this round: 1 P2, 1 re-raise of the unfixed half of CRF-6, 1 Nit, 1 Note.

The P2 is the same class as CRF-3, one directory over. scripts/lib/transform.mjs now has tests; src/mdx-plugins/*.ts still has none, and it is 587 lines of equally pure tree surgery. Worth noting because the decomposition analysis on this PR already flagged "the plugin suite lands with no tests" as the condition that would make a mandatory split proposal, so this is the one open lever on that.

On CRF-6 you asked whether to close the image half now. Yes, close it. This review assumes no follow-up exists, so "cannot trigger today" is the whole safety argument and it expires the first time someone renames an image. The .md half hard-fails the release; the image half ships a broken link silently. The asymmetry is the finding.

A reviewer line I enjoyed, on why rehype-step-toc-numbers.ts deserves tests: it describes the code as a "numeric-vs-string dance across the data-fd-step/dataFdStep key rename by rehype-raw."

🤖 This review was automatically generated with Coder Agents.

Comment thread offlinedocs/src/mdx-plugins/remark-coder-tabs.ts
Comment thread offlinedocs/package.json Outdated

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

Round-3 re-request. All round-2 threads are resolved. Changes since round 2 (commit 1874ea8, CI green):

  • CRF-16: added unit tests for all four remark/rehype plugins plus the shared mdast helper, colocated as *.test.ts. Node's runner cannot import TS, so the suite runs through tsx (new dev dependency) rather than vitest; pnpm test now runs both the .mjs script tests and the plugin tests (41 total, green). Exported the pure helpers under test; no behavior change.
  • CRF-17: dropped the dead lucide-react direct dependency (fumadocs-ui still carries it transitively).
  • CRF-6 (image half): extended the sync hard-fail guard to unresolved image references, verified it exits non-zero and names source -> target on a missing image.
  • CRF-8: corrected my round-1 reply and the PR reviewer note (only picomatch + js-yaml remain; the four no-op entries were already dropped in 26323fe). Pushed back with evidence on the "root install is not recursive" point: pnpm/action-setup run_install: true compiles to pnpm recursive install, which does validate offlinedocs' lockfile. Details in the thread.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review is blocked this round on one unaddressed finding, and the reason it went unaddressed is this review's tooling, not anything you did.

The unaddressed item is CRF-18 (Note), on offlinedocs/src/mdx-plugins/remark-details-accordion.ts. It was raised in round 2 as a reply on the CRF-15 thread, and the bot posting these reviews lacks the GitHub permission to unresolve a thread (unresolveReviewThread: Resource not accessible by integration). So the finding has read as Resolved and Outdated in the UI since the moment it was written, and the round-2 review body named only CRF-6. Your round-3 summary enumerates CRF-16, CRF-17, and CRF-6, which is exactly what a finding nobody could see looks like. Restating it here, where nothing can collapse it.

CRF-18, in full: CRF-15 was fixed at the corpus by hoisting the <details> block out of the list item in docs/ai-coder/ai-gateway/monitoring.md, rather than by teaching the plugin to descend into list items. Reasonable call for a render-parity PR. The residual is that <details> nested inside a list item still falls back to a native disclosure, nothing in the plugin says so, and the corpus edit that dodges it carries no explanation either. The next docs author who nests a <details> under a bullet gets a silently different rendering and no signal. One comment on the sibling-scanning loop naming the list-item case closes it; teaching the plugin to descend is more than this PR needs to carry. If you disagree that it is worth a comment, say so on the thread and it closes as contested. A one-line "not worth it" with a reason is a complete response here.

What needs to happen for the panel to run: address CRF-18, or reply explaining why it should not be fixed, or file a ticket. Any of the three unblocks it. Nothing else is outstanding.

The rest of the round is clean. CRF-16 is closed properly: unit tests for all four plugins plus the shared mdast helper, colocated, 41 tests, and tsx over Node's built-in runner instead of pulling in a second framework, which is the right instinct. CRF-6's image half is closed symmetrically with the .md guard, and you verified it by pointing a doc at a missing image and confirming the non-zero exit and the named source -> target. CRF-17 is gone. Test density on the PR is now 13.7%, from 0.0% two rounds ago.

Two credits on CRF-8. You corrected your own round-1 reply, which is rarer than it should be: the four no-op overrides were already removed in 26323fe, so "kept as a deliberate mirror" was wrong and the end state was already in place. And you pushed back on "the root install is not recursive" with evidence rather than assertion, tracing run_install: true to [{recursive: true}] in the pinned action's dist/index.js and then demonstrating the split: the non-recursive root install passes against a drifted offlinedocs lockfile, pnpm -r install --frozen-lockfile fails with Scope: all 4 projects and ERR_PNPM_OUTDATED_LOCKFILE. That was the review's error and your correction is now recorded on it.

One process note while the panel waits. The panel has still not reviewed this PR. Three rounds have been first-pass and churn checks, so nothing here should be read as approval of the design, only as the mechanical floor being clean.

🤖 This review was automatically generated with Coder Agents.

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

Round-4 re-request. CRF-18 was the only open item, addressed in b99be00.

I documented the limitation with a comment in remark-details-accordion.ts, but corrected the mechanism first: it is not list-item nesting. A <details> nested in a list item converts fine when its delimiters are clean; the original CRF-15 block failed because its close line was </details><br>, and DETAILS_CLOSE requires the closing node to end with </details>. Evidence (three parsed variants through mdast-util-from-markdown) and the reasoning are on the CRF-18 thread. The comment names the real footgun (trailing markup on the close line) and explicitly notes that list-item nesting is fine.

All review threads are resolved.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First panel review. Rounds 1 through 3 were mechanical first-pass and churn rounds, so this is the first time the design has been looked at. 19 reviewers, 22 findings: 3 P2, 8 P3, 8 Nit, 3 Note. No P0 or P1.

The migration holds up under scrutiny, and the parts that were rebuilt in response to earlier rounds hold up best. Four reviewers returned nothing at all: determinism, TypeScript modernization, React modernization, and security. That last one matters given CRF-9; a security reviewer read the raw-HTML pipeline, the path resolution in the sync, and the asset copying, and did not file. The paired-chain reviewer walked five producer-to-consumer chains end to end (plugin order against rehype-raw's passThrough list, the [step] marker from normalizeStepHeadings through remarkSteps through the TOC shim, escapeValue byte-identity across the Fumadocs boundary, manifest to route to meta.json, and the pnpm override mirror) and found each one consistent. The sync's failure output drew unprompted praise from two reviewers for naming every source -> target pair and printing a per-class fix hint before exiting non-zero; that is now the template to copy for future sync failures.

The three P2s share a shape worth naming up front: each is a silent failure with a green build.

CRF-21 is the one I would fix first. Two docs whose basenames slugify to the same segment overwrite each other in the synced output, and the summary line counts writes issued rather than files that survived. Reproduced independently: a three-route manifest containing foo-bar.md and foo_bar.md emits two files, logs pages=3, and one page's content is simply gone. A.md and a.md, or x y.md and x-y.md, do the same. The corpus is clean today and the trigger is one filename in any future PR. This sync now hard-fails on a single unresolved link; it should not stay silent about losing a whole page.

CRF-20 is a class, not an instance. transform.mjs carries two scanners, fenceScan and the blockquote-aware mapProseLines, and only half the transforms use the aware one. Three consequences were reproduced separately by two reviewers and re-verified here: a comment straddling a fence deletes the fence opener and leaves the rest of the document inside a phantom fence; an HTML comment inside a blockquoted fence is stripped as prose; and a .md link inside a blockquoted code sample gets rewritten to a route, so a reader copying the snippet gets the post-rewrite path. The invariant the module wants is that fenced content is opaque to prose transforms, and nothing enforces it.

CRF-19 is about the one line that closed a P1. The values.includes(next) clamp is the entire fix for CRF-2, it has no test, offlinedocs has no component-test harness at all, and three separate couplings to unversioned fumadocs-ui internals sit underneath it. The plugin suite went from zero tests to 41 across two rounds; the component that actually owns the runtime coupling to the framework has none.

On severity spread, since several findings drew different ratings from different reviewers: CRF-19 and CRF-20 went up from P3 to P2 on the combined consequence, CRF-26 and CRF-29 went up from Note to P3, and CRF-21 was held at P2 rather than raised. Reasoning is in each comment. Nothing was dropped or downgraded.

One process note. The "Out of scope" bullet for the heading-anchor bug says "tracked separately" without naming the ticket, while the comparable bundle-size deferral names DOCS-639. Name it, so the claim is checkable.

A reviewer line that sums up CRF-25 better than I can: "The test appraises the wrong stone: it checks that expansion happened, not that expansion is safe."

🤖 This review was automatically generated with Coder Agents.

Comment thread offlinedocs/scripts/sync-docs.mjs
Comment thread offlinedocs/scripts/lib/transform.mjs
Comment thread offlinedocs/src/components/os-tab.tsx
Comment thread offlinedocs/scripts/lib/transform.mjs
Comment thread offlinedocs/src/mdx-plugins/remark-details-accordion.ts
Comment thread offlinedocs/src/lib/shared.ts Outdated
Comment thread offlinedocs/src/components/mdx.tsx Outdated
Comment thread offlinedocs/src/components/mdx.tsx
Comment thread offlinedocs/package.json
Comment thread offlinedocs/src/mdx-plugins/rehype-step-toc-numbers.ts Outdated
@nickvigilante

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

Merged the latest main into this branch to clear the merge conflict (merge commit 041d6326b). Only offlinedocs/package.json and offlinedocs/pnpm-lock.yaml conflicted; both were resolved to the migrated Fumadocs dependency set (next 16, react 19, fumadocs 16), since main's only offlinedocs changes were bumps to packages the migration removed (lodash) or had already moved past (next 15.5.22). The offlinedocs lockfile passes a frozen install, and format:check, lint, test (59), and a full pnpm export (466 pages) all pass against the updated corpus.

Round-4 findings CRF-19 through CRF-40 were addressed in 1687ad94e; all 39 review threads are resolved.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 5. Requesting changes on one P1 that every prior round of this review missed.

First, the fixes. The round-4 response was structural rather than patch-level and it holds up under independent checks. transform.mjs now has one fence primitive and every prose transform routes through it, which five reviewers traced separately and agreed closes the class. routes.mjs and routes.test.mjs were extracted with coverage for collisions, file-vs-directory routes, manifest reorder, and duplicate entries. The jsdom harness is real: one reviewer removed the values.includes(next) clamp and confirmed the test fails, then restored it, so the PR description's claim about that test is verifiable and true. Another rendered seven before-and-after pairs through the actual unified pipeline to confirm the reworded .mdx-forward comments are accurate, including two real corpus edge cases. Six reviewers filed nothing at all, security and determinism among them.

Now the P1, CRF-42. extractTitle scans for the first H1 outside code fences and never accounts for YAML frontmatter. Every file produced by make gen opens with a frontmatter block whose second line is # Code generated by make gen. DO NOT EDIT., which matches the H1 regex. Reproduced on a fresh pnpm sync at this head: 195 of the 463 emitted pages carry title: "Code generated by make gen. DO NOT EDIT.". That is the entire REST API and CLI reference, roughly 42 percent of the corpus, and Fumadocs uses that frontmatter title for the sidebar entry, the page title, and the header. The sidebar under "REST API" and "Command Line" is 195 identical entries. Second consequence from the same root: h1Line splices out only the comment line, so the original --- delimiters survive into the emitted body and render as a horizontal rule followed by literal title: General text above the real content. The old renderer parsed frontmatter and preferred attributes.title, so this is a parity regression against the renderer being replaced.

I want to be straight about how this got here, because it reflects on the review more than on the PR. Five rounds, a 19-reviewer panel, and dozens of verified findings, and nobody read the emitted content/docs/** for the largest generated subtree in the corpus. Every pass checked transforms against synthetic inputs or against the code. The one reviewer who opened the output found it in the first place they looked.

CRF-43 is the other one worth attention. The round-4 refactor established mapOutsideInlineCode as the helper that keeps prose transforms off non-prose regions, and three transforms use it while two do not. Those two are rewriteContent and stripHtmlComments, so a .md link inside inline code gets rewritten to a route and an HTML comment inside inline code gets deleted. Both reproduced. This is the same asymmetry CRF-20 was rated P2 for, one level down, created by CRF-20's own fix, which is why it carries the same severity rather than the P3 both reviewers proposed.

CRF-35 is closed, and the record needed correcting. Your defense was that OSTab is emitted into content/docs/** and is therefore an authored-content contract. It is not: grep -rlF OSTab content/docs/ returns nothing, the generated files contain raw <div class="tabs">, and OSTab is created at MDX compile time by the remark plugin and consumed by mdx.tsx. Four reviewers checked and agreed, and I reproduced it. The disposition survives anyway, because at Nit severity with no runtime consequence a standalone rename is a fair call. Two panel reviewers repeated your premise as fact without testing it, which is a failure on our side, not yours.

Second ask on a description item: the "Out of scope" bullet still says the heading-anchor bug is "tracked separately" without naming the ticket, while the neighboring deferral names DOCS-639. This was raised in the round-4 body and is unchanged. Name it or drop the bullet.

Decomposition was re-analyzed this round because effective additions grew from 2105 to 3803. The verdict is unchanged and advisory: split vertically next time, do not block this. The reasoning is empirical rather than procedural, that each concern did in fact get per-concern scrutiny across five rounds. That analysis also retracted its own round-1 claim that remarkSteps must travel with the step transforms.

From the reviewer who found the inline-code gap, on how it eventually surfaces: "Would you like to see it fire on a page that ships? Ship one that talks about how links work."

🤖 This review was automatically generated with Coder Agents.

Comment thread offlinedocs/scripts/lib/transform.mjs
Comment thread offlinedocs/scripts/lib/transform.mjs
Comment thread offlinedocs/scripts/sync-docs.mjs
Comment thread offlinedocs/src/components/os-tab.tsx Outdated
Comment thread offlinedocs/src/components/mdx.tsx Outdated
Comment thread offlinedocs/scripts/sync-docs.mjs Outdated
Comment thread offlinedocs/src/mdx-plugins/rehype-step-toc-numbers.test.ts Outdated
Comment thread offlinedocs/scripts/lib/transform.mjs Outdated
Comment thread docs/about/contributing/documentation.md
@nickvigilante
nickvigilante force-pushed the vigilante/docs-593-migrate-offlinedocs-to-fumadocs-phase-1-render-parity-single branch from 041d632 to 6de7807 Compare August 14, 2026 17:55

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

Superseding my previous re-request: I replaced the earlier merge-commit resolution with a clean rebase onto the latest main. The branch now has linear history (no merge commit) and a tightly-scoped diff. New head is 6de7807c2.

  • Diff vs main is limited to offlinedocs/, a single .github/workflows/ci.yaml hunk, and two docs/ files. The offlinedocs/ tree is byte-identical to the previously reviewed resolution.
  • Re-validated on the rebased head: pnpm install --frozen-lockfile, pnpm lint (sync reports 0 unmapped links / 0 unresolved images), pnpm test (59/59), and pnpm export (466 pages) all pass with a clean working tree.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No review this round. The offlinedocs tree is unchanged since round 5, so there is nothing new to look at, and all nine open findings are still open.

git diff --stat 041d6326b..6de7807c2 -- offlinedocs/ is empty. The 12 changed files are docs/** prose arriving from main. That matches your own description of the rebase: the offlinedocs tree is byte-identical to the previously reviewed resolution. The rebase itself is fine and the linear history is an improvement; it just does not move any finding.

On timing, because it likely explains this. The round-5 review was submitted at 17:41:47Z and your re-request at 17:56:44Z, fifteen minutes later, and it talks only about replacing the merge commit with a rebase. My read is that the re-request was already in flight and you have not seen the round-5 review yet, rather than that these were considered and set aside. If that is right, this comment is just a pointer to it.

Still open, in priority order:

CRF-42 (P1), offlinedocs/scripts/lib/transform.mjs:384. extractTitle has no notion of YAML frontmatter, so the # Code generated by make gen. DO NOT EDIT. line inside the frontmatter block matches its H1 scan. 195 of the 463 emitted pages, the whole REST API and CLI reference, ship with that string as their Fumadocs title, which drives the sidebar entry, the page title, and the header. Same root cause leaves the original --- delimiters in the body, so those pages render a horizontal rule and a literal title: General paragraph above the content. Reproduced on a fresh pnpm sync at this head. The deleted renderer read attributes.title from frontmatter, so this is a parity regression against the thing being replaced. This one is why the review is blocked.

CRF-43 (P2), offlinedocs/scripts/lib/transform.mjs:456. rewriteContent and stripHtmlComments do not route through mapOutsideInlineCode, so a .md link inside inline code is rewritten to a route and an HTML comment inside inline code is deleted. Both reproduced.

CRF-44 (P3), offlinedocs/scripts/sync-docs.mjs:290. A manifest route with no backing file is dropped from the sidebar with no error, unlike the four defect classes the sync does fail on.

CRF-45 (P3), offlinedocs/src/components/os-tab.tsx:53. detectOS and its three UA regexes have no coverage; both jsdom tests preseed storage and skip that branch.

CRF-41 (Nit), offlinedocs/src/components/mdx.tsx:89. {...props} spreads after target and rel, so caller attributes win over the safe defaults.

CRF-46 (Nit), offlinedocs/scripts/sync-docs.mjs:147. Module-scope currentSourceRel duplicates rewriteContent's rel argument.

CRF-47 (Nit), offlinedocs/src/mdx-plugins/rehype-step-toc-numbers.test.ts:7. HastNode is still duplicated between plugin and test.

CRF-48 (Nit), offlinedocs/scripts/lib/transform.mjs:121. quoted_delim breaks the file's camelCase convention.

CRF-49 (Note), docs/about/contributing/documentation.md:36. No change requested; it records a pattern to watch and one reviewer supplied counter-evidence in your favor on both instances.

Each one needs a fix, a reply explaining why it should not be fixed, or a linked ticket. Any of the three unblocks the round. Nothing else is outstanding, and the round-4 work verified in round 5 still stands: the fence-tracker refactor, the routes extraction, and the jsdom harness were all checked independently and hold.

One detail worth flagging separately: your validation list for this head reports pnpm export at 466 pages and pnpm lint reporting 0 unmapped links and 0 unresolved images. Both are true and neither would catch CRF-42. The sync's summary line counts what it wrote, not whether what it wrote is right, which is exactly how a wrong title on 42 percent of the corpus passed five rounds of green checks.

🤖 This review was automatically generated with Coder Agents.

@nickvigilante

Copy link
Copy Markdown
Contributor Author

Re-review requested. This round addresses all 9 open findings from the round-5 review, in 0701ec07d:

  • CRF-42 (P1): added parseFrontmatter and made extractTitle frontmatter-aware, so the 195 make-gen pages take their frontmatter title instead of the make gen comment and no longer leak the original frontmatter into the body. Verified on a fresh sync: 0 of 195 make-gen pages leak (was 195); reference/api/general.md -> "General", reference/cli/ssh.md keeps its title + description.
  • CRF-43 (P2): routed rewriteContent and stripHtmlComments through mapOutsideInlineCode (new inlineCodeSpan), so .md links and HTML comments inside inline code are left verbatim. Added the missing inline-code peer assertions.
  • CRF-44 (P3): findUnbackedManifestRoutes now hard-fails the sync when a manifest route has no backing file, in the shape of the existing failure gates.
  • CRF-45 (P3): split detectOS(items, ua) + currentUserAgent() and added 3 UA-mapping tests plus a first-visit jsdom render.
  • CRF-41 / 46 / 47 / 48 (Nits): {...props} spread before href/target/rel; per-file createRewriteCtx factory; exported HastNode (imported in the test); quoted_delim -> quotedDelim.
  • CRF-49 (Note): acknowledged, no change requested.

Each inline thread carries the specifics and the resolving commit. Green on 0701ec07d: pnpm test (66 tests), pnpm lint (typecheck), and pnpm sync (463 pages, 0 unmapped links, 0 unresolved images). No prior-round fix regressed (swept the resolved findings' invariants in the touched files).

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The P1 is fixed and I verified it rather than taking the claim. Dismissing the round-5 changes-requested. One new P2, which this round introduced while fixing the P1.

CRF-42 verification: a fresh pnpm sync at this head emits 463 pages, and I scanned every one of them for a missing, empty, overlong, or DO NOT EDIT title and for a stray --- block at the top of the body. Zero real anomalies. The single hit is tutorials/best-practices/organizations.md, where the source genuinely has a thematic break under its H1, which is CRF-51 below. reference/api/general.md is title: "General", reference/cli/ssh.md is title: "ssh" with its description intact. Four panel reviewers re-read the emitted corpus separately and agree. The fix is at the root: parseFrontmatter is consulted first and the precedence chain is frontmatter title, body H1, manifest label, segment.

The rest of the round-5 work holds up too. createRewriteCtx deletes the module-scope variable rather than documenting it. findUnbackedManifestRoutes uses the same collect-then-name-then-fail shape as the four sibling defect classes instead of inventing a fifth. detectOS(items, ua) is now pure with the UA injected. Nine of nineteen reviewers filed nothing, and one ran the jsdom suite 20 times serially and 15 times in parallel looking for flake and found none.

Now the P2, CRF-52, and it is the round-5 lesson repeating one column over. This round started preferring the source's frontmatter description over the manifest, which routes three CLI pages through the new parser for the first time. That parser strips the outer quotes with value.slice(1, -1) and decodes nothing, so a YAML \" survives as a literal backslash-quote all the way into the rendered description, the <meta name="description">, and the search index. Two reviewers found it independently with byte-level evidence and I reproduced it. My own CRF-42 verification checked the title column of the emitted frontmatter and did not look at the description column immediately beside it, which is the same mistake in miniature that let the P1 live for four rounds.

The recommended fix comes from a third reviewer and is worth more than the bug report: fumadocs-core is already a direct dependency here and publicly exports content/md/frontmatter, which is js-yaml backed. It was checked against the same four corner cases your hand-rolled parser handles, including the make-gen comment block and the nested state: list. Adopting it closes CRF-52 and CRF-50 together and deletes about fifty lines, including the loose regex behind CRF-50.

On the config docstring commit that landed alongside the fixes: it is inert and cleanly disclosed, and I downgraded it from three P2s to one Nit. Worth saying plainly though, three reviewers independently flagged it as scope drift in a commit whose subject is a P1 fix. Keeping prose sweeps out of fix commits makes both easier to review.

Two description items with no code line to attach them to. The validation list says pnpm test is 59 tests; the suite is 66, verified at this head. And the "Out of scope" heading-anchor bullet still says "tracked separately" without naming a ticket, which has now gone unaddressed across rounds 4, 5, and 7 while the neighbouring deferral names DOCS-639. Name it or drop it.

From the reviewer who found the description corruption, on where this review keeps looking: "The CRF-42 verification landed on the title column of that output; the description column one over shipped corrupted anyway."

🤖 This review was automatically generated with Coder Agents.

Comment thread offlinedocs/scripts/lib/transform.mjs Outdated
Comment thread offlinedocs/scripts/lib/transform.mjs
Comment thread offlinedocs/src/components/os-tab.test.tsx Outdated
Comment thread offlinedocs/scripts/lib/transform.mjs Outdated
Comment thread offlinedocs/scripts/lib/transform.mjs Outdated
Comment thread offlinedocs/src/components/os-tab.tsx Outdated
Comment thread offlinedocs/next.config.mjs
Comment thread offlinedocs/scripts/sync-docs.mjs Outdated
@nickvigilante
nickvigilante force-pushed the vigilante/docs-593-migrate-offlinedocs-to-fumadocs-phase-1-render-parity-single branch from 0701ec0 to b8dc545 Compare August 18, 2026 20:13
@nickvigilante

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

- CRF-58: skip CommonMark angle-bracket link destinations ([text](<url>)) in the autolink rewrite so a URL containing parentheses is not corrupted into a nested [url](url). Fixes a 404 on the homepage self-hosted link.
- CRF-59: wrap the js-yaml frontmatter parse error as a labeled FrontmatterError and collect-and-name it in sync-docs, so a corpus file with invalid YAML frontmatter fails the sync naming the file instead of crashing with an opaque parser stack trace.
- CRF-61: drop redundant reactStrictMode: true (App Router default).
- CRF-62: reword a stale test comment describing the removed hand-rolled frontmatter parser.

CRF-60 (GitHub-alert case sensitivity) is deferred: it involves docs-corpus edits and a render-parity question for Nick.
…regex

The GitHub-alert MARKER regex matches only the canonical uppercase, no-space form. Add a comment recording that this is intentional, so the matcher stays aligned with GitHub's canonical syntax and non-canonical corpus markers are normalized at the source instead of accommodated in this pipeline. Surfaced by CRF-60 in the offlinedocs Fumadocs migration review.
…omments

CRF-63: add assertions that a plain parenthetical autolink (<url>) is still rewritten (the admin setup Maven link depends on it) and that the bracketed-email lookbehind holds, pinning the two CRF-58 branches the prior test left uncovered.

CRF-64: trim the try/catch comment that restated the FrontmatterError class doc, and tighten the MARKER strictness comment to lead with the rationale.
fumadocs-mdx intermittently writes an empty .source/server.ts when it regenerates over an existing .source (in CI, postinstall generates a stub, then lint regenerates after sync), which fails tsc with a spurious TS2306 'is not a module'. This is the flaky offlinedocs Lint failure seen across review rounds.

Add scripts/ensure-source.mjs after fumadocs-mdx in lint:types: it verifies the generated entry files are non-empty and regenerates up to 3 times if not, so tsc never runs against a half-written .source. It is a no-op in the normal case (a couple of stat calls). Verified locally: forcing server.ts empty triggers one regenerate and recovers; the happy path is untouched.
@nickvigilante
nickvigilante force-pushed the vigilante/docs-593-migrate-offlinedocs-to-fumadocs-phase-1-render-parity-single branch from b5c6650 to 0b48ffd Compare August 20, 2026 14:16
The empty .source/server.ts that intermittently broke tsc (TS2306) was written by next typegen, which ran after the guard. Move node scripts/ensure-source.mjs to run after next typegen and immediately before tsc, so it validates the final generated state and regenerates via fumadocs-mdx when a step leaves server.ts empty.
monitoring.md is corpus content, not migration code, so its callout/details edits belong on main rather than in this PR. Reset the file to main so the migration diff stays offlinedocs-only. This backs out the CRF-15 corpus lift; the AI Gateway possible-client-values note now renders from main's native nested <details>, which still builds and renders as a working disclosure in the static export (verified), so nothing about offlinedocs breaks. A styled accordion for that note can be handled in the renderer as a follow-up if desired.
Back out the straight-vertical-rail TOC flattening that hid Fumadocs' depth-stepped SVG rail and repainted a ::before rail while keeping the step-number badges; that combination misaligned the badges in UAT (bpmct review on PR #27390). Restoring the Fumadocs default rail lines the badges up again. Christin's separate rule that hides the trailing Next up / Next steps TOC entry is kept. The flattened-rail design is deferred for discussion in DOCS-686.
…otcha

Expand the output:export comment to name the reported symptom (the UI renders but nothing is clickable) and its cause (a stale service worker at the dev origin, and next dev erroring on the non-prerendered /serviceWorker.js under output:export), plus the recovery (unregister the service worker; the dev server runs on :26337). Gives anyone running the offline docs locally, human or agent, what they need to avoid the dead-page trap.
@untra

untra commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

re: supergraphic images for offlinedocs eg offlinedocs/public/brand/screenshot-bg-dark.png
I think if users are using the offline docs, it may typically be for airgapped deployments or deployments of limited resources. Images are heavier to bundle; I would think we want to be conscious about image graphic filesize.

I already added minified .webp light/dark supergraphics to the site directory there; maybe those should be reused?

@nickvigilante

Copy link
Copy Markdown
Contributor Author

re: supergraphic images for offlinedocs eg offlinedocs/public/brand/screenshot-bg-dark.png I think if users are using the offline docs, it may typically be for airgapped deployments or deployments of limited resources. Images are heavier to bundle; I would think we want to be conscious about image graphic filesize.

* https://github.com/coder/coder/blob/main/site/static/supergraphic/supergraphic-dark.webp

* https://github.com/coder/coder/blob/main/site/static/supergraphic/supergraphic-light.webp

I already added minified .webp light/dark supergraphics to the site directory there; maybe those should be reused?

@untra I'm inclined to agree, and I'm getting the exact statistics on package size of current vs. new pre-webp vs new with webp.

@nickvigilante

Copy link
Copy Markdown
Contributor Author

@bpmct

* **Duplicate frontmatter renders as text.**

This doesn't reproduce for me. Can you check again?

* **OS tabs are unclickable for me.**

As discussed in our 1:1, I believe this is a problem with the dev server. From Coder Agents:

Cause: output: "export" was set for every build, including next dev. In export mode Next installs a catch-all route that rejects any path generateStaticParams didn't prerender. A browser that had previously loaded the old offlinedocs still requests /serviceWorker.js (a leftover service-worker registration), plus favicon probes, etc. Those are non-prerendered paths, so next dev errored on them instead of serving normally, and the local site came up dead/non-interactive. Your boss hit it because he spun the site up on a machine/browser that had the old site's service worker registered at localhost:3000.

Already fixed in the PR by two commits:

  • 2cf1792 - gate output: "export" to production only, so next dev uses Next's normal server (clean 404s for those incidental requests).
  • b68e8ab - move the dev server off :3000 to :26337, so it no longer shares an origin with the old site's stale service worker (or a local coder server).
* **Sidebar does not scroll when a section is expanded.**

Same cause as the OS tabs issue

* **Light/dark mode is unclickable.**

Same cause as the OS tabs issue

* **TOC step numbers are misaligned.**

I backed out this part of the change, and we'll discuss this with Christin and Tracy separately, but this should now be fixed, and the "worm" is back.

Add regression coverage for the docs homepage frontmatter shape (docs/README.md: a title-less mapping with only markdown_url). parseFrontmatter must treat it as frontmatter and extractTitle must strip the whole block (frontmatterEnd covers it), with the title falling back to the body H1, so markdown_url never renders as plain text in the page. Reported in UAT as duplicate frontmatter rendering as text on the homepage; the current sync already strips it (verified in generated content and the static build), and this guards against regression.
Convert the offlinedocs supergraphic backgrounds from PNG to webp (q80): screenshot-bg-light 442KB -> 44KB and screenshot-bg-dark 298KB -> 48KB (~722KB -> ~89KB total, ~88% smaller), and point the brand.css background-image rules at the .webp files. The light background loads on every page, so this is a real per-page load win. Offline-package impact is negligible (~0.5% of the ~141MB bundle). Raised by Sam in UAT. DOCS-688.
@nickvigilante

Copy link
Copy Markdown
Contributor Author

@untra I swapped out the existing supergraphics to use your webp supergraphics, and it didn't move the needle much in terms of size, but they're there. The thing that occupies most of the increase in storage space of the docs package is the ~45 MB search index, since I want users to be able to search through the docs. The size increase is a trade-off I'm willing to make here. Let me know if you have other feedback on the rest of the offline docs.

Comment thread offlinedocs/scripts/lib/routes.mjs Outdated
nickvigilante added a commit that referenced this pull request Aug 20, 2026
… [DOCS-661] (#28180)

## What

Two small docs fixes, split out of the offlinedocs Fumadocs migration
(#27390) so that PR stays a focused tooling change and these land on
their own review track.

- `docs/about/contributing/documentation.md`: the Vale style-guide link
pointed at `docs/.style/style-guide/`, which is excluded from the
published docs corpus, so the relative link 404s on the live site.
Repointed to an absolute github.com URL.
- `docs/ai-coder/ai-gateway/monitoring.md`: lifted the "Possible client
values" `<details>` block out of the list item so it renders as a
top-level accordion, and restored a dropped blockquote continuation
marker (`>`).

## Why

Both are genuine docs bugs on their own:

- The `.style/` link is broken on the live site today, because that
directory is intentionally excluded from the published corpus.
- The `<details>` nested inside a list item renders inconsistently;
lifting it to a sibling block is portable across renderers.

They are also prerequisites for the offlinedocs Fumadocs migration,
whose stricter build-time sync hard-fails on unmapped inter-doc links
and only converts a `<details>` to an accordion when it is a list
sibling. Landing them here first lets the migration PR drop these two
files on its next rebase.

## Validation

- `make fmt/markdown` and `make lint/markdown`: clean (via
pre-commit-light).
- Docs-only change; no CI or build config touched.

> This PR was created with AI assistance (Coder Agents).
Fumadocs' ScrollArea renders its own Radix scrollbar (client-side, fades when idle). The sidebar CSS re-enables the native scrollbar for an always-visible affordance, so once the nav overflows both painted at once. Hide the Radix scrollbar in the sidebar so only the native one remains.
Rewrite the loop-and-mutate bodies of buildDirRoutes, the buildFileMap collision scan, findUnbackedManifestRoutes, minOrderUnder, and the buildMeta item construction as map/filter/reduce, dropping the local let/for/push scaffolding. Behavior is unchanged and the route/transform unit tests still pass. Higher-risk loops (manifest walk, dir model, line scanners, sync-docs I/O) are left for a follow-up.
Move the architectural narrative from routes.mjs header and section comments into scripts/lib/README.md, leaving a one-line pointer at each function. Addresses review feedback on comment density while preserving the specific rationale earlier review rounds asked to document.
Move the transform.mjs header essays (fence-scanner design, the .mdx-forward escapes, and the frontmatter/title and link-rewrite contracts) into scripts/lib/README.md, leaving terse function summaries and the short at-code notes. Same comment-density compromise already applied to routes.mjs.
Comment on lines +80 to +102
function walk(nodes) {
for (const node of nodes || []) {
const r = manifestRoute(node, dirRoutes);
if (r !== null) {
if (!manifestMeta.has(r)) {
manifestMeta.set(r, {
title: node.title,
description: node.description,
});
manifestPathByRoute.set(r, node.path);
}
if (!routeOrder.has(r)) routeOrder.set(r, order++);
}
if (node.children && node.children.length) {
const childRoutes = node.children
.map((child) => manifestRoute(child, dirRoutes))
.filter((x) => x !== null);
if (r !== null && r !== "") childOrderByDir.set(r, childRoutes);
walk(node.children);
}
}
}
walk(manifest.routes);

@untra untra Aug 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a recursive function here. this smells of bug potential.
ask your llm to review this section of code , and ask it to guard against an infinite depth. How deep should a manifest tree before it's too far?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Human me/TL,DR: you're right to take pause on recursive functions in general, but in this specific scenario, I'm confident that recursion is safe and will always terminate. The manifest it's talking about here is manifest.json, which will never have a cycle, even if a page is its own predecessor/ancestor. I've also used recursion in other SSGs (Jekyll, for example) to render sidebars.

I can add a unit test that feeds a deeply-nested manifest and asserts it still builds correctly. Let me know.


AI me: Thanks - I reviewed the manifest walk (buildManifestModel) specifically for this. Short version: recursion is safe here, and a depth guard would gate valid input against an arbitrary number rather than prevent a real failure.

Why it can't run away:

  • The input is docs/manifest.json, parsed with JSON.parse. JSON has no references, so it cannot encode a cycle - it is always a finite tree. There is no "infinite depth" state to reach; the recursion always terminates.
  • The base case is already there: walk iterates nodes || [] and only recurses when node.children?.length is truthy, so a childless node ends that branch. Depth equals the manifest's literal nesting depth and nothing else.
  • That depth is hand-authored nav nesting, a few levels (section -> subsection -> page). V8 handles on the order of 10k stack frames, so a stack overflow would need a manifest nested thousands of levels deep. This is a build-time script over a committed, trusted file, not runtime or untrusted input.

Why I did not add a maxDepth guard:

  • It would guard a state that cannot occur, and it would introduce a failure mode we do not have today: a legitimately deep but valid manifest would start failing the build at whatever threshold we pick. "How deep is too far" has no principled answer, because there is no real limit to enforce - any number is arbitrary.
  • If stack depth were the actual risk, the fix would be an explicit iterative stack, not a cap - and that reintroduces the let/.push/mutation we just moved away from, to defend against input JSON cannot produce.
  • The sync already hard-fails on the manifest and corpus problems that can actually happen: output collisions, unmapped inter-doc links, unresolved images, and unclosed HTML comments. Runaway recursion is not in that set because it cannot occur.

Your instinct is the right one for untrusted or potentially cyclic input (a user-supplied graph, or a filesystem walk that can hit a symlink loop). It just does not transfer to a static acyclic JSON tree.
If it is useful for peace of mind, I am happy to add a unit test that feeds a deeply-nested manifest and asserts it still builds correctly - that documents "deep nesting is fine" without inventing a cap. Let me know.

This response was generated with Coder Agents.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as long as it's finite, that's fine. but also know you can set a depth limit to enforce a design constraint on layout structure.

The generated root meta.json sorted the About and Support sections after
Reference, so Reference was not the last top-level section. The manifest's
top-level "About" section maps to the homepage (README, route ""), so the
about/ and support/ directories it owns through nested children are not among
the manifest's top-level routes and fell into the "ordered by own manifest
position" bucket, which sorts after every listed section.

Build the root order from the directories each top-level section introduces,
expanding only sections that own no top-level directory of their own (a
homepage-mapped or pure grouping section). about/ and support/ now land at the
About section's position and Reference stays last, and sections that merely
link into another section's directory no longer pull it out of place. Add
routes.test.mjs coverage for the homepage-mapped case, manifest-vs-disk order,
and cross-section links.

DOCS-706
aslilac pushed a commit that referenced this pull request Aug 24, 2026
… [DOCS-661] (#28180)

## What

Two small docs fixes, split out of the offlinedocs Fumadocs migration
(#27390) so that PR stays a focused tooling change and these land on
their own review track.

- `docs/about/contributing/documentation.md`: the Vale style-guide link
pointed at `docs/.style/style-guide/`, which is excluded from the
published docs corpus, so the relative link 404s on the live site.
Repointed to an absolute github.com URL.
- `docs/ai-coder/ai-gateway/monitoring.md`: lifted the "Possible client
values" `<details>` block out of the list item so it renders as a
top-level accordion, and restored a dropped blockquote continuation
marker (`>`).

## Why

Both are genuine docs bugs on their own:

- The `.style/` link is broken on the live site today, because that
directory is intentionally excluded from the published corpus.
- The `<details>` nested inside a list item renders inconsistently;
lifting it to a sibling block is portable across renderers.

They are also prerequisites for the offlinedocs Fumadocs migration,
whose stricter build-time sync hard-fails on unmapped inter-doc links
and only converts a `<details>` to an accordion when it is a list
sibling. Landing them here first lets the migration PR drop these two
files on its next rebase.

## Validation

- `make fmt/markdown` and `make lint/markdown`: clean (via
pre-commit-light).
- Docs-only change; no CI or build config touched.

> This PR was created with AI assistance (Coder Agents).
Comment on lines +7 to +24
const text = (value: string): MdastNode => ({ type: "text", value });
const para = (children: MdastNode[]): MdastNode => ({
type: "paragraph",
children,
});
const quote = (children: MdastNode[]): MdastNode => ({
type: "blockquote",
children,
});
const kids = (n: MdastNode): MdastNode[] => (n.children ?? []) as MdastNode[];
const attrs = (n: MdastNode): MdastNode[] =>
(n.attributes as MdastNode[]) ?? [];

function run(children: MdastNode[]): MdastNode {
const tree: MdastNode = { type: "root", children };
remarkGithubCallouts()(tree);
return tree;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure I love these little test helpers (especially kids() lol) since you need to grok those first before the tests make any sense. consider inlining these smaller ones at least


// Every directory route implied by the corpus (for docs/a/b/c.md: "a" and
// "a/b"). See README.md ("Directory routes and index collisions").
export function buildDirRoutes(allMd) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coming from typescript, this whole file is kind of tough to read when you don't know what allMd is. could you add jsdoc annotations to this and the other mjs files?

Comment on lines +33 to +41
* These stub components are a SECONDARY guard: if any document is ever treated
* as MDX (renamed to `.mdx`, or a future config change), the unknown tags
* resolve to an inert passthrough instead of throwing
* "Expected component `X` to be defined".
*/
const stubComponents: MDXComponents = {
children: Passthrough,
Children: Passthrough,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems a little too clever. is renaming a file to mdx a real concern? if so, would it be better to fail loudly with a more descriptive error message?

@aslilac aslilac left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this pr is entirely too large to be well reviewed

  • could this be done more granularly? does it need to all be done in one fell swoop?
  • could we start by introducing a partial "offlinedocs-next/" directory that can live independently while kinks get ironed out, then a separate pr to remove the existing next.js implementation
  • could each plugin be its own pr discussing the design and what problem it is meant to solve?

a pr this large with this much going on cannot actually be understood. there's no way to tell what change has what effect without spending days on it. there's an overwhelming amount of information buried in here and it's unreasonable to expect it to be sifted through effectively. there have already been 177 comments on this pr, and that's not even including the ones I am about to leave from this review. most of them are essay length and written by robots. that is not the sign of a healthy contribution.

Comment on lines +35 to +46
// Rewrite GitHub-style alerts (`> [!NOTE]`) into `Callout`,
// `<div class="tabs">` blocks into `Tabs`/`Tab`, and `<details>`/`<summary>`
// blocks into `Accordions`. All run before the default plugins
// (structure/TOC) and the rehype stage so downstream sees the rewritten
// tree. Tabs runs before the accordion pass so a tabs block inside a
// `<details>` is converted first, then wrapped.
// `remarkSteps` (Fumadocs core) groups consecutive `## Title [step]`
// headings into a numbered `.fd-steps` structure. The `[step]` markers are
// baked into the synced source by `normalizeStepHeadings` (refer to
// scripts/lib/transform.mjs), so no custom step plugin runs here.
// Prepending is safe: these plugins match core Markdown blockquotes and raw
// HTML nodes, independent of any default plugin.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yap, and misplaced. this isn't one big comment, this is several comments tripping over each other. there's a comment in here specifically about the remarkGithubCallouts plugin, another about remarkCoderTabs, another about remarkDetailsAccordion, etc. this could easily be broken into five comments totaling in half as many words.

Comment on lines +18 to +31
// A `<details>` block is recognized only when its opener and closer arrive as
// sibling raw-HTML nodes: DETAILS_OPEN must start the opening node and
// DETAILS_CLOSE must end the closing node. `transform` recurses into every
// parent first, so a `<details>` nested inside a list item is still converted,
// as long as those two delimiters are clean.
//
// Known limitation: a closing node that carries trailing markup on the same
// line, e.g. `</details><br>`, does not match DETAILS_CLOSE, so the block is
// left as a native `<details>` disclosure instead of an Accordion. The fallback
// still renders (the inner Markdown, including callouts, converts); it is just
// not Accordion-styled. Author `<details>` and `</details>` each alone on their
// own line to get the Accordion. A `<details>` that contains another
// `<details>` also falls back to a native disclosure. Depth-balancing is not
// attempted.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yap

Comment on lines +1 to +14
// Convert raw `<details>`/`<summary>` HTML blocks into Fumadocs `Accordions`.
//
// In the plain-Markdown (.md) pipeline, `<details>`, `<summary>...</summary>`,
// and `</details>` arrive as separate raw `html` mdast nodes with the body
// content as normal Markdown nodes in between (the same shape the corpus uses
// for `<div class="tabs">`). This plugin finds that run, lifts the summary text
// into an accordion title, and wraps the body in a `Callout`-style
// `mdxJsxFlowElement` (`Accordions` > `Accordion`) so it renders through the
// component map. Runs at the mdast level, so no per-file rewrites are needed.
//
// Each `<details>` becomes its own single-item `<Accordions type="single">`,
// which Fumadocs renders collapsible: it opens and closes exactly like the
// native element. The summary is flattened to text (any inline tags such as
// `<code>` are dropped) because the title is passed as a string attribute.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yap

Comment on lines +3 to +35
// The coder/coder corpus authors tabs as a raw HTML wrapper whose immediate
// headings are the tab labels:
//
// <div class="tabs">
//
// ### UI
// ...content...
//
// ### CLI
// ...content...
//
// </div>
//
// In the plain-Markdown (.md) pipeline those `<div ...>`/`</div>` lines arrive
// as raw `html` mdast nodes with the headings and body as normal Markdown nodes
// between them. This plugin finds each wrapper, splits the inner nodes on the
// first heading depth, and emits one tab per heading so it renders through the
// component map.
//
// On top of that base conversion it applies three authoring upgrades:
//
// * Nested tabs. Inner `<div class="tabs">` blocks are converted before the
// outer split, so a distro picker inside a "Linux" tab stays nested instead
// of being flattened into sibling tabs (e.g. install/uninstall).
// * Split bundled OS labels. A heading whose label is only operating-system
// names joined by `/`, `,`, `&`, or `and` (e.g. `Linux/macOS`) becomes one
// tab per OS, with the content duplicated, so OS detection can target a
// single operating system.
// * Grouping + OS awareness. A set whose labels are all operating systems is
// emitted as `OSTab` (shared `os` group + a user-agent default). Every
// other multi-tab set is emitted as `Tabs` with a `groupId` derived from
// its labels and `persist`, so repeated sets (CLI/UI, Docker/Kubernetes,
// ...) stay in sync and survive reloads.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yaaaaaaap

Comment on lines +1 to +17
// Restore numeric `data-fd-step` on step headings after `rehype-raw`.
//
// Fumadocs numbers steps in the table of contents through its default
// `rehypeToc` plugin, which copies a heading's step index onto the TOC entry
// only when the property is a real number:
//
// _step: typeof element.properties["data-fd-step"] === "number" ? ... : undefined
//
// `remarkSteps` sets `data-fd-step` as a number, but this site runs `rehype-raw`
// first (to keep the corpus's raw HTML). `rehype-raw` serializes and reparses
// the whole tree, which turns that number into a string and renames the hast
// key to camelCase (`dataFdStep`). By the time `rehypeToc` runs, the numeric
// check fails and the TOC loses its step numbers (the body circles are
// unaffected, since they come from the class-based `.fd-step` CSS counter).
//
// This plugin runs between `rehype-raw` and the default plugins and rewrites
// the value back to a number under the dashed key `rehypeToc` reads.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yap

Comment on lines +135 to +148
// Clamp the shared group value to an OS this set actually offers.
// Without this guard, a set that omits the stored OS (e.g. a
// macOS/Windows set when another set has stored `os = linux`) adopts
// `linux`, matches no panel, and renders an empty box - the exact
// regression that shipped once before this clamp existed.
//
// The clamp sits on three couplings to fumadocs-ui internals; a
// refactor that breaks any of them silently reintroduces the empty
// box: (1) this set's storage key (OS_GROUP) is the same key
// fumadocs' groupId store reads and writes; (2) escapeValue here
// matches fumadocs' own label escaping byte-for-byte, so `values`
// compares against the same strings; (3) fumadocs applies a stored
// group value by calling onValueChange (rather than writing its
// internal state directly), which is where this runs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yap yap yap yap yap

Comment on lines +78 to +101
/**
* OS-aware Tabs.
*
* Renders Fumadocs Tabs under a shared `os` group so every OS tab set stays in
* sync, and, on the first visit with no stored choice, selects the tab matching
* the reader's operating system.
*
* Detection is client-only. Fumadocs' Tabs reads the group store in a mount
* layout effect, so seeding `sessionStorage` during the first client render
* (never on the server) sets the initial tab without changing the hydrated
* markup, so there is no hydration mismatch. A persisted explicit choice
* (`localStorage`, written by Tabs on click) is left untouched, and if the OS
* cannot be detected or is not one of the tabs, Tabs keeps its default.
*
* OS tab sets offer different subsets of operating systems (some show all
* three, some only macOS/Windows). The `os` group value is shared across every
* set, so a value one set stores (e.g. `linux`) is applied to every other set
* too. Fumadocs only guards that in its styled `Tabs` when `items` is passed,
* which also renders a plain text tab list and would drop the OS icons. So we
* build on the unstyled container with a controlled value and clamp updates to
* the operating systems this set actually offers: an out-of-set value is
* ignored (this set keeps its default) while the shared choice stays intact for
* the sets that can honor it.
*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yaaaaaaap

Comment on lines +104 to +112
// The root's children are the top-level directories and files in the order
// their manifest section introduces them. A section is normally a top-level
// directory (get-started, reference), contributing that one segment. A
// section with no page of its own - one mapped to the homepage (About ->
// README, route "") or a pure grouping node - instead owns its directories
// through nested children (about/, support/), so those are collected from its
// subtree and land at the section's position. Only such sections expand, so a
// normal section that merely links into another's directory does not pull
// that segment out of its own place.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yap

Comment on lines +1 to +9
// jsdom environment for the offlinedocs component tests, loaded as a Node
// --import preload so the DOM globals exist before React DOM and fumadocs-ui
// are imported by any test file. Kept as plain .mjs outside src/ so it is not
// part of the TypeScript build and needs no jsdom type stubs.
//
// The component under test (OSTab) drives fumadocs-ui's Radix-based Tabs, which
// expect a handful of browser globals jsdom does not provide on its own
// (ResizeObserver, matchMedia, requestAnimationFrame, scrollIntoView). They are
// stubbed minimally: the tests assert tab-panel state, not layout or animation.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yap

Comment on lines +18 to +40
// Emit a fully static site into out/ so the docs bundle is self-contained
// and can be served by any static file host with no Node server. This is
// what the release pipeline tars into coder_docs_<version>.tgz for
// offline/airgapped use.
//
// Only export for production builds (`next build`). Under `output: export`
// Next installs an optional catch-all route that rejects any path
// generateStaticParams did not prerender. In dev that turns incidental
// requests into hard errors instead of clean 404s - most notably a stale
// /serviceWorker.js still registered at the dev origin by a prior site (a
// local coder server or the old offlinedocs, both of which used :3000),
// plus favicon probes and similar. When next dev errors on those, the page
// renders but never becomes interactive; the symptom reported in UAT was
// "nothing in the UI is clickable" (OS tabs, theme toggle, etc.). Gating
// export to production keeps next dev on Next's normal server (clean 404s),
// so local dev stays interactive, while next build still produces the full
// static export.
//
// Running the docs anywhere: if a machine shows a dead or unclickable page
// from an earlier session, a stale service worker is cached - unregister it
// (DevTools > Application > Service workers > Unregister) and hard-reload.
// The dev server also runs on :26337 (see the package.json scripts), not
// :3000, to stay off that shared origin.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yap yap yap yap yaaaaaap

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants