Repository navigation
Run the Mermaid browser pass and security probes #529
Description
Part of #521. Depends on #528. Read the tracker first: its §Shared contract binds this issue. Work on main.
Summary
Prove the feature in real browsers, in all seven themes and on both phone engines. Then run nine hostile inputs against the webapp and the bare package. A code change happens only to fix a failed row.
Rulings: 2, 4, 10, 14 (#521 §Maintainer rulings).
Files and interfaces
Files (gitignored, under REPO/Notes/local-docs/)
- Create:
<run date>-mermaid-ship-check.mdwith the sections## Measure and release gates,## Browser matrix,## Security probes,## Type sweep,## Quality pass,## Doc audit,## check:ci verdictand## Owed to the maintainer. Each row ischeck | PASS/FAIL | evidence. - Create:
mermaid-ship-check/browser.ts(the Playwright row runner),mermaid-ship-check/mermaid-qa.html(the pad fixture),mermaid-ship-check/types.json(one source per diagram type), and its screenshots.
Implementation steps
-
Step 1: Start clean.
bun run buildinPKG. RemoveREPO/apps/webapp/.next/cache. Runmake dev-localin the background. Store the webapp port from itsLocal: http://localhost:<port>line asAPP_URL.curl -s -o /dev/null -w '%{http_code}' "$APP_URL/"prints200.- In the test tab, unregister every service worker (
navigator.serviceWorker.getRegistrations()), then hard-refresh. - Note: dev uses Turbopack's chunk loader. The production webpack loader is checked by
offline-prod(Step 5).
-
Step 2: Fill
## Measure and release gatesby citing four results. They are the D10 verdict and thestarterrow p95 (the Measure Pele render time on a phone and set the default diagram limits #522 comment), the CJS verdict and the pack list (Register extension-mermaid as the sixth family package #527), and the D6 proof (Draw Mermaid diagrams in the pad, history and /editor #528). Run the Mermaid quality pass and ship gates #530 adds themake build-prod-cirow. -
Step 3: Write and load the fixture.
mermaid-qa.htmlholds an<h1>, 40 filler paragraphs of about 60 words each (so no diagram is near the first screen), then one<h2>per block, each followed by<pre><code class="language-…">: Flowchart (nested subgraphs), Pie (8 slices), Gantt (fixed dates, onecrittask), Sequence, Broken (flowchart TD/A --), Accessible (accTitleandaccDescr), Wide (flowchart LRwith six nodes), and Plain (language-js).- Open
$APP_URL/mermaid-qa. Check that.ProseMirror[contenteditable="true"]exists while signed out. If not, every typing row runs signed in:browser.tsusesapplyActorSession, and Chrome uses the maintainer's existing session. - Load the fixture with
Meta+aand a syntheticClipboardEvent('paste')carrying it astext/html. Expect 7.mermaid-diagramand 1 plainpre. Read the editor asdocument.querySelector('.ProseMirror').editor.
- Open
-
Step 4: Run the desktop matrix in Claude in Chrome at 1280×800, theme
docsplus. Before judging a row, checkdocument.visibilityState === 'visible'.Check Pass criterion Ready Scroll each block into view. Each valid block reaches readywith onesvg.pele; Broken reacheserror; Plain has no wrapper.Name Flowchart's SVG has aria-label="flowchart diagram". Accessible keepsaria-labelledby,aria-describedby,<title>and<desc>, and has noaria-label.Sanitize keeps attrs The root svg.pelekeepsrole,aria-roledescription,classandstyle(D8).No live regions .ProseMirrorholds no[aria-live],[role=status],[role=alert]oroutput.Undo after load Reload, scroll each block into view, wait until all 7 are readyorerror, and save the JSON.Mod-zleaves the JSON unchanged (no dispatch).Empty Deleting a block's text gives empty, an empty diagram host, and the hint "Write a Mermaid diagram" inside theprebox (screenshot).Slash /diagramand/mermaideach list one "Diagram" item. Enter inserts the starter, and a typedxlands at the end of the source. Inside Plain, typing/opens no menu (the slash menu never opens in code).Code select state In Flowchart, only "Diagram" is checked and the trigger shows the diagram icon. In Plain, only "Code block" is checked. Code select actions "Code block" on Flowchart gives a code block with language: nulland the same text; undo restores the diagram. "Diagram" in Plain changes nothing.Fold Folding Flowchart hides its frame. Unfolding shows ready, with no error line.TOC rail clone With the rail committed (sash below 240px), hover the Flowchart tick. The clone holds no .pele-zoomand no error line;svg.pelehastabindex="-1"; noidremains under.mermaid-diagram; the arrowheads draw./editorA pasted mermaid fence reaches ready(D2).Chat A mermaid fence in a chat message stays a plain code block. Pop-in Scroll to a diagram far down the pad. Record whether it pops in or the content shifts. Record only; this row does not gate. Full-doc copy and paste Meta+a. Dispatch aClipboardEvent('copy')with a freshDataTransfer, then aClipboardEvent('paste')with that sameDataTransfer(AGENTS.md:112 asks for a full-document paste check). Still 7 diagrams; no.ProseMirror pholds a default-view string;text/htmlholds no<svg.Source fold At rest only the mermaidlabel shows. Hover swaps it in place for Syntax, Copy and Hide source. Copy puts the source on the clipboard and shows a check for 2.2 s. Hide source folds it smoothly; reload keeps it folded. ArrowDown into the folded block opens it. Syntax opens the Mermaid guide in a new tab. On#historyand for a signed-out visitor of a read-only pad, blocks start folded. With reduced motion on, nothing animates.Exit and entry keys ArrowDown at the end and Mod-Enterleave the block. ArrowUp from the paragraph below lands in the last source line, and a typedxchanges the source.Themes Switch with data-themeon<html>throughdocsplus,docsplus-dark,docsplus-dark-hc,docsplus-graphite,docsplus-graphite-dark,docsplus-paperanddocsplus-paper-dark. For each, screenshot Flowchart, Pie and Broken. The frame border shows, the SVG ink reads on its background, the error line reads, and Pie keeps Pele's default colours (D4). -
Step 5: Write and run
browser.ts.- Imports:
chromium,webkitanddevicesfromREPO/packages/document-swarm/node_modules/playwright/index.mjs;applyActorSessionfromREPO/packages/document-swarm/src/actors/sessionInject.ts;readActorsFilefromREPO/packages/document-swarm/src/actors/loadActors.ts, called withREPO/packages/document-swarm/actors.json. Anullresult means "provision first": runbun run --filter @docs.plus/document-swarm provision --count 20once. - It reads
APP_URL(exit 1 when unset), runs each named row (all rows when none), gives each row a fresh context, and printsROW <name> PASS|FAIL <detail>. Timing rows measure inside the page. Apageerrorevent fails the row, and the row detail prints its message, so an app error can be told apart from a diagram failure. Any row that renders a Gantt callspage.clock.setFixedTime(Date.UTC(2026, 9, 1))before loading. - Run:
cd REPO && APP_URL=<APP_URL> bun --env-file=.env.local Notes/local-docs/mermaid-ship-check/browser.ts.
Row Setup Pass criterion type-racechromium, desktop, cold context First record the class-diagram chunk URL in a throwaway context. Hold that request with page.route(do not continue it yet). Replace Flowchart's text with `classDiagram\n A <mobilerun twice: webkit with devices['iPhone 13'], chromium withdevices['Pixel 7']html.m_mobileexists;(pointer: coarse)matches; no.pele-zoom; the page scrolls over a diagram; Wide's viewBox is taller than wide (D9);/diagram, Enter andxgive areadyblock withxat the end. Screenshots underdocsplusanddocsplus-darkpass the Themes rule.offlinechromium, cold context First, in a separate throwaway context, load once online and record the Pele chunk URL pattern. Load at the top: no request matches the pattern yet, no .mermaid-diagramintersects the viewport, and 7loadingviews. Go offline and scroll to Flowchart: "Could not load the diagram renderer." and "Try again"; oneframenavigatedonly; one[observability] mermaid:load-errorconsole line (requires an emptyNEXT_PUBLIC_GLITCHTIP_DSN). Dev runs Turbopack, which caches a failed chunk for the life of the page, so Try again recovery is not checked here:offline-prodchecks it.printScroll Flowchart and Broken into view and wait for readyanderrorfirst. ThenemulateMedia({ media: 'print' })Flowchart shows its diagram unclipped, with the prehidden. Broken shows itspre, with the view hidden. Every.pele-zoomis hidden.collabtwo chromium contexts, A and B Simultaneous typing in Flowchart keeps both texts in both tabs. When B stops on a broken edit, A shows no error at +900 ms and shows it by +1500 ms, wherever A's caret is (A1). history-restoresigned in Setup: type the Broken source into a block, wait 15 s idle, and confirm that #historylists a new version. Then#historyshowsreadydiagrams (D2), and restoring that version leaves the pad editable within 2 s.history-visitorsigned out #historyrenders the diagrams, and no Restore control exists (restore is signed-in only).typeschromium, $APP_URL/mermaid-typesEach of the 33 types from pele/dist/detect.d.ts(pele.run examples, or the Mermaid docs where pele.run has none) reachesready, orerrorwith its message printed. Save one full-page screenshot.- Production loader rows (
offline-prod). Dev uses Turbopack; production uses webpack, which retries a failed chunk. Stopmake dev-local, and startmake dev-backendin the background, so REST and the collaboration socket stay up for the pad.- Build with the Draw Mermaid diagrams in the pad, history and /editor #528
build:cicommand. Then, fromREPO/apps/webapp, runcp -r .next/static .next/standalone/apps/webapp/.next/static && cp -r public .next/standalone/apps/webapp/public, and startPORT=3100 bun run startin the background. - From
REPO:APP_URL=http://localhost:3100 bun --env-file=.env.local Notes/local-docs/mermaid-ship-check/browser.ts offline-prod.browser.tscreates this row's context withserviceWorkers: 'block', so the service worker cannot serve chunks. The row opens$APP_URL/mermaid-qa. offline-prodpasses when: offline shows "Could not load the diagram renderer."; going online and clicking Try again reachesready. Then, with Flowchart drawn, go offline and change it toclassDiagram\n A <|-- B(a type absent from the fixture):load-errorand oneframenavigatedonly; online and Try again draws the class diagram.- Under Add the extension-mermaid playground and clean-room Cypress suite #525 Result B or C, prove S5 here (and S4 under Result C) with the sabotage form across calls: back up the file, apply the change, rebuild
PKG, repeat the build, copy and start lines, and runoffline-prod. S5 must fail at Try again. S4 must fail the row, by a page reload or a page error. Restore the file, checkcmp, then rebuild both. - Afterwards stop the 3100 server and
make dev-backend, and runrm -rf REPO/apps/webapp/.nextandrm -f REPO/apps/webapp/public/sw.js REPO/apps/webapp/public/workbox-*.js REPO/apps/webapp/public/worker-*.js. Then rerun Step 1.
- Build with the Draw Mermaid diagrams in the pad, history and /editor #528
- Harness limits, recorded as limits and added to Owed: WebKit not installed (skip the iOS run; install nothing); a mobile row where
(pointer: coarse)does not match.
- Imports:
-
Step 6: Run the security probes as two more
browser.tsrows:probeson$APP_URL/mermaid-qa(withsanitizeDiagramSvg), andprobes-playgroundonhttp://127.0.0.1:5178(package defaultsanitize: null; start the playground first and stop it after). Each row sets every source as a mermaid fence and logs any request tobeacon.invalid.Probe Source H1 classDefurlflowchart LR\n A --> B\n classDef evil fill:url(https://beacon.invalid/h1),stroke:#f00\n class A evilH2 styleurl and filterflowchart LR\n A --> B\n style A fill:url(https://beacon.invalid/h2),filter:url(https://beacon.invalid/h2f)H3 @importflowchart LR\n A --> B\n classDef evil fill:#fff;@import url(https://beacon.invalid/h3)\n class A evilH4 init fontFamily%%{init: {"fontFamily": "x;}</style><script>window.__pwned=4</script><style>"}}%%\nflowchart LR\n A --> BH5 init themeCSS%%{init: {"themeCSS": "@import url(https://beacon.invalid/h5); .node{fill:url(https://beacon.invalid/h5b)}"}}%%\nflowchart LR\n A --> BH6 frontmatter config ---\nconfig:\n themeCSS: "@import url(https://beacon.invalid/h6);"\n fontFamily: "x</style><script>window.__pwned=6</script>"\n---\nflowchart LR\n A --> BH7 script text in labels flowchart LR\n A["<script>window.__pwned=7</script>"] --> B["<img src=x onerror=window.__pwned=7>"]H8 script text in a message sequenceDiagram\n A->>B: <script>window.__pwned=8</script>H9 link flowchart LR\n A --> B\n click A "javascript:window.__pwned=9"- Check through the DOM, never by regex on serialized markup.
- Pass needs: 0 requests to
beacon.invalid;window.__pwned === undefined500 ms after the last probe; no.ProseMirror scriptand no[onerror]; noa[href^="javascript:"]in a diagram host; nostyleattribute or<style>text in a diagram host matching/url\(\s*['"]?(?!#)/or/@import/i; the editor and the toolbar still visible. - Record H6's effect beside the
docs/security.mdfrontmatter claim. - Any
probesFAIL blocks the ship: record it and stop. The fix (strippingurl()insanitizeDiagramSvg) waits for the maintainer. - A
probes-playgroundFAIL thatdocs/security.mdalready names as a default risk is not a blocker. Otherwise it is a doc defect for Run the Mermaid quality pass and ship gates #530.
-
Step 7: Fix failed rows. Fix each FAIL in the file that owns the behaviour, with the smallest change. Then rebuild (Step 1), rerun that row plus
type-race, and runbun run testinPKG. A fix that needs a new option, token or a change to [Tracking] Mermaid diagrams in docs.plus through a headless Pele extension #521 §Design is a design change: record it and stop. -
Step 8: Review checkpoint. Report the matrix, the
browser.tsrows, both probe rows and the type sweep.
Decisions
- R5. Should we keep the
aria-labelfallback? Recommended: yes, unless VoiceOver and NVDA both read the type twice (aria-roledescriptionplusaria-label). The maintainer checks. Dropping it reopens Add the Mermaid renderer, default view, styles and node view #524 and Add the extension-mermaid playground and clean-room Cypress suite #525 case 1.
Acceptance criteria
- Every Step 4 row and every
browser.tsrow passes, or is a recorded harness limit. probeshas no FAIL. AnyprobesFAIL blocks the ship.
Activity
HMarzban commentedon Oct 19, 2026
Maintainer ruling, 2026-10-19
R5 is confirmed: keep the aria-label fallback. A human VoiceOver and NVDA check is still owed.
feat(webapp): draw Mermaid diagrams in the pad, history and editor
HMarzban commentedon Oct 19, 2026
Browser matrix on the final code, progress 2026-10-19
The ship-check harness ran all 29 rows, with the main log exiting 0. The webapp served the current build.
d-themes(lowest contrast 4.53), type-race, offline, print, all 33 types, the app and playground probes (no beacons, no script, nojavascript:link), the Pixel 7 row, history-restore, history-visitor, fold-initial, and offline-prod on a production build (load-error, then Try again, then ready, with no reload).collabfails at random. That is Keep a collaborator's caret in place when a remote edit lands in the same code block #532, which existed before this work; it failed 1 of 4 runs tonight.src/edit; the Step 4 desktop matrix in a visible Chrome tab; real phones; VoiceOver and NVDA.The harness
core.tsnow picks the newest installed Playwright Chromium, because the hard-coded build 1243 is gone.HMarzban commentedon Oct 21, 2026
Step 4 desktop matrix in a visible Chrome tab, 2026-10-21
The matrix ran in Claude in Chrome, the maintainer's real Chrome, signed in, against
make dev-localonhttp://localhost:3000. Padmqa-chrome-1021held the Step 3 fixture (7 diagrams and 1 plain block). The code wasmainat3784c2c65, with Pele1.7.0.document.visibilityStatewasvisiblefor every row.Deviation: the viewport was 1440×857, not 1280×800. The window did not resize, because it is maximized. Theme
docsplusunless the row says otherwise.readywith onesvg.peleeach, Brokenerror, Plain not wrapped.aria-label="flowchart diagram". Accessible keepsaria-labelledby,aria-describedby,<title>and<desc>, with noaria-label.role,aria-roledescription,classandstyle.Mod-zleft the JSON unchanged.empty, an empty host, and "Write a Mermaid diagram" inside theprebox./diagramand/mermaideach list one "Diagram". Enter, thenx:ready, caret andxat the end./inside Plain opens no menu.language: nullwith the same text, and undo restores the diagram. "Diagram" in Plain changes nothing.readywith no error line..pele-zoomand no error line,svg.pelehastabindex="-1", 0idunder.mermaid-diagram, and the arrowheads draw./editorready. (A paste into a heading becomes plain text, because a heading holds inline content only.)pre, with no diagram. The composer has nomermaidextension.pholding a default-view string, and no<svgintext/html.https://mermaid.js.org/intro/syntax-reference.htmlin a new tab. On#historyall 7 start folded, and all draw when scrolled into view. Reduced motion: read from the CSS, not toggled.(prefers-reduced-motion: reduce)setstransition: noneandanimation: noneon.mermaid-diagram *. Not run: a signed-out visitor of a read-only pad, because signing out of the maintainer's Chrome is not allowed.Mod-Enterleaves to a new paragraph. ArrowUp from below lands in the last source line, andxchanges the source.docsplus-graphite), the same as the headless run. Pie keeps Pele's default fills in every theme. Checked by eye: Pie indocsplus-dark, Broken indocsplus-graphite.Still owed on #529: real iPhone and Android, VoiceOver and NVDA, the S4 and S5 sabotage, and the signed-out read-only start-folded item above.
HMarzban commentedon Oct 21, 2026
Real iPhone and VoiceOver pass, 2026-10-21
The maintainer checked the production pad https://docs.plus/mermaid-device-check-2026-10-21 on a real iPhone, in Safari. The pad holds Flowchart, Pie, Gantt, Sequence, Accessible and Wide diagrams plus a plain
jsblock. All of them were checked in a desktop browser first, and they drew 6 of 6.All pass:
mermaidlabel shows Syntax, Copy and Hide source, and Copy shows its check.flowchart TD/A --) shows the error message, not a diagram.aria-labelfallback does not cause a double read.Still owed on #529: the same list on a real Android phone, NVDA, the S4 and S5 sabotage, and the signed-out read-only start-folded item.
HMarzban commentedon Oct 21, 2026
Real Android passes; NVDA not run, 2026-10-21
aria-labelfallback stays as ruled. NVDA stays a known gap, not a FAIL.Still owed on #529: the S4 and S5 sabotage and the signed-out read-only start-folded item. The agent runs both.
HMarzban commentedon Oct 21, 2026
S4, S5 and the signed-out read-only item, 2026-10-21
Run on a production build (
build:ci, standalone on :3100) withmake dev-backend, Pele 1.8.0, usingbrowser.ts offline-prod fold-initial.offline-prodready; the class-diagram leg passes; 0 navigations, 0 page errorsimport('pele/lazy')cached (droppeleImport = null)load-error. Every other check passes.preparerethrows the import failureloadingwith no Try again, and the harness click times out after 30 s. The issue expected a page error or a reload; the row fails either way.createRenderer.tsrestored from backup,cmpidentical, and equal toHEADSigned-out read-only item: PASS.
fold-initialsets the pad read-only in the local database. A signed-out visitor is then not editable, and all 7 blocks start folded and still draw (6ready, Brokenerror). An open pad starts open for an editable visitor. This replaces the manual Chrome check, which needed the maintainer to sign out.With the device rows above (iPhone, Android, VoiceOver; NVDA recorded as a gap), every item this issue owed is done.
HMarzban commentedon Oct 21, 2026
Closing, 2026-10-21. Every row passed or is a recorded harness limit, and the probes have no FAIL. Real iPhone, real Android and VoiceOver pass; NVDA is a recorded gap (no Windows machine). S4, S5 and the signed-out read-only item pass on a production build. Deployed to production with Pele 1.8.0 (
db3c0bf94).