11---
22main_commit : abc6a78ed
3- analyzed_date : 2026-08-27
3+ analyzed_date : 2026-08-31
44key_files :
55 - src/command/call/axe/cmd.ts
66 - src/command/call/axe/config.ts
@@ -11,6 +11,7 @@ key_files:
1111 - src/command/call/axe/conformance.ts
1212 - src/command/call/axe/report.ts
1313 - src/command/call/axe/readme.ts
14+ - src/core/cri/launch.ts
1415---
1516
1617# Axe Scan Architecture (` quarto call axe ` )
@@ -97,29 +98,49 @@ scan.
9798
9899## The scan stage: raw CDP, fail-closed cells
99100
100- The driver is a ~ 150-line generic CDP client (` CdpClient ` in ` scan.ts ` )
101- with three capabilities: send a command and await its result, wait
102- (cancellably) for one event, close. It is transport, not scan logic — the
103- scanning is six CDP * commands* that ` scanCell ` sends through it (navigate,
104- viewport override, media emulation, evaluate-with-` awaitPromise ` , an
105- on-new-document script, the load event), and those stay the scanner's
106- responsibility under any refactor. Because the command surface is that
107- small, there is no wrapper library; puppeteer-core is the named fallback if
108- raw CDP gets painful.
109-
110- quarto-cli's existing wrapper (` src/core/cri/cri.ts ` , which drives Chrome
111- for mermaid) was read and declined — but be precise about which part: its
112- * facade* exposes mermaid's commands only (navigate/query/screenshot — no
113- emulation, no ` awaitPromise ` , no per-command timeout, so a hung ` axe.run() `
114- would hang forever). Underneath that facade sits the vendored ` deno-cri `
115- library, itself a generic send-any-command client; retargeting ` CdpClient ` 's
116- internals onto it (keeping the interface the tests stub) is the plausible
117- future unification of the * transport* . The more valuable near-term share is
118- the * launcher* : two exist in the tree (` launchScanBrowser ` here,
119- ` criClient ` 's spawn half there), ` cri.ts ` cross-refers here, and extracting
120- one shared launcher is tracked follow-up work. Browser discovery is already
121- shared: ` getBrowserExecutablePath() ` (` src/core/puppeteer.ts ` ) encodes
122- ` QUARTO_CHROMIUM ` → installed ` chrome-headless-shell ` → system Chrome/Edge.
101+ Think of a Chrome driver as three layers — launcher, transport, task logic —
102+ because the answer to "why not reuse quarto's existing one?" is different at
103+ each. The launcher and the transport are now shared with ` src/core/cri/cri.ts ` ,
104+ which drives Chrome for mermaid. The task logic never will be: mermaid's
105+ commands and the scanner's commands are different jobs.
106+
107+ ** Launcher.** ` launchChrome() ` (` src/core/cri/launch.ts ` ) is the one place
108+ quarto's CDP drivers start headless Chrome — this scanner and cri.ts.
109+ (` src/core/puppeteer.ts ` keeps a separate ` puppeteer.launch() ` path —
110+ ` withHeadlessBrowser ` , reached through ` withPuppeteerBrowserAndPage ` and
111+ ` inPuppeteer ` — which nothing outside that file enters today.) It owns the
112+ flag set, the
113+ ` QUARTO_CHROMIUM_HEADLESS_MODE ` escape hatch, stderr draining, exit cleanup,
114+ and the wait for the CDP endpoint; callers pass in only what they genuinely
115+ disagree about — ` --hide-scrollbars ` and a throwaway profile dir here (so a
116+ scan cannot attach to, or be short-circuited by, a Chrome the user already has
117+ running), ` --renderer-process-limit=1 ` for mermaid. Browser * discovery* is
118+ shared a level up again: ` getBrowserExecutablePath() `
119+ (` src/core/puppeteer.ts ` ) encodes ` QUARTO_CHROMIUM ` → installed
120+ ` chrome-headless-shell ` → system Chrome/Edge.
121+
122+ ** Transport.** ` CdpClient ` in ` scan.ts ` has three capabilities: send a command
123+ and await its result, wait (cancellably) for one event, close. The socket
124+ under it is the vendored ` deno-cri ` (` src/core/cri/deno-cri/ ` ), the same
125+ generic send-any-command client cri.ts connects with. ` CdpClient ` is the typed
126+ layer over it, and what it adds is the behaviour fail-closed cells depend on
127+ and ` deno-cri ` does not have: a dropped connection rejects every command still
128+ in flight, so a crashed tab fails one cell inside ` --timeout ` rather than
129+ hanging the scan. Because the command surface is so small there is no wrapper
130+ library on top; puppeteer-core is the named fallback if raw CDP gets painful.
131+
132+ What is still * not* reused is cri.ts's ** facade** , and it is worth being
133+ precise about why: it exposes mermaid's commands only
134+ (navigate/query/screenshot — no emulation, no ` awaitPromise ` , no per-command
135+ timeout, so a hung ` axe.run() ` would hang forever). The cleaner end state,
136+ where cri.ts * exports* a typed transport as a core surface and the scanner
137+ consumes it, waits on the scanner's own needs settling — concurrent tabs in
138+ particular.
139+
140+ ** Task logic** is the six CDP * commands* ` scanCell ` sends through the
141+ transport: navigate, viewport override, media emulation,
142+ evaluate-with-` awaitPromise ` , an on-new-document script, the load event.
143+ Those stay the scanner's responsibility under any refactor.
123144
124145** Cells fail closed.** A timeout, an evaluation error, a payload that is not
125146an axe result, or a page that moved is an infrastructure failure in the
0 commit comments