Skip to content

Commit 142f0f9

Browse files
committed
llm-doc: the scan stage now shares a launcher and a socket
The scan-stage section still described two launchers and a hand-rolled WebSocket, with the shared launcher as follow-up work and the deno-cri retarget as a "plausible future". Both are done, so rewrite the section around the three layers the PR discussion settled on — launcher, transport, task logic — and say which are shared and which never will be. Both the section and launch.ts's own header say "the one place quarto's CDP drivers start headless Chrome", not "the one place quarto starts headless Chrome": src/core/puppeteer.ts launches through puppeteer instead (withHeadlessBrowser, reached through withPuppeteerBrowserAndPage and inPuppeteer). Nothing outside that file enters it today, but a maintainer chasing browser-launch behaviour should not be told it doesn't exist.
1 parent d994feb commit 142f0f9

2 files changed

Lines changed: 51 additions & 25 deletions

File tree

‎llm-docs/axe-scan-architecture.md‎

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
main_commit: abc6a78ed
3-
analyzed_date: 2026-08-27
3+
analyzed_date: 2026-08-31
44
key_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
125146
an axe result, or a page that moved is an infrastructure failure in the

‎src/core/cri/launch.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
* launch.ts
33
*
4-
* The one place quarto starts headless Chrome.
4+
* The one place quarto's CDP drivers start headless Chrome.
55
*
66
* Two subsystems drive Chrome over CDP: `criClient` (src/core/cri/cri.ts,
77
* which renders mermaid diagrams) and the axe scanner
@@ -10,6 +10,11 @@
1010
* hard-won detail lives: which headless mode, how to tell the CDP endpoint is
1111
* up, and how not to orphan a process that never exits on its own.
1212
*
13+
* Not to be confused with the launch path in src/core/puppeteer.ts
14+
* (`withHeadlessBrowser`, reached through `withPuppeteerBrowserAndPage` and
15+
* `inPuppeteer`), which starts Chrome through puppeteer rather than over CDP.
16+
* Nothing outside that file enters it today, but it is a second launch path.
17+
*
1318
* Everything the two callers genuinely disagree about is passed in
1419
* (`--renderer-process-limit=1` for mermaid; `--hide-scrollbars` and a
1520
* throwaway profile for the scanner). Browser *discovery* is shared upstream

0 commit comments

Comments
 (0)