Vixen
Vixen is a focused cross-platform Firefox replacement: one Flutter web renderer and GUI targeting Linux, macOS, Windows, Android, and the Apple Silicon iOS Simulator, first-class chrome-less Flutter/CDP automation, and the most web capability per byte.
The hard, spec-heavy subsystems are delegated where that keeps Vixen smaller and more correct: Stylo/selectors for CSS matching and cascade, deno_core/V8 for JS execution, html5ever for HTML, and Flutter's Paragraph/Canvas/scene/Semantics substrate. BrowserCore owns browser truth; Flutter owns bounded CSS formatting, paint, geometry, hit testing, semantic bounds, capture, chrome, and host presentation through exact mutation/commit protocols. R7 deleted the former WebRender/EGL/RGBA and Rust layout/paint path.
Linux release
Tagged releases publish the official x86_64 Flutter bundle:
curl -LO https://github.com/adonm/vixen/releases/latest/download/vixen-linux-x86_64.tar.gz
tar -xzf vixen-linux-x86_64.tar.gz
./vixen/vixen_shell
FlatPark repackages this unchanged release archive as a signed convenience Flatpak. The package remains unavailable until its registry submission is accepted, and submission/publishing is intentionally deferred until the Linux Flutter shell passes the basic-browser usability gate. Neither form implies parity with the remaining platform targets.

Start here
- Project Direction — current focus and constraints.
- Architecture — crate layout and dependency direction.
- Flutter Shell — five-platform GUI contract and gates.
- Roadmap and Milestones — current delivery and evidence.
- Historical Plan — original Linux/Relm4 phase record.
- Development — local workflow and contribution mechanics.
- Linux release and FlatPark — the official release archive and community Flatpak path.
Repository
- Source: https://github.com/adonm/vixen
- Releases: https://github.com/adonm/vixen/releases
- Docs: https://vixen.adonm.dev/
- FlatPark: https://flatpark.org/
Project direction
This is the short source of truth for product focus. Detailed implementation
notes live in the crate docs, DECISIONS.md, and code.
North star
Vixen is a focused, cross-platform Firefox replacement with one Flutter web renderer and shell on Linux, macOS, Windows, Android, and the Apple Silicon iOS Simulator, plus first-class Flutter-hosted rendered CLI/CDP automation. It is optimized for the most web capability per byte of binary and per MiB of memory.
Linux is the highest-priority GUI, integration, packaging, and release target. Product work should make the Linux Flutter browser useful and pass its native gates before equivalent platform expansion. macOS, Windows, Android, and the iOS Simulator remain committed targets, but they follow the shared contract proven on Linux rather than competing with Linux convergence for priority.
The product should feel closer to Ghostty than to a kitchen-sink browser: small, fast to build, efficient to run, easy to iterate on, and boringly reliable.
The ambition is a real browser, not a demo shell. Vixen should first make a measured corridor of everyday sites reliable, then keep widening toward ordinary Firefox-replacement use: accessible documents and applications, media, offline storage/workers, richer graphics and communications, automation, and a credible security/release lifecycle. The constraint is not lower ambition; it is refusing duplicate engines, duplicate renderers, broad unbacked API shape, and UI features that do not move the browser toward daily usefulness.
The product bet is: one small Rust BrowserCore feeding bounded render mutations to one Flutter formatter, then validating exact atomic commits, can become useful across desktop, mobile, and automation before it is universal.
Primary users
- Desktop and Android users who want a focused browser on Linux, macOS, Windows, or Android, plus developers exercising the shared GUI on iOS Simulator.
- CLI/CDP users running a chrome-less Flutter renderer for headless workflows, Playwright-style automation, and terminal-oriented apps such as https://adonm.github.io/zuko/app.html.
- Maintainers and agents using text reports to drive rapid, high-quality iteration.
Product metric
The leading metric is maximum capability for the smallest binary. When two solutions are both correct enough for the target WPT/spec surface, prefer the one with:
- smaller runtime/binary footprint,
- lower memory use,
- faster local builds,
- fewer moving parts,
- clearer text output for automation and review.
Correctness beats smallness at security/trust boundaries, data-loss boundaries, and rendering invariants. “Small” means fewer duplicate models and less framework gravity, not skipping the browser semantics users rely on.
Priority ranking
The user-facing rank is:
- CSS cascade, layout, and rendering — a Firefox replacement must draw real pages. Vixen owns CSS semantics while Flutter supplies the sole Canvas/Paragraph/scene substrate; keep the formatter WPT-driven and small.
- DOM/WebIDL/Web API runtime — modern pages need correct host APIs over
deno_core/V8. - Network/security/fetch/cookies — real browsing needs safe, fail-closed loading before breadth.
- Storage/history/session — required for real browsing and app-like sites.
- Flutter renderer and shell, Linux first — Linux is the highest-priority rendered integration and release target. Dart owns ephemeral formatting, Paragraph/Canvas scenes, geometry commits, chrome, and host-service presentation without acquiring browser state. Impeller is the required Flutter engine backend; the pinned Flutter stable SDK supplies Linux Impeller (the default desktop renderer since stable 3.47). The same proven contract then expands to the other four native targets.
- Flutter-hosted headless + CDP/Playwright-compatible seams — rendered automation and text reports are product features, not test-only scaffolding.
- WPT/imported fixture coverage and reports — correctness driver for every item above. Treat it as cross-cutting, not optional polish.
- HTML parsing/serialization — essential but mostly delegated to
html5ever; Vixen must preserve tree shape and integration semantics. - CLI ergonomics — keep commands stable, scriptable, and useful.
- Embeddable Rust API — important as an internal seam, but not a separate product until the browser is credible.
Design lessons now baked in
Recent work proved that shared fetch/storage/runtime pieces are valuable and that component sharing alone is insufficient. BrowserCore now gives Flutter, CDP, WPT, and text/rendered automation one engine-owned lifecycle. The following lessons are requirements:
- One browser state graph. Profile → browser → browsing context → document is the ownership hierarchy. BrowserCore is that owner and exposes it to JS, CDP, WPT, GUI, and automation. Parallel frontend navigation, history, runtime, permission, or profile coordinators are forbidden regressions.
- A component seam is not lifecycle integration. Sharing
Page,Network, orJsRuntimetypes is insufficient if frontends decide independently when to create, commit, cancel, persist, or destroy them. Those decisions remain in the production engine-owned lifecycle. - Asynchrony needs identity. Context, navigation, document, request, runtime, render revision/commit, and download work carries stable ids/generations. Cancellation invalidates the generation, and late work cannot mutate state, target input, publish accessibility, or emit success.
- Trust boundaries are product features. Validate URL/header/body/storage inputs near entry, fail closed, and apply response policy before exposure, execution, decode, cache insertion, persistence, download, or UI handoff.
- Automation must share the browser. CDP events, waits, DOM queries, input, and screenshots observe the same lifecycle and network/rendering paths as the GUI. Protocol shape without independent live targets is not multi-page support.
- Profiles are durable, bounded browser state. Cache, cookies, storage, history, sessions, permissions, downloads, and security state need one owner, partitioning, limits, recovery, and clear-data integration.
- Observability is an API. Stable errors and bounded privacy-minimal traces are product contracts. They distinguish policy, transport, unsupported, cancellation, stale state, and resource exhaustion without leaking content.
- Measure before budgeting; reduce before claiming. Size/performance limits need reproducible baselines. Every broad feature needs focused fixtures and a WPT path; every real-site bug becomes a reduction or an explicitly tracked unreduced failure.
- One renderer contract, platform-specific proof. Flutter is the sole web renderer and shell substrate, but each platform/ABI earns support through native BrowserCore, V8, mutation/commit/query, accessibility, host-service, package, size, and performance evidence on that target's latest stable major OS release. Framework support is not Vixen support.
- Geometry and semantics stay commit-bound. Flutter returns one atomic scene/basic-geometry/text/scroll/semantic-bound commit plus an opaque Flutter-side hit-test handle for an exact BrowserCore revision. BrowserCore authors roles, names, state, relationships, focus, policy, and actions; Dart may not infer browser meaning from pixels or retain a durable DOM.
Non-goals before alpha
- A kitchen-sink UI or clone of every Firefox chrome feature.
- WebKit fallback, runtime engine switching, or a generic JS-engine abstraction.
- A second GUI shell or fallback rendered UI outside Flutter.
- A second web-content renderer beside Flutter Canvas/Paragraph.
- Media and WebGPU before their post-v1 promotion under ADR-008; WebRTC and service workers unless promoted by an accepted roadmap/architecture change.
- Full WPT/browser parity claims before measured profiles justify them.
- A full extension ecosystem before the browser core and five-platform shell are credible.
- Site isolation/OOPIF work before the single-process browser is measured enough to know what isolation architecture is actually needed.
Alpha means
Alpha is not broad API completeness. Alpha means the architecture is frozen and validated for full delivery:
- one JS runtime target (
deno_core/V8), - one target GUI path (Flutter),
- one bounded BrowserCore mutation → Flutter atomic-commit contract,
- one Flutter Canvas/Paragraph paint path and web formatting architecture,
- one WPT/reporting workflow,
- hk-enforced git lifecycle gates,
- honest compatibility docs with measured local/imported fixture results.
Flutter alpha additionally requires the browser-scoped Rust bridge contract, bounded mutation/full-snapshot/resync and atomic-commit protocols, a Linux fake and real renderer/shell, input and viewport routing, exact-commit scene capture, and the accessibility projection shape. ADR-022 R1–R7 are landed, including synchronous geometry/cancellation/recovery and deletion of the native/Rust renderer path. R8 stabilization/rebaseline, richer gesture/DOM event input, and complete semantics/native AT behavior remain open.
Alpha also requires a production browser core: one profile service, one context registry, one generational navigation/document lifecycle, and one command/event path used by Flutter, CDP, WPT, automation, and page runtime. Two contexts must run independently while sharing only intended profile state, active navigation must be cancellable, and live DOM mutation must reach the visible render path. Narrow surfaces are acceptable; duplicate models are not.
Delivery horizon in one sentence
- Beta: a controlled real-site corridor is usable in the Linux Flutter GUI and chrome-less Flutter headless host, with measured compatibility/performance and known gaps; desktop expansion proceeds from the same renderer bridge.
- v1.0: Vixen is an honest daily-driver minimum on every platform that has passed its declared gate and a useful Playwright/CDP automation target, with security/reliability limits documented instead of hidden.
- Replacement horizon: continue through accessibility, media, offline apps, richer graphics/communications, ecosystem support, and stronger isolation until ordinary browsing, not only a curated corridor, is credible on supported targets.
After alpha, API surface can still change, but architecture changes need a new ADR and human approval.
Roadmap
This roadmap moves Vixen from its original WebRender/RGBA prototype to the full project goal: a credible Firefox replacement with one Flutter-hosted web renderer and browser shell on Linux, macOS, Windows, Android, and the Apple Silicon iOS Simulator, plus first-class rendered CLI/CDP/WPT automation through the same Flutter renderer.
Linux is the first renderer, GUI, automation, integration, packaging, and release target. The other platforms remain committed, but they reuse the BrowserCore/renderer contract proven on Linux rather than delaying it.
Product direction lives in PROJECT_DIRECTION.md, the
current architecture in ARCHITECTURE.md, accepted decisions
in DECISIONS.md, measured support in
COMPAT.md, and executable commands in
MILESTONES.md. PLAN.md is historical only.
Destination and release ladder
The stages are capability gates, not dates:
- Renderer transition — one cross-platform visual truth. BrowserCore emits bounded render mutations; Flutter commits layout, scene, geometry, hit testing, text queries, scroll state, and semantic bounds. Rendered GUI/headless/CDP/WPT use it. WebRender/EGL/RGBA and superseded Rust layout/paint are deleted.
- Alpha — one browser architecture. BrowserCore owns one profile/context/ document/runtime lifecycle; the Flutter renderer owns no browser truth; live script mutation, inspection, input, and pixels converge on exact render commits.
- Beta — a measured useful browser. A controlled real-site corridor works in the Linux GUI and chrome-less renderer with representative layout, interaction, persistence, downloads, diagnostics, accessibility, and host integration.
- v1.0 — an honest daily-driver minimum. The published corridor is reliable enough for focused daily use, release/security operations are credible, and every supported capability and platform has reproducible evidence.
- Replacement horizon — broad modern-browser capability. Media, offline applications, richer graphics/communications, extensions, accessibility, and stronger isolation widen ordinary use until “Firefox replacement” is an honest default description.
No stage implies global Firefox or WPT parity. Every compatibility claim names the profile, platform, renderer host, command, and measured result.
Current baseline and transition debt
As of 2026-07-16 the repository has:
- one eight-crate Rust workspace with hk/
justgates, stable diagnostics, fuzz targets, a fixture/WPT harness, and a committed 270 fixture / 2,027 check 100% baseline; - dependency-free renderer protocol v1 DTOs and reference validation in
vixen-apifor exact revisions, bounded source snapshots/mutations/resync, atomic commit/presented state, geometry/text/scroll queries, displayed-commit input, semantic actions, replay rejection, and explicit handle retirement; - one
BrowserCoreowner for profile services, contexts, navigation generations, DOM/Page state, V8 runtimes, history, input intent, inspection, and ordered events used by Flutter, native text utilities, CDP, and WPT; html5ever, Stylo selector/cascade integration,deno_core/V8, shared network/security policy, and bounded redb profile tables;- generation-cancellable main-document, external-script, stylesheet, and bounded PNG loading plus deadline-bounded V8/runtime-fetch cancellation;
- a useful CDP/Playwright slice and a Linux Flutter shell with native Wayland chrome, input/IME, Semantics, scrolling/find/zoom, recovery, and deterministic release/Cage evidence; and
- one Flutter renderer: R7 deleted the Rust layout/paint island, WebRender/gleam, both EGL owners, native visual headless, RGBA frame transport, Linux texture presentation, raw coordinate input, and their obsolete tests/gates.
Architecture rules for every stage
- BrowserCore owns browser truth. Profile → browser → context → document is authoritative for navigation, DOM, V8, Stylo computed styles, network/security, persistence, history, resource acceptance, events, and accessibility meaning.
- Flutter owns rendered truth. The renderer owns CSS box/anonymous trees, formatting/fragmentation, Paragraph/image measurement, paint order, clips, transforms, mechanical scroll geometry, hit testing, semantic bounds, scenes, and capture. Public Flutter scene APIs sit over required Impeller; a Skia fallback does not satisfy a Vixen rendered-platform gate.
- Mutations are not a second DOM. Dart receives bounded immutable
RenderMutationBatchdata with stable ids and exact compound revisions. It cannot mutate navigation, DOM, policy, or durable state. - Commits are atomic. One
RenderCommitpairs scene-ready layout, geometry, an opaque Flutter-side hit-test handle, text-query state, scroll state, and semantic bounds. Visible input and native accessibility name the displayed commit. - Basic geometry comes back to BrowserCore. Flutter computes it; BrowserCore validates and queries the immutable index for synchronous DOM/CSSOM/CDP operations. Paragraph-specific queries remain bounded renderer services.
- Synchronous layout is explicit. Same-task mutation followed by geometry
uses deadlock-safe, cancellable, deadline-bounded
EnsureLayout; stale approximations cannot become the permanent behavior. - One renderer after cutover. Experimental Flutter rendering is test-only until parity. Production cuts over once, then deletes WebRender/EGL/RGBA and obsolete Rust renderer ownership. No fallback renderer survives.
- Policy precedes renderer exposure. URL/CSP/CORS/mixed-content/integrity, response type, body/decode limits, and cache policy run before Flutter receives image/font/resource data.
- Every content-controlled boundary is bounded. Mutations, snapshots, strings, nodes/depth, resources, image/font bytes, fragments, queries, commits, queues, V8 work, protocol handles, and diagnostics have explicit limits.
- Generations reject late work. Navigation, runtime, resource, renderer, query, scroll, input, and semantic results cannot affect a replacement document or commit.
- Flutter supplies primitives, not CSS semantics. Flutter Flex/widgets and
packages are not accepted as CSS implementations. Vixen formatting code is
WPT-driven and uses
dart:uiParagraph/Canvas/scene primitives. - Linux proves the contract first. Framework support is not Vixen support; each platform and ABI earns native renderer, input, accessibility, lifecycle, host-service, package, size, and performance evidence under ADR-019.
Renderer transition — execute before feature breadth
Keep one active renderer slice. Only an independently critical BrowserCore security/lifecycle fix may run beside it. Do not widen native interaction, WebRender, Rust layout, text shaping, paint effects, packaging registries, or new Web API shape while it would create porting work for the renderer transition.
R0. Freeze and name ownership — landed with ADR-022
- Consolidate current decisions and remove superseded renderer/shell/layout ADRs.
- Mark WebRender, EGL, RGBA frame transport, native visual headless, and Rust layout/paint breadth as transitional.
- Make the mutation/commit/query model and aggressive deletion policy the sole current direction.
Proof: no current-direction document names WebRender or Rust layout as the
target; git diff --check, docs build, and architecture references are clean.
R1. Renderer protocol types — landed
Dependency-free, versioned, bounded DTOs in vixen-api now provide:
- compound
RenderRevisionwith context, document, source/style, viewport, and resource generations; - incremental
RenderMutationBatch, exactbase_revision, bounded full snapshot, and resync request; - stable render node/resource/fragment/commit ids;
- atomic
RenderCommitand separatePresentedacknowledgement; - immutable geometry indices, opaque Flutter-side hit-test handles, text/caret/range query DTOs, scroll snapshot/commands, semantic bounds, and truncation/limit diagnostics;
- input targets carrying displayed commit, revision, node/fragment, and finite coordinates; and
- semantic-action targets carrying document, displayed commit, semantic node, and advertised action generation.
Define limits before payload details. Prefer plain arrays/records and explicit release over a generic scene framework.
Proof: just test-api covers malformed/round-tripped ids, exact monotonic
source and viewport generations, non-finite geometry, oversized/deep snapshots,
unknown resources, atomic invalid-batch rejection, missed bases, deterministic
full-resync recovery, equal-revision idempotence, stale/late commits, separate
presentation, query correlation, bounded UTF-16 ranges, truncation policy,
scroll-command replay, forged/stale/replayed semantic actions, and explicit
opaque-handle retirement. Strict API Clippy and the all-target workspace check
pass. This is model-only evidence: no C ABI, Dart bridge, broker, or production
renderer changed.
R2. Native/Dart bridge and broker — landed
- Carry R1 DTOs through the safe Rust controller, C ABI, handwritten Dart models, and fake controller.
- Add a dedicated renderer request/response channel that the Flutter UI/renderer isolate can service while the BrowserCore command worker or V8 evaluation is waiting.
- Keep ordinary mutation/commit flow asynchronous; reserve the broker for
EnsureLayoutand bounded renderer queries. - Prohibit renderer-to-BrowserCore re-entry during layout and release every retained payload/resource explicitly.
Proof at R2: ABI/header/layout checks, Dart/Rust golden round trips, malformed and stale wire tests, cancellation/timeout tests, queue bounds, worker-blocked broker service, shutdown, and full resync. Production still displayed the old frame at that checkpoint.
Implemented evidence: the bounded RenderBroker is independent of the
serialized BrowserCore controller lock. Ordinary snapshots, every mutation
variant, and handle releases use a bounded asynchronous update queue; commits,
presentation, and resync use a separately bounded submission queue. Only
EnsureLayout, hit tests, and text queries use correlated request/response.
C renderer_poll/renderer_respond/renderer_submit/renderer_shutdown
entrypoints and handwritten Dart records are strict, versioned, and retain C
output only through the existing tokenized release contract. Total in-flight
requests remain capped after polling, update source is capped at 512 KiB before
JSON encoding, incoming messages remain capped at 64 KiB, and encoded output at
1 MiB. Timeout, late response, exact identity/kind correlation, cancellation,
queue saturation, shutdown wakeup, malformed wire, double release,
worker-blocked progress, native header, and Rust/Dart golden tests are checked
in. A small Dart service drives the formatter from the same transport; the
scripted fake enforces the same queue/payload bounds. Normal browsing still used
the old frame at R2.
R3. First Flutter-rendered document — landed test-only
Use one controlled fixture containing:
- block and inline boxes with margin/padding/background;
- mixed styled text requiring Paragraph measurement and wrapping;
- one BrowserCore-policy-accepted PNG image; and
- semantic heading/link/text descriptors.
Build the smallest Vixen Dart formatter over dart:ui, not a widget-per-DOM
adapter. Construct a Flutter scene and return one atomic commit with geometry,
an opaque Flutter-side hit-test handle, text ranges, scroll limits, and semantic
bounds.
Proof: exact-generation Impeller-backed Canvas pixels/visual hash, Paragraph line/range checks, image pixels, geometry index, renderer hit tests, Semantics bounds, scene capture, mutation update, stale rejection, and full resync. This path remains test-only.
Implemented evidence: just test-flutter-formatter-impeller drives one
immutable snapshot through a small flow formatter over dart:ui Paragraph,
Canvas/Picture, encoded PNG decode, Scene capture, geometry, reverse-paint-order
hit testing, UTF-16 range/point queries, scroll limits, semantic bounds, mutation,
presentation, explicit idempotent handle release, stale/equal-snapshot rejection,
deterministic resync, and reset. Candidate source/scene state publishes only
after successful formatting and bounded commit submission; failed submissions or
superseded asynchronous builds retain the previous revision and dispose their
Paragraph/image/Picture resources. Mixed
text runs have run/line fragments, padded boxes retain distinct content bounds,
and wrapped semantic text retains all Paragraph rectangles.
Software and Impeller-requested captures have separate exact raw-RGBA hashes.
The formatter remained test-only through R3; the bounded production vertical
below now reuses it without claiming the rest of R4.
R4. One interactive commit vertical — landed
Route one controlled Linux document through the new renderer for:
- displayed-commit pointer targeting and DOM click;
- wheel/key/script scroll intent,
preventDefault(), renderer clamp, returned scroll commit, and DOMscrolleffect; - find match and caret/range geometry from Paragraph;
- page zoom/viewport change as a new revision;
- BrowserCore semantic meaning combined with renderer bounds; and
- lifecycle hide/resume with stale scene/commit suppression.
Proof: widget/core/ABI tests plus a Cage interaction smoke. Every assertion names one commit id. The old texture path remains production-only comparison and is not widened.
Implemented evidence: the native Linux shell now requests
one bounded BrowserCore projection for the selected document, carries it over the
dedicated renderer update queue, formats it with the R3 service, validates the
returned commit in Rust, and paints the accepted RenderCommitPainter view.
At R4 completion the source was deliberately a basic title plus at most 64
non-hidden semantic elements (or bounded body-text fallback), not a claim of
computed-style or general CSS rendering. The R5 source checkpoint below has now
replaced that temporary projection.
Presentation is acknowledged only from a Flutter post-frame callback. Pointer input uses the formatter's displayed commit, opaque hit-test handle, exact revision, fragment, viewport point, and local point; Rust validates all of them, resolves text hits to the nearest BrowserCore semantic element, and only then dispatches the DOM event. Snapshot replacement, submissions, releases, and queues stay bounded; consuming a submission and publishing all resulting handle releases is atomic. At R4 the WebRender/RGBA texture was still the explicit fallback; R7 deleted it.
All six R4 behavior slices now cross the production seam:
- renderer-targeted down/up input synthesizes a real DOM
clickon the exact displayed commit in the native ABI smoke; - find results, highlight boxes, and endpoint carets come from commit-bound Paragraph UTF-16 geometry rather than the transitional layout;
- page zoom and physical viewport changes produce, accept, and present newer revisions/commit ids while retiring old handles;
- BrowserCore semantic descriptors use Flutter-computed bounds, and advertised tap/focus/value/range actions are suppressed unless the same commit and accessibility generation are still displayed; and
- lifecycle generations clear hidden presentation, reject late hidden work, and require a newer commit before resume while bounding acknowledgement retries.
- BrowserCore snapshots carry the accepted root offset and extent only after its
cancelable wheel/key/script policy runs. The formatter independently clamps
that intent, translates pixels, geometry, Paragraph queries, hit testing, and
semantic bounds together, and returns the offset in a newer atomic commit.
Canceled wheel input leaves the offset unchanged; the native ABI smoke covers
script, wheel cancellation/default, and key commits while the release-process
Cage interaction smoke correlates DOM effects with exact presented commit ids.
mousedownno longer publishes a replacement source before its matchingmouseup, and input is suppressed during source/commit transition windows, so strict stale validation remains enabled without breaking click synthesis.
Computed styled nodes/resources, nested Flutter scroll nodes, and DOM/script mutation batches remain broader renderer-transition work; deletion of the fallback remains R7.
R5. Chrome-less Flutter automation host
- Add a minimal Flutter entrypoint that opens an exact viewport without browser chrome, drives the same BrowserCore/renderer bridge, and captures an exact presented commit.
- Run it under Cage/wlroots headless Wayland on Linux.
- Move visual hashes, layout-box evidence, screenshots, and CDP screenshot/input workflows to it in coherent groups. Keep text-only native tests only where they require no pixels or geometry.
- Retire
display-list-containsand migrate its three assertions in two fixtures to commit-bound layout/pixel evidence before claiming the full manifest; do not recreate a Flutter display-list dump compatibility API. - Preserve independent contexts/targets and bounded startup/shutdown behavior.
Proof: fixture manifest through the Flutter host, external Playwright smoke, multiple target viewports, input, before/after script capture, renderer loss, and no compositor/chrome pixels in page screenshots.
First implemented checkpoint: the release bundle now runtime-selects a
page-only Dart host with an undecorated Linux runner window. One strict
--vixen-automation invocation requires an absolute file/HTTP(S) URL, a viewport
within the existing 4,096-pixel/64-MiB bounds, and an absolute bounded .png
output path. It bypasses profile tab restore/save and browser/frame fallback
capture, paints the accepted formatter view without browser widgets, then only
after a Flutter frame acknowledges and captures the exact still-presented commit
through Scene.toImage. Startup/capture is bounded to 60 seconds; successful
work closes the sole BrowserCore, while shutdown gets a five-second grace before
the process fails closed. just linux-automation-smoke launches that same
release/AOT bundle under Cage twice at 320×240 and 480×300 with fresh profiles;
it checks Impeller and exact commit diagnostics, strict PNG structure/dimensions,
RGBA scene pixels, and pinned full-scene hashes. Because capture serializes the
formatter scene rather than the Flutter or compositor surface, browser,
runner, and compositor chrome cannot enter the PNG. Dart tests cover
configuration rejection, legacy-capture suppression, exact presentation identity,
PNG encoding, and output bounds. At that checkpoint this did not yet satisfy
full R5: fixture
manifest, layout evidence, CDP/Playwright screenshot and input routing,
independent simultaneous targets, before/after mutation capture, and renderer
loss remain to migrate.
Renderer-source checkpoint: BrowserCore now publishes the bounded renderable
DOM tree rather than synthetic title/semantic wrappers. Element ids are the
stable BrowserCore node ids; renderer-only text ids occupy a disjoint range;
parent/sibling/depth topology, viewport-resolved Stylo properties, accepted PNG
resources, semantic descriptors, and root scroll intent travel in one validated
FullRenderSnapshot. Metadata/script/style subtrees are counted for stable DOM
ids but excluded from renderer payload and paint. The Dart formatter consumes
authored dimensions, per-side margin/padding, background colors, visibility,
image sizing, and page zoom while preserving exact commit input validation. The
release Cage hashes now cover actual fixtures/dom/basic.html DOM text rather
than the former synthetic document card. This establishes the source needed by
the remaining manifest/CDP migration; it does not by itself satisfy the proof
paragraph above.
Shared-core CDP checkpoint: CDP protocol ownership now lives in the reusable
vixen-cdp adapter. BrowserCore can create independent bounded event
subscriptions without cloning lifecycle ownership, so the long-lived release
Flutter host runs the listener against its sole BrowserCore. Rendered CDP
screenshots publish a target-specific full snapshot, wait for its exact Flutter
commit and Presented acknowledgement, then return bounded raw PNG bytes from
the displayed scene. DOM.getContentQuads/DOM.getBoxModel and mouse input use
Flutter commit geometry/hit testing in this mode. At that checkpoint native CDP
retained a comparison backend; R7 later deleted it. just flutter-cdp-playwright-smoke proves
320×240 and 480×300 targets alive together, target isolation, Flutter-routed
input, before/after mutation pixels, target switching, no chrome pixels, and a
forced renderer reset followed by byte-identical full-resync capture. The old
display-list-contains manifest check is removed; its three assertions now use
computed-style together with existing layout/pixel evidence. At that checkpoint,
full fixture-manifest routing was the last R5 migration item.
R5 complete: the Dart formatter now implements the bounded fixture slice of
content-box/border-box block and inline flow, relative/absolute positioning,
row/column/reverse flex sizing, fixed/fractional/minmax grid tracks, gaps,
deterministic text line geometry, backgrounds, borders, and images. just flutter-fixture-manifest starts one release/AOT Flutter host under Cage and runs
all 270 fixtures / 2,027 checks in manifest order. Every fixture uses a fresh
target in the host's sole BrowserCore, so script/style mutations and rendered
assertions share one document/runtime lifecycle. The 1,868 native-safe
document/runtime checks use typed BrowserCore inspection; 19 flutter-js-eval,
104 layout-box, 25 visual-hash, and 11 ref-equivalent checks use exact
presented Flutter commits. Reference
checks compare direct RGBA scene pixels, visual baselines now name Flutter
scenes, and the native runner is text/runtime-only. just gate-r5 composes this
manifest with the one-shot and external Playwright gates. R6 synchronous layout
and R7 cutover/deletion are now also complete; R8 stabilization is next.
R6. Synchronous layout and recovery gate — landed
Implement:
- DOM mutation → Stylo flush → mutation batch →
EnsureLayout→ matching commit → synchronous geometry answer; - repeated/batched geometry reads without repeated layout;
- cancellation by navigate/stop/close/shutdown;
- renderer timeout, crash/loss, malformed commit, resource eviction, missed revision, and bounded full-resync recovery; and
- no BrowserCore mutex held during wait, no Dart re-entry, no late commit, and no poisoned next request.
Proof: same-task style/DOM mutation plus getBoundingClientRect(), Range and
caret queries, forced races/timeouts, isolate reuse, and GUI/CDP agreement on the
same commit.
Implemented evidence: BrowserCore page realms now share the one authoritative
Page with a synchronous geometry host. A geometry read drains the task's
bounded DOM mutation sink, refreshes the Page cascade, diffs the previous exact
renderer source into a RenderMutationBatch (or publishes a full snapshot for
first load/resync), and waits on the dedicated broker without holding the C
controller or renderer-state mutex. The response is accepted only after its
matching asynchronous commit submission validates against the same replica.
Repeated element reads reuse that commit; Range boxes and collapsed caret
rectangles use commit-bound batched Paragraph text queries.
Navigation, stop, close, shutdown, and the V8 deadline carry explicit renderer
cancellation while the normal GUI keeps a separate bounded UI-isolate broker
pump alive even when its browser command worker is blocked. Late replies are
unknown/inert. One bounded retry sends a full snapshot after renderer resync,
timeout, malformed commit, or missed state; a non-finite malformed submission is
consumed and retired without poisoning the next request. Focused tests prove
same-task style mutation plus two reused element reads, Range and caret geometry,
exact source batches, renderer-reset full resync, navigation/stop races, late
reply rejection, malformed-commit recovery, and same-isolate reuse. just test-r6 runs the focused Rust/Dart gate; just gate-r6 composes it with all R5
rendered fixture/CDP/Cage evidence.
R7. Production cutover and aggressive deletion
R7 cut over after R3–R6 were green and removed in one reviewed migration series:
webrender,gleam,GlContext, native renderer integration, and WebRender image upload;- native-headless and FFI frame EGL implementations;
- RGBA frame ABI/tokens/pools, Dart frame worker, Linux pixel-buffer texture plugin/presenter, and texture recovery tests;
- Rust display-list/paint modules and formatting/layout code not explicitly reused by the Dart formatter;
- obsolete visual/layout tests, gates, docs, dependencies, fixtures, and CLI flags rather than preserving compatibility shims; and
- duplicated scale, hit-test, scroll, text-metric, and semantic-bound projections.
Use source search and dependency gates to prove absence. Do not retain dead APIs for hypothetical embedders.
Proof: one Flutter renderer in dependency/source scans; no WebRender/EGL/frame transport; GUI and chrome-less host share mutation/commit code; all supported layout/pixel/input/semantics/CDP evidence uses it.
Landed: production GUI and automation always paint Flutter commits. The
WebRender/gleam dependency graph, GlContext, both EGL implementations, native
visual headless, screenshot/incremental CLI flags, RGBA C/Dart transport, Linux
texture path, Rust layout/display-list/paint and paint-helper modules,
PaintSnapshot, Page hit testing/geometry/semantic bounds, raw coordinate-input
ABI, native rendered WPT/CDP checks, and obsolete Phase 4/5 gates are deleted.
flutter-js-eval makes renderer-dependent manifest checks explicit. just test-r7 proves source/dependency absence and both native/Flutter surfaces;
just gate-r7 composes all R5/R6 rendered evidence.
R8. Linux stabilization and rebaseline
- Reproduce the compatibility manifest and imported profiles through appropriate
native or Flutter-hosted paths; update
COMPAT.mdonly from output. - Re-run Linux interaction, IME, AT-SPI, release archive, startup, memory, frame, screenshot latency, and profile-growth evidence.
- Rebaseline hello-Flutter versus Flutter+Vixen and attribute removed WebRender/EGL/frame code, new Dart formatter, and chrome-less-host costs.
- Fix renderer-transition regressions before broadening APIs or resuming FlatPark publication work.
Compatibility reproduction checkpoint: on clean revision e224bf6, just compat-report reproduced all 270 fixtures and all 1,868 native-safe BrowserCore
checks at 100%. The post-R7/Yaru release/AOT Flutter host subsequently reproduced
the full 270 fixtures / 2,027 checks at 100%, including 19
flutter-js-eval checks plus 104 exact layout boxes, 25 visual hashes, and 11
exact-pixel references. Renderer evidence is kept separate from, not inferred
from, the native run.
The matching external Playwright/CDP rerun is also green: two target viewports, Flutter-routed geometry/input, before/after mutation captures, target switching, and forced renderer reset/full-resync all retained exact scene identity.
Renderer/frame/GPU measurement checkpoint:
just baseline-flutter-linux now measures the release/AOT CDP host from process
spawn through exact capture, then joins eight direct mutations and one mouse
release to exact presented Flutter commits and engine frame timings. Clean
five-run/one-warmup version-2 references contain 45 interaction frames each.
Mesa software records 15.402 ms median mutation → commit-frame, 26.364 ms mouse
release → commit-frame, and 2,587 µs exact-frame total span; the corresponding
AMD Ryzen 7 7700X integrated-GPU/radeonsi/Mesa 26.0.4 run records 14.527 ms,
25.269 ms, and 2,590 µs. Renderer-specific exact PNGs repeated in every sample
and all processes exited cleanly. Cage reported no refresh rate, and Flutter
raster finish is not compositor scanout. These are checked-in measurement-only
single-host observations, not budgets, animation stability, physical-input
latency, isolated Flutter/GPU attribution, or a supported GPU matrix.
First size/release checkpoint: clean, equally stripped Flutter 3.47 hello and post-R7/Yaru Vixen release bundles now have a checked-in component report. The 85,377,960-byte Vixen bundle is 131,560 bytes smaller than the historical pre-R7 bundle despite adding Yaru assets/plugins; its aggregate native library is 2,076,976 bytes smaller. The hello control also shrank, so the current 63,979,292-byte Vixen-minus-hello delta is larger and is not misreported as a product regression. The same Vixen bundle produces a deterministic 31,913,890-byte archive; clean extraction and a bounded Cage launch reported Impeller and presented an exact Flutter commit. These are unreproduced measurements and one controlled launch, not budgets, sustained release evidence, or FlatPark install evidence.
Profile-growth checkpoint: a clean five-repeated/five-unique-visit run kept the opaque profile's logical size constant, added 8,192 allocated bytes across repeated visits and zero across unique visits, then added 139,264 bytes for a 65,536-byte localStorage payload that a fresh process reopened successfully. This is a checked-in single-host measurement, not a growth budget or broad history/cache workload.
Native interaction/accessibility checkpoint: R8's final gate passed on
2026-07-17. An unchanged Fedora ibus-mozc/mozc
2.29.5111.102-16.fc43 pair ran from a workspace-local extraction under a private
IBus daemon; a user-namespace bind supplied its compiled /usr/libexec path
without changing host packages. The release/AOT Cage run observed real GTK
preedit start/update/end and commits in both the native input and
contenteditable controls. A narrowly scoped Linux-runner guard terminates
Flutter 3.47's recursive Component.get_extents walk at its non-component
FlViewAccessible root; descendant bounds remain Flutter-authored. The same run
then observed the editor as text/editable/visible/showing with positive bounds
(8, 187, 40, 20), invoked Flutter's unchanged native Focus action, reached
DOM focus=editor, and advanced the same document from commit 18 to 20. The
complete interaction corridor continued through IME, wheel ownership and
cancellation, script/root scroll, navigation stop/recovery, keyboard input, and
clean app exit (commits=3>31>34>40>45). just linux-at-spi-smoke separately
passed the process-filtered name gate. This closes R8; it is one controlled
Linux/IBus/Mozc/AT-SPI proof, not an IME, assistive-technology, compositor, or
device matrix.
GTK4 toolchain migration checkpoint: on 2026-07-18 the release runner moved
to the immutable flutter-dev 328b829d35 SDK, Dart 3.14.0-28.0.dev, and
libflutter_linux_gtk4.so. The GTK3 ATK guard and GTK3-only Yaru/window plugins
were removed. Fresh GTK4 evidence observes BrowserCore names, text role,
editable/visible/showing states, and positive local (0, 0, 40, 20) bounds,
while /proc proves GTK4 is loaded and GTK3 is not. The deterministic
headless-window interaction run advances atspi=21>24 and
commits=3>37>40>46>51 through native IME and pointer input. The pinned GTK4
engine does not expose AT-SPI Action or transformed screen-coordinate origins;
those old GTK3 properties remain historical evidence, not current GTK4 claims.
Exit: the controlled Linux corridor uses no transitional renderer component, all renderer failure modes are bounded, and the next compatibility failure can be reduced directly against the final architecture.
Alpha — converge live browser state on render commits
R8 and A1 are complete. Continue shared-core convergence in this order without reintroducing native renderer ownership or weakening the landed host gates.
A1. Live document/runtime convergence
Status: complete (2026-07-17). The mutable surface Vixen currently claims is live and Page-backed. Bounded op snapshots remain transport read models; they no longer stand in for mutable host-object ownership. APIs outside the claimed subset fail explicitly rather than presenting plausible inert behavior.
- Replace remaining Page/runtime compatibility snapshots with live Node/Element/Document, CSSOM, events, focus, selection, forms, history, and storage resources.
- Make every relevant mutation produce one render-source revision and invalidate accepted geometry explicitly.
- Execute parser classic/module scripts with document event-loop and microtask ordering; preserve realm teardown and same-origin frame boundaries.
- Delete plausible inert compatibility shims as real owners land.
First A1 checkpoint: HTMLElement.dataset is now one stable live
DOMStringMap per element instead of a frozen property projection. External
attribute changes reflect into the retained object; property assignment/deletion
uses the shared Rust name conversion and the normal DOM mutation path. Focused
runtime proof requires exactly one render-source generation per write and Stylo
attribute-selector recascade. The release/AOT Playwright smoke then performs one
dataset write, observes 140×32 geometry synchronously in that task, reads the
same attribute/node/geometry through CDP, and pins different before/after exact
Flutter PNGs. This is one live host-family vertical, not completion of A1.
Second A1 checkpoint: Element.classList now retains one live
DOMTokenList identity across external and list-driven class mutations rather
than discarding the wrapper after every attribute write. Focused runtime proof
retains the object through setAttribute, reflects current tokens, advances
exactly one renderer-source generation per write, and recascades .wide and
.tall selectors to 140×30. The release/AOT Playwright corridor retains the
same object through Flutter-routed input, observes clicked and 140px geometry
in the page task and CDP, and pins the resulting exact Flutter PNG to
5633ca7a032c8c6a1582f5389b6b4a594b91d99e89784683fbf3679f18639f95 before
byte-identical target switching and renderer recovery. This converges one more
attribute-backed host object; other token lists, inline style, collections, and
attribute nodes remain separate work.
Third A1 checkpoint: HTMLAnchorElement.relList now retains one live
DOMTokenList across external and list-driven rel mutations. Focused runtime
proof retains identity through setAttribute and add, reflects ordered tokens,
advances exactly one renderer-source generation per write, and recascades
[rel~="wide"]/[rel~="tall"] selectors to 140×30. A hidden real anchor keeps
the prior release/AOT baseline, dataset, and classList hashes unchanged; its rel
mutation becomes visible at 120×32, agrees with CDP attributes/geometry, and
pins exact Flutter pixels to
7ae6e6d8f650d733922b1af018dfdcac310bdcbb4f14537cdb20500c44da3c04 before
byte-identical target switching and renderer recovery. Sandbox tokens, inline
style, collections, and attribute nodes remain separate work.
Fourth A1 checkpoint: HTMLIFrameElement.sandbox now retains one live
DOMTokenList across external and list-driven sandbox mutations, completing
the three attribute-backed token-list identities currently hosted by the
runtime. Focused proof retains identity through setAttribute and add,
reflects valid ordered sandbox tokens, advances exactly one renderer-source
generation per write, and recascades token selectors to 140×30. A hidden real
iframe preserves all earlier exact hashes; allow-same-origin allow-forms
reveals a 120×32 box in the release/AOT corridor, agrees with CDP, and pins
Flutter pixels to
57b9814c22902e40fc38180d79a1a78068f1b15154f4149bef8fbea5b6cf05cb
before byte-identical target switching and renderer recovery. Inline style,
collections, and attribute nodes remain separate work.
Fifth A1 checkpoint: HTMLElement.style now retains one live inline
CSSStyleDeclaration across external style replacement and declaration API
writes instead of replacing its wrapper after each mutation. Focused proof
retains identity through setAttribute and setProperty, reflects current
declarations in both directions, advances exactly one renderer-source generation
per write, and recascades to 140×30. A hidden target preserves all prior exact
hashes; the release/AOT corridor reveals it at 120×32, matches its serialized
style and geometry through CDP, and pins exact Flutter pixels to
b4fe0e2cdba9f98193e8dfc7aadb7fa892e508e269a4a94beb9c2970d8ce5096
before byte-identical target switching and renderer recovery. Collections and
attribute nodes remain separate work.
Sixth A1 checkpoint: Element.attributes now retains one live
NamedNodeMap, with dynamic length/index/name lookup and stable attached Attr
identity across external writes. Attached Attr.value reads current state and
writes through the authoritative DOM mutation path. Focused proof retains both
identities through setAttribute and Attr.value, advances exactly one
renderer-source generation per write, and recascades to 140×30. A hidden target
preserves all prior exact hashes; the release/AOT corridor reveals it at 120×32,
agrees with CDP attribute/geometry state, and pins Flutter pixels to
17cb0de692001fcb97dcab23c870b800e7e7c3b09010e312a0bbc64e496ec1ea
before byte-identical target switching and renderer recovery. Detached Attr
lifecycle plus setNamedItem/removeNamedItem, and live structural collections,
remain separate work.
Seventh A1 checkpoint: live structural collection attributes now retain
resolver-backed identity while reflecting Page mutations: Node/Element
childNodes/children, document forms/images/links/scripts, form controls,
select/datalist options, labels, and table collections. Element/document
getElementsByTagName and getElementsByClassName return cached live
HTMLCollections; querySelectorAll remains a static NodeList as required.
Focused proof performs two structural writes, observes exactly one
renderer-source generation each, preserves collection identity/index/name
lookup, and proves a pre-mutation query list stays static. The release/AOT click
corridor retains empty collections before Flutter-routed input, observes the
rendered #dynamic.badge afterward through the same objects, matches the
authoritative CDP node, and keeps the pinned classList scene hash byte-identical.
Detached Attr operations and live CSSOM/script scheduling remain separate work.
Eighth A1 checkpoint: document.styleSheets now retains one live
StyleSheetList, each author <style> resolves to the same stable
CSSStyleSheet, and retained CSSRuleList, CSSStyleRule, and rule
CSSStyleDeclaration objects resolve refreshed BrowserCore CSS after an
external style-element mutation. The CSSOM resource refreshes even when a
same-task synchronous geometry query consumed the pending mutation before the
ordinary runtime drain. Focused proof retains every identity, advances exactly
one renderer-source generation, and observes Stylo's 140×30 result. The
release/AOT corridor retains the objects across all seven earlier stages,
changes one dedicated author rule, observes 120×32 synchronously and through
the retained CSSOM plus CDP, and pins exact Flutter pixels to
b09bce0ee8acf5ac3b40a2190241a6592880a3e47615c030469b2a887d118f1d
before target switching and byte-identical renderer recovery. CSS rule mutation
APIs and parser-module/task scheduling remain separate work.
Ninth A1 checkpoint: Document.createAttribute, detached Attr.value, and
NamedNodeMap.setNamedItem/removeNamedItem now complete the hosted attribute
lifecycle. Attaching preserves the supplied Attr identity, replacement and
removal return the prior object detached with its value intact, direct
removeAttribute detaches cached nodes, and attaching an Attr still owned by a
different element fails closed. Focused proof covers replace/remove/reattach/
external-remove transitions, Stylo recascade, and exactly one renderer-source
generation per actual mutation. The release/AOT corridor repeats replacement,
removal, reattachment, and in-use rejection in one retained map, observes
120×32 synchronously and through CDP, and pins exact Flutter pixels to
92181acffcd1e39ac9720c8edeeba2c148034a89f61297652dc948306f3af052
before target switching and byte-identical renderer recovery. Parser-module/task
scheduling and remaining plausible runtime shims are the next A1 boundary.
Tenth A1 checkpoint: parser-discovered inline and external ES modules now use
V8's native module parser/evaluator in the document realm. Modules defer until
parser classics finish, top-level await and exports execute, and each classic,
module, and document task receives its own microtask checkpoint. The document
task owner replaces Promise-backed timer shims with bounded timeout, interval,
animation-frame, cancellation, and post-load/automation pumps. CSP, mixed
content, response policy, cancellation, and stale document/runtime rejection
remain on the existing BrowserCore external-script boundary. Unresolved module
imports were left fail closed for A2's unified dependency loader. Focused
runtime and production-navigation proofs pin classic → microtask → deferred module → module
microtask/await → load → task ordering, task cancellation, one interval turn,
animation-frame delivery, post-load tasks, realm reuse after failure, exactly
one renderer-source generation for the module mutation, and external module
loading. The release/AOT fixture preserves every earlier exact hash, proves the
same parser order, reveals a module-owned 120×32 target synchronously and through
CDP, and pins exact Flutter pixels to
faa3c863350c742bdeb38338bca09307a4db49e6f7bb7a3f4e6d73eef60ae2fa
before target switching and byte-identical renderer recovery. The obsolete
non-page inert history object and fallback inert stylesheet object were deleted.
A1 exit: live Node/Element/Document mutations, author CSSOM objects, events,
focus, selection, forms, history, and profile/context-partitioned storage all
share the BrowserCore page realm and render-source path. Every mutation vertical
above proves authoritative Page state, explicit geometry invalidation, CDP
agreement, and Flutter pixels. Parser classics, modules, microtasks, and bounded
document tasks have production lifecycle ordering; cross-document navigation
retires the old realm, and two contexts retain isolated runtimes/session state.
Vixen still does not fabricate child-frame realms: contentWindow and
contentDocument remain null until A3 establishes same-origin access and
cross-origin wrappers, preserving the frame boundary without an inert fake.
Static module dependency graphs moved to A2's first loader checkpoint; broader
CSSOM/DOM/Web API surface remains compatibility breadth rather than an A1
ownership blocker.
Proof: script-driven mutation visibly changes the Flutter scene; synchronous and asynchronous geometry observe the right commit; CDP and page script inspect the same nodes.
A2. Unified loader and profile policy
Status: in progress (started 2026-07-18). Converge one resource family at a time without moving network or profile ownership into V8 or Flutter.
- Finish one resource loader for documents, scripts, styles, images, fonts, fetch/XHR, frames, and downloads with shared request ids, redirect/policy, cookies/cache, priorities, cancellation, and diagnostics.
- Complete streaming/abort/progress behavior and policy-before-renderer exposure.
- Integrate profile state, partition keys, cert/proxy/path/portal host services, and a real bounded download lifecycle.
First A2 checkpoint: parser-discovered inline and external ES modules now load nested static dependencies through the same bounded external-resource loader as parser scripts, stylesheets, and images. V8 discovers and evaluates the graph, while BrowserCore supplies shared numeric request ids, redirect and final-URL resolution, CSP/mixed-content checks, strict JavaScript response MIME, profile cookies and cache writes, bounded network diagnostics, and graph/event limits before source reaches V8. File and same-origin HTTP graphs execute in the persistent page realm; redirected roots resolve relative imports from the accepted final URL. Stop aborts the in-flight transport and rejects late module, DOM, cookie, cache, and lifecycle effects. Focused tests prove nested execution, cross-context profile cookies, cache records, distinct request ids, cross-origin fail-closed diagnostics, final-URL resolution, and transport disconnect on cancellation. The existing release/AOT Playwright fixture now imports a real dependency before producing the unchanged module-owned Flutter scene.
Second A2 checkpoint: static HTTP(S) graphs now enforce CORS for external
module roots and every dependency/redirect response before source reaches V8.
Cross-origin requests carry the serialized document Origin; default and
anonymous module graphs suppress cross-origin credentials and ignore response
cookies, while crossorigin="use-credentials" requires an exact allowed origin,
Access-Control-Allow-Credentials: true, and inherits credentialed behavior
through dependencies. Wildcard default graphs remain credentialless. Focused
BrowserCore tests prove allowed redirect/final/nested responses, missing-header
rejection without following the redirect or executing source, ignored default
cookies, credentialed root-cookie propagation, and stable lifecycle settlement.
Third A2 checkpoint: eligible exact-URL HTTP(S) module cache entries now
conditionally revalidate both external roots and graph dependencies through the
shared resource loader. Cached validators are added to live requests; only a
matching 304 restores bounded raw source bytes, while current URL/CSP,
mixed-content, CORS, status, and strict JavaScript MIME policy still run before
V8 exposure. Cache-disabled contexts perform neither module cache reads nor
writes. Entries with no-store, unsupported Vary, no validator, non-2xx
status, or bodies beyond the current resource limit are not reused. Focused
two-context tests prove root/dependency validator requests, raw 304 diagnostics,
source execution, persisted 200 representations, cache-disable bypass, current
CORS rejection, and strict MIME for external roots. Freshness-based reuse,
redirect aliases, full Vary, import maps, dynamic import(), and import
attributes remain explicit next work.
Fourth A2 checkpoint: one bounded parser-discovered inline import map may now
register before module discovery. The Deno-maintained import_map resolver
handles exact, prefix, URL-like, null-blocking, and most-specific scoped
imports/scopes mappings; each mapped URL still crosses the existing graph's
scheme, CSP, mixed-content, CORS, credentials, strict-MIME, cache, request-id,
cancellation, and diagnostics boundaries. Maps are capped at 256 KiB, 2,048
mappings, 128 scopes, and 16 KiB strings/URLs; recoverable parser diagnostics are
bounded before becoming runtime warnings. Import maps do not remap a module
script's src, and import.meta.resolve() uses the same frozen map. External,
multiple, late, integrity-bearing, malformed, or oversized maps fail closed with
stable script.import-map diagnostics and no partial registration. Focused file
graphs prove bare/prefix/base/scoped resolution; BrowserCore HTTP tests prove
numeric request ids, visible module mutation, and CORS rejection for a mapped
cross-origin target. Modern multiple-map merging/resolved-module-set behavior,
integrity maps, dynamic import(), and import attributes remain explicit next
work.
Fifth A2 checkpoint: dynamic import() originating in parser-discovered
page module graphs now extends the same retained graph instead of consulting a
mutable “last root” policy. Every specified and accepted-final module URL keeps
its original root's CSP, CORS credentials mode, import map, profile/cache path,
and shared request-id allocator. Static plus dynamic loads share the existing
64-load graph cap; per-graph and per-realm provenance maps are separately
bounded. Dynamic redirects register their accepted URL before child resolution,
and later module-owned functions/document tasks are driven to bounded event-loop
quiescence. Stop aborts tracked transport tasks, generation-checks profile
effects, suppresses stale DOM/cookie/cache/lifecycle effects, and rebuilds the
cancelled page realm before reuse. Focused tests prove delayed mapped file
imports, module-map single evaluation, cumulative graph limits, rejected import
attributes, credential policy retained after a different root runs, redirected
child resolution, cache records, transport disconnect, and clean subsequent
evaluation. The release/AOT Playwright corridor now evaluates one real dynamic
dependency without changing the pinned Flutter scene. Dynamic imports directly
authored by classic scripts or automation source remain fail-closed until those
scripts carry an exact URL and graph policy; import attributes, workers, modern
multiple-map merging, and integrity maps remain explicit breadth.
Sixth A2 checkpoint: page fetch()/XHR and parser-module HTTP(S) loads now
share one bounded private-cache decision module. The transport records the exact
effective final-hop request headers, including automatic compression, user-agent,
host, cookie, and body-length fields. Cache records retain at most 32 normalized
Vary names and their exact present/absent values; wildcard, malformed,
oversized, no-store, non-success, mismatched, and legacy Vary records are not
reused. Default requests reuse max-age freshness after accounting for Age,
stale/no-cache entries conditionally revalidate only when a validator exists,
forced cache modes retain their explicit behavior, and cache-disabled contexts
bypass reads and writes. Cached responses still cross current CORS, integrity,
status/MIME, graph provenance, and body-size policy before exposure. Focused
runtime tests prove a fresh exact-language variant performs one transport
request while a changed value refetches; a two-context module graph proves fresh
root and dependency reuse through the same profile cache. At this checkpoint
the URL-keyed store retained only the latest representation for a URL;
simultaneous variants,
Expires/heuristic freshness, request cache directives, and redirect aliases
remain explicit breadth.
Seventh A2 checkpoint: the shared HTTP transport now drains response bodies
chunk by chunk and checks the destination limit before extending its bounded
buffer, rather than allocating an unchecked complete body first. Stable
response/progress/completed events carry chunk, cumulative, optional total, and
final body bytes through BrowserCore, the C ABI, module diagnostics, and CDP.
Sub-quantum transport chunks coalesce into at most 256 progress records per
response before crossing those boundaries; CDP maps them to
Network.dataReceived/loadingFinished. Page Response.body is a real bounded
ReadableStream over the retained transfer chunks with one-shot bodyUsed
semantics; Blob streams use the same implementation. XHR emits typed upload and
download ProgressEvents with exact loaded/total values and preserves
headers-received → progress → loading/done → load/loadend ordering. A pre-aborted
fetch rejects with the signal's first reason and performs no transport. Focused
transport, runtime, XHR, CDP, module, and cancellation tests cover the new event
order and byte counts. The current text/cache/integrity pipeline still buffers
the bounded response before resolving fetch(): active page AbortSignal
cancellation and policy-safe response-before-completion streaming remain the next
loader boundary, while BrowserCore stop/navigation cancellation continues to
drop the live reqwest future.
Eighth A2 checkpoint: page fetch() now starts one host-owned asynchronous
request instead of holding V8 inside a blocking op. Each realm admits at most 32
active requests with opaque ids, one completion waiter, explicit cancellation,
and teardown cancellation. An active AbortSignal drops the pending reqwest
transport, rejects with the signal's exact first reason, and records a bounded
request/failure diagnostic; XHR owns a controller and send generation so
abort() drops the same transport and cannot publish late ready-state/load/error
events into a reopened request. Runtime stop and deadlines use a persistent
interrupt generation, so cancellation remains visible after V8 termination is
cleared and no partial cookie, preflight-cache, or response-cache effect can
commit. Focused stalled-peer tests prove fetch and XHR disconnect, exact reason,
terminal event order, and the existing stop/preflight-stop recovery corridor.
The Deno realm now retains one current-thread Tokio executor for async host ops
across evaluations rather than stranding op tasks on a per-evaluation runtime;
non-blocking shutdown keeps async CDP owner teardown safe. Responses still
resolved only after the bounded body, integrity, cache, and visibility decisions
completed; policy-safe response-before-completion streaming remained the next
transfer boundary at that checkpoint.
Ninth A2 checkpoint: the profile cache now retains simultaneous response
variants as independently bounded rows under a versioned URL-plus-selector key.
Canonical sorted Vary selectors preserve absent versus empty values, cap total
selected request-header data at 64 KiB, replace only the matching variant, and
continue counting every representation toward the existing 512-record global
eviction limit. Legacy URL-only rows remain readable and are transactionally
replaced on the next write. Page fetch/XHR and BrowserCore module/resource loads
select the newest matching usable variant through one shared decision before
rerunning current policy. Store tests prove two variants survive, selector order
does not create a duplicate, legacy migration works, and bounds count rows. The
runtime en → fr → en proof performs exactly two transport requests and
returns the first English representation on the third fetch. At this
checkpoint, Expires/request directives and redirect aliases remained the next
cache breadth.
Tenth A2 checkpoint: the shared cache decision now computes current age from
strict HTTP Date, Age, stored time, and resident time, then uses response
max-age or Expires for explicit freshness. Malformed max-age/Age is
stale, while invalid Date/Expires is ignored rather than guessed. Effective
request no-store bypasses
reuse and insertion; no-cache/legacy Pragma: no-cache, max-age, and
min-fresh force revalidation when required. Bounded or valueless max-stale
may reuse an expired response but cannot override response no-cache or
must-revalidate. Numeric overflow and conflicting duplicate directives do not
become permissive. Unit boundaries pin age/freshness equality, contradictory
max-age/Expires, malformed values, request constraints, and stale allowance.
An end-to-end page test performs one Expires-fresh cache hit, then sends author
Cache-Control: no-cache and proves validator/304 revalidation with exactly two
transport requests; a separate profile test proves request no-store persists
no representation. At this checkpoint heuristic freshness and redirect aliases
remained cache breadth.
Eleventh A2 checkpoint: permanent same-origin redirect aliases now retain
accepted final-URL identity without duplicating response bodies. A separate
profile table stores at most 512 aliases, each capped at 20 hops and 64 KiB of
targets; clear-data removes aliases with representations. Only complete 301/308
chains without Cache-Control: no-store whose hops remain in the original
origin are persisted. Temporary,
cross-origin, malformed, looping, over-limit, and policy-blocked aliases fall
back to live transport; direct or unsafe later responses invalidate the original
alias. Lookup revalidates URL/CSP/mixed-content policy on every target, computes
final-hop cookies and Vary headers, and reuses aliases only while the final
representation is fresh. Cached diagnostics replay request/redirect/response/
progress/completion with the accepted final URL and redirect count. A page
fetch() proof performs the initial redirect plus target requests, then repeats
the original URL with no transport while preserving Response.url and
redirected. A two-context module proof reuses the redirected root and relative
dependency from the profile, preserving final-URL import resolution and network
redirect events. Store and decision tests pin eviction, validation, deletion,
clear-data, legacy safety, and rejection of temporary/cross-origin aliases.
General redirect-response caching still requires retained redirect response
headers; this checkpoint deliberately does not treat 302/307 as permanent.
Twelfth A2 checkpoint: ordinary page fetch() responses now resolve at a
separate policy-accepted final-response head while one host worker retains the
body and terminal profile effects. Every live redirect target reruns URL, CSP,
and mixed-content policy; final same-origin/CORS visibility and filtered headers
are fixed before status, URL, or headers reach V8. Accepted raw chunks cross an
eight-message backpressured channel under the existing destination body cap and
32-request realm cap. ReadableStream reads and XHR loading/progress therefore
advance before transport completion, while terminal success waits for bounded
cookie/cache commit and preserves one request id through response, progress, and
completion. A serialized fetch-generation gate permits those commits while V8 is
idle between reads but rejects them after abort, stop, or deadline invalidation.
Abort after head exposure drops transport, removes ownership, emits one failure, and rejects pending or later body reads with the exact first JS
reason. CORS failure at the head cancels without reading the stalled body.
Integrity-bearing requests, conditional 304 revalidation, and opaque no-cors
responses remain buffer-before-resolution; active network-body clone/tee also
fails closed rather than creating an unbounded second consumer. Gated-peer tests
prove response-before-first-chunk, first-read-before-completion, stable split
diagnostics, post-response abort/disconnect, redirect-policy rejection before a
second request, final-head rejection, and integrity buffering.
Thirteenth A2 checkpoint: dynamic import() authored by parser classic
scripts and BrowserCore automation evaluations now enters the same retained
module loader with an explicit source URL and document policy. Inline classics
receive distinct document-base fragment identities and the import map available
at their parser position; redirected external classics execute under the
accepted final URL, so relative imports cannot fall back to the original
request. Automation uses the current document base, CSP/bypass decision, origin,
and retained import map. Source-only harness documents whose identifiers are not
absolute URLs continue ordinary evaluation but cannot create module provenance;
their dynamic imports fail before transport. Every admitted path receives same-origin module credentials,
shared profile/cache state and numeric request ids, the existing 64-load graph
cap, generation cancellation, response policy, and terminal diagnostics.
Focused file tests prove mapped classic and automation imports, while a
BrowserCore redirect test proves a classic dependency resolves from the final
URL and settles navigation under one numeric module request id. Import
attributes remain fail closed pending destination-specific response policy.
Fourteenth A2 checkpoint: static and dynamic JSON modules now admit exactly
with { type: "json" } through the existing graph loader. A V8 import-attribute
validation callback sees the complete static/dynamic attribute map and records a
bounded dynamic denial before resolution, so unknown keys and non-JSON types
cannot trigger transport even when deno_core later reduces attributes to a
requested module type. File loads require a .json URL; HTTP(S) loads require
application/json or a +json subtype. Accepted source remains under the same
URL/CSP/mixed-content/CORS, credentials, redirect, profile/cache, body, graph,
request-id, diagnostics, and cancellation policy as JavaScript modules before
V8 creates a JSON module namespace. Focused tests prove static and dynamic file
imports, reject an extra dynamic key without a third request, preserve isolate
reuse, and reject JavaScript MIME without a profile cache write. A BrowserCore
HTTP proof persists an accepted JSON representation and exposes one numeric
request id. Text/bytes/custom attributes and integrity metadata remain fail
closed.
Fifteenth A2 checkpoint: parser-discovered external classic and module roots
now retain their authored integrity metadata through the shared resource
request. BrowserCore verifies the strongest recognized SHA-2 candidate against
accepted raw response bytes after URL/CSP/CORS/status/MIME policy but before
UTF-8 conversion, V8 execution, profile cookies, or cache insertion. Mismatch
adds one stable integrity failure under the existing numeric request id,
leaves the document runnable for later scripts/lifecycle, and commits no response
profile effects. Unknown or malformed-only algorithms retain the existing SRI
no-metadata behavior rather than inventing a failure. Focused parser and
BrowserCore tests prove a SHA-384 module executes and caches, while a mismatched
classic neither executes nor exposes its response cookie/cache row. Import-map
integrity metadata for graph dependencies remains fail closed. Cross-origin
classic SRI uses the module-style CORS boundary: requests carry document origin,
anonymous mode omits cross-origin credentials, and source is rejected before SRI
unless the response grants access.
Sixteenth A2 checkpoint: the retained inline import map now owns at most
2,048 exact normalized-URL integrity entries beside the Deno-maintained resolver.
Absolute and URL-like relative keys resolve against the map base; non-object,
non-string, bare-relative, oversized, and normalized-duplicate forms reject the
whole map without partial registration. Static and dynamic JavaScript/JSON
dependencies select metadata only after import-map resolution, while an external
module root uses it as fallback only when no authored integrity attribute is
present. Accepted raw bytes pass URL/CSP/mixed-content/CORS/status/MIME policy,
then strongest-recognized SHA-2 verification before redirect graph publication,
V8, response-cookie commit, or cache insertion. Mismatch preserves the allocated
numeric request id, adds one terminal integrity failure, and leaves no module or
profile effect. Focused file proof accepts two mapped dependencies and rejects a
tampered one without execution. BrowserCore HTTP proof accepts and caches a
mapped root/dependency under separate numeric ids, while mismatch neither
executes nor exposes its cookie/cache row. Multiple-map merging remained a
separate follow-up trust boundary at this checkpoint.
Seventeenth A2 checkpoint: a document may now register up to 64 bounded
inline import maps before or after module discovery. Each source retains the
256 KiB parse cap; the merged normalized resolver plus integrity state is capped
at 512 KiB, 2,048 mappings, 128 scopes, and 2,048 integrity rows. Imports,
scope rules, and integrity entries merge first-wins; conflicts produce at most
32 bounded warnings plus one omission marker, and malformed or
cumulative-overflow maps leave the prior state unchanged. One shared successful-resolution set retains at most 2,048
(referrer, normalized specifier) records and 1 MiB of URL data. A repeated pair
returns its first successful URL even across immutable map versions, while merge
filtering prevents new global/scoped rules from affecting prior resolutions.
Parser-discovered static roots keep their parser-position snapshot. Dynamic
imports and import.meta.resolve() switch that graph to the latest document map
without changing its CSP, credentials, profile, request-id, cancellation, or
limit provenance; BrowserCore automation also receives the latest snapshot.
Focused tests pin first-wins imports/scopes/integrity, late-map static isolation,
late-map dynamic success, result stability, and every count/byte cap. A
BrowserCore HTTP graph requests only the first map's conflicted module plus the
second map's additive module, verifies both integrity entries, exposes numeric
request ids and bounded conflict warnings, and settles once. External maps remain
unsupported because HTML's import-map delivery format is inline.
Proof: multi-context profile tests, waterfalls, CORS/CSP/SRI/mixed-content/ cache profiles, cancellation races, safe download tests, and Linux host smokes.
A3. Renderer and frame model breadth
- Establish child-frame render mutations, same-origin access, cross-origin boundaries, sandboxing, nested viewport/scroll commits, and lifecycle teardown.
- Widen Flutter formatting only from reduced corridor/WPT failures; do not build isolated CSS helpers without a rendered commit consumer.
- Make animation/timers request bounded commits without starving BrowserCore or creating unbounded scene work.
Alpha exit gate
Alpha requires:
- one BrowserCore profile/context/document/runtime lifecycle;
- one Flutter mutation/commit renderer for GUI and rendered automation;
- two contexts that independently load, script, render, inspect, and share only intended profile state;
- active navigation/runtime/render work cancellable without stale commits;
- same-task DOM/style mutation driving correct synchronous geometry and visible pixels;
- input, scroll, find/selection, CDP, and accessibility naming exact commits; and
- reproducible architecture, compatibility, limitations, and measurements.
Beta — build a useful measured browser
B1. Rendering and content fidelity
Drive the Dart formatter from reductions and pinned profiles:
- common block/inline formatting, floats, positioned/fixed/sticky, overflow, flex, grid, tables, intrinsic sizing, replaced elements, fragmentation/print;
- responsive raster images, SVG basics, accepted web fonts, gradients, borders, shadows, transforms, opacity/compositing, filters, animation;
- typography, bidi/writing modes, fallback, line breaking, caret/selection; and
- browser-correct form-control rendering and interaction.
Prioritize typography, intrinsic sizing, tables, controls, and scrolling because they dominate real-page failures.
B2. Runtime and application basics
Widen live DOM, HTML, CSSOM, events, forms, navigation, URL/encoding/streams, timers, observers, messaging, WebSocket/EventSource, modules, workers, frames, sandboxing, and resource timing from corridor failures. Unsupported APIs remain explicit; inert shape does not count.
B3. Network, security, privacy, and downloads
Complete transfer streaming, upload/download progress, authentication/proxy, HTTP/2 interoperability, cache freshness, safe filenames/resume/history, Permissions Policy, COOP/COEP/CORP, HSTS, Trusted Types, partitioned state, private-network access, prompts, and failure classification.
B4. Daily-smoke Flutter product
Deliver robust tabs, address/search, reload/stop, history, find, zoom, downloads, permissions, error/recovery pages, session restore, settings/privacy controls, keyboard navigation, safe external opens, and host integration. Chrome remains a controller over BrowserCore; renderer state remains ephemeral and commit-bound.
B5. Automation and inspection products
Support independent targets/contexts, reliable waits, DOM/runtime handles, commit-aware input, downloads, dialogs, network/console/lifecycle events, Flutter-scene screenshots, permissions, and bounded traces. Drive additions from external Playwright workflows rather than method-name counts.
B6. Compatibility, performance, and reliability loop
- Expand pinned WPT profiles across parser, DOM/events/forms, CSS/layout/paint, network/security, storage/history, runtime APIs, and accessibility.
- Publish a controlled corridor spanning static content, docs, forms, downloads, app-like pages, and automation-heavy pages.
- Track startup, navigation, cascade/layout/paint/commit time, frame stability, memory, capture latency, throughput, install size, and profile growth.
- Bound malformed/content-controlled work and make renderer/runtime/profile recovery diagnosable.
Beta exit gate
The corridor loads in Linux GUI and chrome-less automation, supports meaningful interaction/persistence, survives restart/cancellation/renderer loss, and has published screenshots, reductions, profile counts, automation results, measurements, and known gaps. Other platforms remain committed targets until they pass their own gates.
v1.0 — honest daily-driver minimum
Vixen may call itself v1.0 when:
- common document, documentation, form, download, and app-like corridor pages are readable and usable with stable typography, images, layout, scrolling, interaction, navigation, and profile state;
- GUI and Playwright/CDP share BrowserCore and the Flutter renderer and recover predictably from network, document, runtime, renderer, and profile failures;
- supported security/privacy behavior is fail-closed and tested; single-process isolation limits are prominent;
- Linux install/update, certs, fonts, portals, downloads, GPU, settings, session restore, accessibility, and clear-data flows pass, and each additional platform claimed as supported by that release passes its native gate;
- compatibility, performance, memory, binary/install size, and unsupported capabilities are published from reproducible commands; and
- every claim maps to an acceptance gate, fixture/profile/smoke, and owner.
v1.0 is a useful supported subset, not the end of the replacement goal.
Platform expansion
After Linux R8 and beta-quality renderer stability:
- macOS and Windows: same render mutation/commit broker, native Flutter runner, fonts, input/IME, accessibility, host services, signing/packaging, capture, size, and performance evidence.
- Android: pinned V8 source/toolchain, lifecycle/process recreation, touch/IME, accessibility, host services, split-ABI packaging, capture, and resource budgets through the same renderer contract. A prewarmed builder is allowed only as a reviewed digest-pinned cache that exactly matches the Flutter/engine/JDK/API/NDK/Gradle pins and still supports reproducible Rust/V8 source builds.
- Apple Silicon iOS Simulator: same Flutter renderer, BrowserCore, V8 JavaScript/WebAssembly, simulated lifecycle/input/accessibility/host services, and reproducible Xcode runner. Physical iOS requires a new decision.
- WebAssembly: widen API and resource/conformance proof on every declared target without adding an alternate runtime.
Replacement horizon
After v1, prioritize by measured site/user impact:
- Accessible browser: complete semantics, screen-reader interaction, keyboard, caret/selection, forced colors, reduced motion, and native controls.
- Media: Flutter-compatible platform media integration, codecs, controls, tracks, fullscreen/PiP, autoplay/permissions, Media Source, and WebAudio.
- Offline applications: IndexedDB, Cache Storage, service workers, workers, file/blob streaming, notifications, installability, and offline lifecycle.
- Communications: production WebSocket/EventSource, WebRTC/device permissions, richer streaming/compression, and justified WebTransport.
- Graphics/documents: Canvas 2D, SVG breadth, WebGL/WebGPU, print/PDF, color management, advanced typography/writing modes, and CSS long tail.
- User ecosystem: scoped extensions, content blocking, password/autofill, import/export, developer tools, and policy controls.
- Defense in depth: renderer/content sandboxing, site isolation/OOPIF, brokered host access, crash containment, update/signing hardening.
- Broader compatibility: continuously widen WPT and the real-site corridor until exceptions are uncommon across supported targets.
Immediate execution queue
Work top-to-bottom and finish/document/commit each slice:
- Continue A2 request metadata: retain authored external-script/module referrer policy and fetch priority through roots, accepted redirects, static and dynamic descendants, cache requests, and bounded diagnostics. Apply the policy at the shared loader rather than synthesizing headers in V8.
- Preserve the R8/A1 corridors: keep real Mozc preedit/commit, native AT-SPI role/state/positive-local-bounds plus native-pointer focus → DOM → newer-commit evidence green while widening shared-core behavior; do not replace it with injected text or BrowserCore geometry. Restore an AT-SPI Action claim only after a newer immutable GTK4 engine provides it.
Do not reintroduce native layout/paint/frame ownership while stabilizing. A security, data-loss, or release-blocking regression may preempt the queue.
Velocity and deletion policy
- One renderer slice at a time. A critical BrowserCore fix may run beside it; adjacent feature breadth may not.
- Delete before adapting. If transitional code has no independent BrowserCore value and replacement evidence exists, remove it instead of adding compatibility layers.
- No speculative renderer framework. Start from the R3 fixture and generalize only when a second reduced case proves the need.
- One trust boundary per commit. Split protocol, ABI/broker, formatter, automation host, synchronous flush, and deletion at independently reviewable points.
- Use the test ladder once. Focused checks while editing, relevant gate before
commit,
just gate-pushonce for a coherent push batch. - Executable evidence beats prose. A commit advances only with DTO adversarial tests, fixture pixels/geometry, a race, a native smoke, or measured output.
- Update or delete gates with ownership. Tests that prove removed WebRender/ EGL/texture behavior disappear at cutover; tests of browser semantics move to the Flutter renderer rather than pinning old implementation details.
- Keep handoffs cheap. Update limitations and leave the next smallest queue item; completed queue prose is replaced, not accumulated.
Working rule
Every milestone lands with:
- one named authoritative owner and no parallel browser/renderer truth;
- exact revisions/commit ids across every renderer boundary;
- focused unit/adversarial tests plus one browser-visible fixture or smoke;
- stable bounded diagnostics at trust and lifecycle boundaries;
- compatibility/limitation updates when behavior changes; and
- the cheapest focused checks followed by the relevant hk/
justgate.
Prefer small, boring verticals. A large surface of plausible APIs is less valuable than one exact BrowserCore mutation becoming one Flutter commit observed by pixels, script, input, CDP, and accessibility together.
Vixen implementation plan
This document describes the active plan after ADR-022 R7. Historical plans for the deleted Rust layout/display-list/WebRender/EGL/RGBA architecture are no longer normative and were removed with that implementation.
Landed foundation
R1–R7 are complete:
- BrowserCore owns contexts, navigation/history, DOM/cascade/runtime, profile and network policy, input intent, resource acceptance, and accessibility meaning.
- BrowserCore publishes bounded full renderer snapshots and deterministic incremental mutations with exact context/document/source/style/viewport/ resource generations.
- Flutter owns formatting, Paragraph text measurement, Canvas/Picture/Scene paint, root and nested scroll mechanics, hit testing, semantic bounds, find geometry, and direct scene PNG capture.
- Synchronous CSSOM geometry waits for the matching Flutter commit and supports cancellation, timeout, one bounded resync, and late-response rejection.
- GUI, page-only automation, rendered CDP/Playwright, and rendered fixture checks use the same Flutter formatter and commit painter.
- Native
vixen-headlessis text/runtime/profile-only. Renderer-dependent operations fail closed instead of inventing geometry or pixels. - R7 deleted WebRender/gleam,
GlContext, both EGL owners, native screenshots, Rust layout/display-list/paint and paint-helper modules, RGBA frame transport, Linux pixel-buffer texture presentation, raw coordinate input, and obsolete gates/tests.
The current ownership contract is specified in ARCHITECTURE.md, the renderer
and shell contract in FLUTTER_SHELL.md, acceptance in ACCEPTANCE.md, and
compatibility evidence in COMPAT.md.
Immediate queue: A2 after A1 convergence
The complete 270-fixture Flutter manifest, external rendered Playwright/CDP, release archive/size, startup/capture/memory, profile growth, 45-frame software and physical AMD/Mesa measurements, renderer reset, and exact scene recovery now have post-R7 checkpoints.
R8 completed on 2026-07-17: the full release/AOT native interaction gate passed
with real workspace-local IBus Mozc preedit/commit, Flutter-authored positive
AT-SPI bounds, native Focus → DOM focus → newer same-document commit evidence,
and the remaining interaction corridor. Keep that gate intact; it is one
controlled host proof rather than an IME or assistive-technology matrix.
- A1 completed on 2026-07-17. The claimed mutable DOM/CSSOM/events/focus/ selection/forms/history/storage surface is live; attached/detached attributes, structural collections, parser classics/modules, microtasks, and bounded document tasks have focused and release/AOT proof. Cross-document realm teardown and context isolation remain pinned. Unsupported child-frame realms stay fail-closed for A3.
- A2's first five loader checkpoints now route static and dynamic ES-module
dependency graphs through the shared external-resource loader with numeric
BrowserCore request ids, redirect/final-URL policy, strict response MIME,
profile cookie/cache writes, bounded diagnostics, and stop cancellation. The
release/AOT Playwright fixture imports a real dependency without changing its
exact Flutter scene. HTTP(S) roots, dependencies, and redirects now enforce
CORS before V8 exposure; default cross-origin graphs omit credentials, while
use-credentialsrequires exact credentialed permission and propagates through the graph. Eligible exact-URL root/dependency cache entries now conditionally revalidate through live requests; a matching 304 restores bounded bytes only before current CORS/status/strict-MIME policy reruns, and cache-disabled contexts bypass reads and writes. One bounded inline import map registered before module discovery now resolves exact/prefix/URL-like and scoped mappings through the same loader without remapping modulesrcor bypassing policy. Dynamic imports from page module code retain per-module graph policy/import maps across later roots and tasks, share cumulative bounds, resolve children from accepted redirect URLs, and cancel without late profile or runtime effects. The release/AOT fixture now imports both a real static and dynamic dependency without changing its scene. Page fetch/XHR and module resources now also share boundedDate/Ageplusmax-age/Expiresfreshness, request cache directives, and exact simultaneousVaryvariants plus bounded permanent same-origin redirect aliases. Transport body reads, exact progress/completion diagnostics through BrowserCore/C ABI/CDP, retainedReadableStreamchunks, XHR upload/download progress, and pre-aborted fetch rejection are landed. Page fetch/XHR now have bounded asynchronous ownership and active-signal transport cancellation. Ordinary policy-accepted response heads now resolve before completion into an eight-message backpressured body stream; body terminal state still owns cache/cookie commit and exact cancellation. Integrity, 304 revalidation, and opaque responses remain buffered. Direct classic and automation dynamic imports now carry exact source/document policy, retained import maps, profile state, graph limits, and cancellation through the same loader. Exact static/dynamic JSON import attributes now retain that boundary and strict file/HTTP response typing; unsupported keys/types fail before transport. External classic/module root SRI and bounded import-map integrity metadata now verify root/dependency raw bytes before cache/profile insertion or V8 exposure. Up to 64 inline maps now merge first-wins under cumulative state and successful-resolution bounds; continue with module referrer-policy and fetch-priority provenance.
Post-stabilization priorities
Compatibility
- Expand CSS formatting and painting only in the Flutter formatter.
- Expand Paragraph shaping, writing modes, bidi, selection/caret, and font fallback with exact commit tests.
- Expand images and replaced elements after BrowserCore policy/resource acceptance; decoding and intrinsic rendered geometry remain Flutter-owned.
- Increase WPT coverage with explicit source/runtime versus rendered ownership.
Interaction and accessibility
- Complete pointer, touch, gesture, drag/drop, selection, nested/smooth scroll, and overscroll behavior through commit-bound input.
- Complete Linux IME and accessibility device matrices, then add equivalent evidence for each supported platform runner.
- Keep BrowserCore semantic meaning independent of scene capture while requiring displayed Flutter commits for bounds and pointer-like semantic activation.
Performance and hardening
- Measure formatter build, incremental mutation, Paragraph query, scene capture, bridge queue, and BrowserCore owner-thread latency.
- Enforce release size, startup, memory, and renderer recovery budgets.
- Add process/sandbox boundaries only with an explicit threat model and bounded protocol; do not recreate renderer ownership in native code.
Product and distribution
- Finish browser chrome behavior, downloads, settings, permissions, and session UX after the stabilization gate.
- Add non-Linux runners one at a time with native build, input/IME/AT, package, and sustained smoke evidence.
- Complete signed packaging, update, rollback, provenance, and release channels.
Invariants for all new work
- There is one production renderer: Flutter.
- BrowserCore never fabricates rendered geometry, hit tests, semantic bounds, or screenshots.
- Pointer input names an exact displayed commit and Flutter hit target.
- Render commits and queries are bounded, generation checked, cancellable where blocking, and fail closed when stale.
- Renderer-dependent fixture checks run in the Flutter host; native runners do not claim rendered evidence.
- No compatibility shim may restore deleted WebRender/EGL/frame/texture/Rust layout-paint details.
- Prefer deletion and direct data flow over parallel ownership or abstraction.
Gates
Focused final-cutover proof:
just test-r7
Full composed rendered proof:
just gate-r7
test-r7 checks source/dependency absence, native tests, clippy, C header syntax,
manifest/script validity, Dart formatting/analyze, and the complete
Impeller-requested Flutter suite. gate-r7 first preserves all R5/R6 release,
Cage, fixture, CDP, synchronous-layout, cancellation, and recovery evidence.
A Linux release build additionally requires CMake and the standard Flutter Linux toolchain.
Executable gates and evidence
This file is intentionally not a second roadmap. Product order and future
milestones live in ROADMAP.md; historical phase instructions live
in PLAN.md; measured compatibility lives in
COMPAT.md. This file answers only: “which checked-in command proves
which layer today?”
ADR-022 transition status: R1–R7 are checked in. GUI and automation use one Flutter formatter/commit/painter; synchronous geometry and recovery are landed; the native/Rust renderer, frame transport, raw coordinate input, and obsolete gates are deleted.
Gate index
| Command | Current evidence |
|---|---|
just test-api | R1 renderer protocol v1 DTO/reference-state tests: typed nonzero ids, exact compound revisions and viewport values, bounded snapshot/mutation/resync, atomic commit/presented identity, immutable geometry, opaque handle retirement, bounded hit/text/scroll exchanges, displayed-commit input, and stale/late/replayed semantic-action rejection; model-only, not ABI/Dart/Flutter evidence |
just test-flutter-formatter-impeller | R3 test-only data-oriented formatter over dart:ui Paragraph/Canvas/Picture/Scene and encoded PNG decode, with exact Impeller-requested RGBA hash, run/line geometry, hit/text/scroll/multi-rect semantic commit state, atomic mutation/presentation, failed/superseded-build disposal, stale/resync/reset and explicit release tests; not production cutover or Linux runner backend proof |
just gate-alpha | formatting, all-target/all-feature Clippy, host workspace checks, generated WebIDL/runtime seams, BrowserCore ownership tests, BrowserCore-backed committed fixture runner, and stable crate-boundary allowlist |
just gate-architecture | leaf-crate/frontend dependency rules; CDP has no renderer owner and rendered composition belongs only to Flutter |
just test-flutter-controller | Safe controller and native boundary crate tests: one non-clone BrowserCore/event owner, immediate navigation acceptance, exact terminal events, active-load stop, contexts/profile session, and C ABI unit/integration coverage; not Dart or Flutter proof |
just gate-native-abi | Builds vixen-ffi library forms and runs focused ABI v1 layout/header, opaque handle, bounded UTF-8/JSON command, stable response/event/error, event-sequence, output-buffer ownership, panic containment, and the R2 renderer poll/respond/submit/shutdown surface with bounded async updates/submissions, total in-flight request saturation, deadlines, cancellation, blocked-worker progress, shutdown wakeup, strict correlation, and retained-buffer release; native C ABI evidence only |
just gate-flutter-shell | pinned Flutter/Yaru shell formatting, analysis, unit/widget tests, exact commit presentation/input/Semantics, lifecycle retirement, native bridge smoke, and Linux source evidence; no frame/texture fallback |
just gate-smoke | reviewer baseline: formatting, clippy, host checks, and all host-runnable tests |
just gate-push | hk pre-push integration point: alpha, phase-6 runtime, smoke, and diff checks |
just gate-webidl | generated WebIDL constructor/prototype coverage plus headless/CDP runtime-host integration |
just gate-phase0 | workspace/API DTO and trait-shape foundation |
just gate-phase1 | network/store tests, audit, and security fuzz targets |
just gate-phase2 | deno_core runtime and headless eval seam |
just gate-phase3 | HTML/selector/cascade behavior and CSS fixture profile |
just gate-phase6 | engine host-family tests, WebIDL, page/classic/automation module provenance, bounded multiple import maps and module SRI, strict JSON import attributes, headless runtime, and CDP runtime integration |
npm test | bounded-process, timeout, percentile, /proc parser, hash, and recursive-size unit tests used by the baseline tools |
just wpt-profile <profile> <root> | optional external profile execution after fail-closed validation of the canonical repository, full pinned commit, clean checkout root, and sparse-path coverage |
just test-browser-core | BrowserCore owner/thread/generation proof for contexts, navigation cancellation, DOM/V8, resources, profile state, accessibility meaning, and headless source/runtime adapter; no layout or paint evidence |
just compat-report | current BrowserCore-backed committed fixture/profile counts and per-source/category output |
just fuzz-security | URL, CSP, cookie, and HTML parser fuzz targets at the configured run count |
just audit | cargo audit plus cargo deny check |
just linux-release-smoke | pinned x86_64 Flutter 3.47.1/Dart 3.13 GTK3 release/AOT plus Rust bridge build; stripped ELFs, deterministic archive creation, clean extraction, and Impeller-aware Cage/headless-Wayland launch smoke |
just linux-at-spi-smoke | real release/AOT GTK3 Flutter bundle in Cage's headless Wayland compositor with a fresh BrowserCore profile and local fixture; bounded process-filtered AT-SPI traversal must observe the BrowserCore-derived DOM Basic heading and /proc must show GTK3; Linux native AT evidence, not a screen-reader matrix |
just linux-interaction-smoke | real release/AOT GTK3 Flutter bundle in deterministic headless-window geometry with native-pointer focus → DOM → newer-commit checks; physical address entry visibly navigates to the controlled fixture, native back/forward and reload restore BrowserCore-owned root/nested offsets, a gated FIFO read proves the visible stop control cancels an active navigation and recovers the prior page, wtype drives IBus preedit+commit, and a wlr virtual pointer proves nested wheel ownership/cancellation/root chaining |
just linux-automation-smoke | same release/AOT executable runtime-selects the page-only host under Cage, projects the controlled fixture's renderable full DOM/resolved styles/stable element ids through the renderer protocol, bypasses profile tabs and browser chrome, acknowledges one exact Flutter commit, and writes its direct scene PNG at 320×240 and 480×300; validates Impeller, strict PNG structure/dimensions, real document content, pinned full-scene hashes, and bounded exit; direct scene serialization excludes browser/runner/compositor chrome |
just flutter-cdp-playwright-smoke | release/AOT Flutter host under Cage owns the sole BrowserCore and an in-process vixen-cdp subscriber; focused external Playwright proof obtains layout from Flutter commits, writes stable live DOM/attribute/collection/CSSOM objects, proves parser classic/static-dependency/dynamic-dependency/module/microtask/task ordering, matches same-task and CDP geometry/attributes/nodes, pins each exact scene, routes pointer input through Flutter hit testing, keeps 320×240 and 480×300 targets independent, and forces renderer reset/full-resync to a byte-identical scene; no browser/compositor chrome in direct PNGs |
just flutter-fixture-manifest | one release/AOT Flutter host and BrowserCore execute all 270 fixtures / 2,027 checks in manifest order; each fixture gets an isolated target in the same core, 1,868 native-safe source/runtime checks use typed BrowserCore inspection, while 19 Flutter JS geometry checks, 104 layout boxes, 25 Flutter visual hashes, and 11 exact-pixel references use the matching presented Flutter commit |
just gate-r5 | complete R5 product gate: bounded one-shot scene capture, shared-core external Playwright/CDP input/capture/isolation/loss recovery, and the complete Flutter-hosted fixture manifest |
just test-r6 | focused R6 exact source diff, same-task DOM/style mutation → EnsureLayout → matching commit geometry, repeated-read reuse, Paragraph Range/caret queries, blocked-command broker progress, cancellation/late-reply races, malformed commit, and full-resync recovery evidence across Rust and Dart |
just gate-r6 | complete R6 gate: every R5 rendered fixture/CDP/Cage proof plus test-r6 synchronous layout and recovery evidence |
just test-r7 | R7 absence scans plus native tests, Rust clippy, C header syntax, manifest/script validation, Dart format/analyze, and full Impeller-requested Flutter tests |
just gate-r7 | complete renderer-transition gate: every R5/R6 rendered product proof plus R7 cutover/deletion evidence |
just size-headless | structured logical/allocated size, file count, and SHA-256 for the headless release binary |
just size-flutter-linux | controlled release/AOT build and component-attributed raw-bundle comparison against the checked-in hello-Flutter peer; measurement-only and not FlatPark package evidence |
just baseline-headless / just baseline-headless-json | per-scenario latency and Linux process-memory measurements for committed startup, navigation/runtime, layout, paint, and screenshot controls |
just baseline-flutter-linux / just baseline-flutter-linux-json | release/AOT Flutter exact-commit startup/capture/memory plus serialized mutation/input-to-commit frame timing under Cage; measurement-only, software rendered, and outside gate-push |
just baseline-flutter-linux-hardware / just baseline-flutter-linux-hardware-json | same bounded workload without the software override; fails unless the same Wayland display reports a non-software EGL renderer and records its GPU/driver fingerprint; one-host evidence, not a matrix or budget |
just baseline-profile-growth | opaque temporary profile growth at init/repeated/unique/storage checkpoints with localStorage reopen proof |
just baseline-measure | hermetic local headless scenarios, profile growth, and headless artifact size; measurement-only and outside gate-push |
Evidence rules
- Run the cheapest focused crate test while editing, then the relevant gate above.
- After R7, renderer work must use the Flutter source/commit/query boundary; do not restore deleted native/Rust rendering ownership.
- A pure unit test proves an algorithm. A browser claim also needs a shared-core integration path, fixture/profile, external automation smoke, or GUI smoke.
- Fixture behavior changes update
COMPAT.mdfromjust compat-report; do not hand-invent counts. - ADR-017 frontend ownership migration is enforced by
gate-architecture; subsequent lifecycle work adds cancellation/partition/live-document evidence without restoring direct frontend composition. - Released Linux shell changes use
just linux-release-smoke. FlatPark package submission and verification follow only after the Linux basic-browser gate; an immutable GitHub Release alone does not make registry publishing a current priority. Flutter is the only rendered frontend target and parity concern. just gate-native-abiproves the handwritten C ABI/header/wire/buffer ownership milestone over the same safe controller.just gate-flutter-shelladds Dart, widget, worker-isolate, commit-painter, and live native smoke evidence. It proves physical viewport, pointer/wheel/keyboard routing, monotonic host focus/visibility/lifecycle state, and the bounded BrowserCore-to-Flutter Semantics hierarchy with bounded descriptions, three non-tree relationships, native/authored range actions, live regions, and event-driven full projection refresh.linux-at-spi-smokeadds first native Linux AT evidence;linux-interaction-smokeadds the controlled native IME and basic-navigation vertical. Neither proves complete screen-reader coverage, packages, broader release behavior, or non-Linux GUI support; useFLUTTER_SHELL.mdfor remaining gates.- Size/performance thresholds become gates only after a representative baseline, environment, and comparison method are committed.
- Hosted
ci.ymlruns architecture/native-ABI checks, Node baseline tests, the workspace checks/tests, and the release native interaction/archive smoke; the external Playwright/CDP smoke remains a local/release gate. Its separate security job runscargo auditandcargo deny check.fuzz.ymlruns all four existing fuzz targets on a bounded weekly/manual CI budget and retains crashes. The one-million-iteration local/release command remainsjust fuzz-security.
Current measured anchors
- Compatibility baseline: 270 fixtures / 2,027 checks / 100% passing. R8
reproduced all 1,868 native-safe checks and then the full 2,027-check
release/AOT Flutter-hosted manifest on 2026-07-16.
COMPAT.mdis authoritative. - Post-R7/Yaru Linux x86_64 Flutter raw-bundle reference:
21,398,668-byte hello / 85,377,960-byte Vixen / 63,979,292-byte delta,
plus a 31,913,890-byte deterministic release archive; measurement-only,
not independently reproduced, and not FlatPark package evidence. The Vixen
bundle is 131,560 bytes smaller than the historical pre-R7 report; see
BASELINES.mdfor component and control-version attribution. - Post-R7 release/AOT renderer version-2 references: five software and five
physical AMD/Mesa samples each joined 45 exact interaction frames. Software
median mutation/mouse-release/total-frame values are 15.402 ms / 26.364 ms /
2,587 µs; hardware values are 14.527 ms / 25.269 ms / 2,590 µs. Cage
reported no refresh rate. Single-host, measurement-only, not a budget or
GPU/driver matrix; see
BASELINES.md. - Post-R7 profile-growth reference: five repeated and five unique visits caused
8,192 bytes and 0 bytes of allocated growth respectively; a persisted
65,536-byte localStorage payload added 139,264 bytes and passed reopen.
Single-host, measurement-only, and not a budget; see
BASELINES.md. - R8 native-host checkpoint: the release/AOT Cage corridor passed with real IBus
Mozc preedit/commit in native and contenteditable controls, positive Flutter
AT-SPI editor bounds (8, 187, 40, 20), unchanged native
Focus→ DOM focus → same-document commit 18 → 20, wheel cancellation/scroll/navigation recovery, and clean exit. Single controlled Fedora host, not an IME, assistive-technology, compositor, or device matrix. - External automation contract:
CDP_PLAYWRIGHT_SMOKE.md. - Browser ownership/cancellation vertical:
just test-browser-core(engine, headless, and FFI controller adapters through the production command/event handle). - Release requirements:
ACCEPTANCE.md. - Measurement methods, report schemas, acceptance policy, and current gaps:
BASELINES.md. - Five-platform Flutter GUI contract and gate plan:
FLUTTER_SHELL.md.
When a gate and its description diverge, fix this table in the same change as the recipe. Do not copy already-landed feature inventories back into the roadmap.
Vixen acceptance criteria
Release is done only when every applicable gate below passes. Capability claims
map to fixtures/profiles/smokes and the exact BrowserCore/Flutter renderer path
defined in SPEC.md, COMPAT.md, and ADR-022.
Alpha architecture and delivery order are defined in
PROJECT_DIRECTION.md and ROADMAP.md.
Hard gates
-
One concrete BrowserCore and one
deno_core/V8 runtime; no WebKit fallback or runtime-engine abstraction. - One Flutter Canvas/Paragraph web renderer over an explicitly enabled and evidenced Impeller backend for GUI and rendered automation; Skia fallback is not accepted platform proof.
-
No production
webrender,gleam,GlContext, headless/frame EGL, RGBA frame ABI/pools, pixel-buffer texture presenter, fallback painter, or second screenshot path after renderer cutover. - BrowserCore owns navigation, DOM/runtime, Stylo computed styles, network/security, profile state, resource acceptance, web-event semantics, and accessibility meaning; Dart owns no durable DOM/browser state.
- Render revisions, mutation/full-resync payloads, atomic commits, presented ids, geometry/text/scroll/semantic queries, opaque Flutter-side hit-test handles, input targets, and semantic-action targets with advertised action generations are bounded, versioned, and stale-safe.
-
Same-task DOM/style mutation followed by geometry uses cancellable,
deadline-bounded, deadlock-safe
EnsureLayoutand returns the matching Flutter commit. - GUI, chrome-less Flutter host, CDP layout/input/screenshots, visual/layout WPT, and native Semantics use exact commits from the same renderer.
-
fixtures/manifest.jsonand every declared external profile are green;COMPAT.mdpublishes measured counts and limitations. -
just audit,just check, hk pre-push, relevant fuzz targets, andgit diff --checkpass from a clean checkout. - No non-test module over 1,000 lines without an immediate named split.
- Release artifacts, startup, memory, capture latency, and profile growth are measured under the accepted baseline/regression policy.
Renderer-transition acceptance
Protocol
Done when R1/R2 from ROADMAP.md prove:
R1's dependency-free DTO validation and R2's strict C/Dart dedicated broker are landed. R3 adds the formatter consumer, R4/R5 connect displayed input and rendered automation, and R6 connects production BrowserCore mutation flushes and synchronous layout. R7 deletion is the remaining renderer-transition hard gate.
- compound revisions include context/document/source/style/viewport/resource generations;
- incremental batches require exact base revisions and deterministically request bounded full resync after a gap;
- malformed ids, unknown resources, non-finite geometry, excess depth/count/ bytes, truncation, stale commits, and double release fail closed;
- forged, unknown, stale-commit, stale-generation, and replayed Semantics actions fail closed;
- C ABI and Dart models round-trip the same wire values; and
- the renderer broker remains serviceable while the originating BrowserCore/V8 command waits, with cancellation, timeout, and shutdown proof.
First renderer vertical
Done when one controlled background/text/PNG document proves, from one commit:
- Vixen Dart CSS box/inline formatting over BrowserCore computed inputs;
- Flutter Paragraph shaping/wrapping/range/caret geometry;
- Canvas pixels, paint order, clips, transforms, and image pixels;
- returned immutable basic geometry and renderer-authoritative hit testing;
- scroll limits/offsets and semantic bounds;
- scene capture without browser/compositor chrome;
- mutation update, stale rejection, renderer loss, and full resync; and
- no production claim while the old renderer still serves normal browsing.
Interactive renderer vertical
Done when the displayed commit drives pointer target validation, cancelable wheel/ key/script scrolling and returned scroll state, find/text/caret ranges, viewport/ zoom revision, native Semantics bounds/actions, lifecycle hide/resume, and stale scene suppression through widget/core/ABI tests plus Cage smoke.
Implemented: just gate-flutter-shell covers the formatter/coordinator/native
ABI identities and just linux-interaction-smoke correlates accepted and canceled
DOM scroll effects with exact presented Flutter commit ids in the release process.
The official stable engine exposes process-filtered names through GTK3. The
native Semantics action and transformed-screen-origin clauses remain open until
the migrated release corridor records fresh evidence; current interaction proof
uses native Wayland input and does not weaken that release criterion.
Chrome-less renderer checkpoint
The first R5 checkpoint is implemented when just linux-automation-smoke runs
the same release/AOT bundle under Cage at two exact viewports, with no browser
widgets, native decorations, legacy frame capture, or compositor pixels in the
PNG. Capture must occur only after exact Presented acceptance, fail if that
commit changes, use bounded explicit URL/viewport/output configuration, and close
the sole BrowserCore on success or fail the process after bounded shutdown grace.
This checkpoint is green; at the time it landed, full R5 still required the
fixture-manifest, layout, CDP/Playwright screenshot/input, independent-target,
mutation, and renderer-loss evidence now covered below.
The follow-up renderer-source checkpoint is also green: the exact scene is built from bounded renderable DOM topology, viewport-resolved styles, accepted images, stable BrowserCore element ids, disjoint renderer text ids, and semantic/scroll metadata. The native bridge smoke proves a Flutter hit target is commit-bound before BrowserCore DOM dispatch.
The shared-core rendered CDP checkpoint is green through just flutter-cdp-playwright-smoke. The release host owns one BrowserCore and a
non-owning CDP subscriber; Playwright screenshot, commit geometry, and pointer
input all cross the Flutter renderer. Two live targets retain separate viewports
and DOM state, before/after mutation scenes differ, direct scene pixels exclude
chrome, and forced renderer reset recovers through a full snapshot to the exact
prior scene.
Full R5 acceptance is green through just gate-r5. just flutter-fixture-manifest keeps every fixture's ordered document/runtime/style
and rendered assertions on one target in the release Flutter host's sole
BrowserCore. The result is 270/270 fixtures and 2,027/2,027 checks: 1,868
native-safe BrowserCore source/runtime checks plus 19 Flutter geometry-dependent
JavaScript checks, 104 exact Flutter layout boxes, 25 Flutter visual hashes, and
11 exact-pixel Flutter references. The native fixture runner does not claim
rendered evidence.
Synchronous geometry
Done when tests cover:
DOM/style mutation
→ Stylo flush
→ RenderMutationBatch
→ EnsureLayout(required revision)
→ matching RenderCommit
→ synchronous DOM/CSSOM/CDP geometry
No browser mutex is held while waiting; Flutter cannot re-enter BrowserCore; navigate/stop/close/shutdown and deadline cancel the request; late replies are inert; repeated geometry reads reuse the accepted commit.
Implemented: just test-r6 proves exact full-source-to-mutation diffs,
same-task style mutation followed by two reused element geometry reads, Range
boxes and collapsed caret geometry through the commit's Paragraph query handle,
navigation/stop/deadline cancellation, a broker pump independent of blocked
browser commands, malformed-commit and renderer-resync recovery, inert late
replies, and same-isolate reuse. just gate-r6 composes that focused evidence
with the complete R5 fixture/CDP/Cage gate.
Cutover and deletion — implemented
Source/dependency/gate searches prove the full R7 inventory is gone:
WebRender/gleam, GlContext, both EGL paths, image upload, frame ABI/tokens/pools,
the Dart frame worker, texture presenter/plugin and recovery tests, superseded
Rust layout/paint, duplicate scale/hit/scroll/text/semantic projections, obsolete
fixtures/gates/docs/dependencies, and renderer-internal CLI flags. GUI and
chrome-less automation share one renderer implementation, and no compatibility
flag/API preserves deleted details. Any retained pure Rust CSS algorithm has an
active Dart consumer through a named stable formatter contract, focused
cross-language tests, and documented evidence that reuse is simpler than
deletion; no Rust geometry, text measurement, hit testing, or paint authority
survives.
just test-r7 checks that inventory, all native source/runtime suites, WPT
ownership routing, C header syntax, Rust clippy, Dart formatting/analyze, and the
full Impeller-requested Flutter test suite. just gate-r7 preserves the complete
R5/R6 rendered fixture/CDP/Cage evidence before running the deletion gate.
Browser capability acceptance
HTML, cascade, and selectors
- HTML parser/serialization profiles are green.
- Stylo/selectors profiles cover the supported selector/cascade/computed-value surface.
- A computed-style mutation creates the correct renderer source revision; stale commits cannot answer inspection.
DOM/runtime/events/forms
- DOM, events, forms, history, storage, and selected Web API profiles run through
the live
deno_corerealm and BrowserCore document. - Script mutation drives a visible Flutter commit and CDP observes the same nodes.
- Focus/event/form-validation ordering pinned by
SPEC.mdremains exact.
Layout and paint
The Flutter-hosted Vixen formatter passes the published layout/paint profile for the claimed subset. Nested geometry, clips, transforms, scroll, hit testing, text/range geometry, semantic bounds, and pixels agree by commit without frontend coordinate repair. Unsupported tables/floats/fragmentation/writing modes remain explicit until promoted by measured tests.
Networking/security/storage/downloads
vixen-netpolicy and transport tests are green, including URL/private-host, cookies, CSP, CORS, mixed content, referrer, integrity, nosniff, and cache rules.- ES-module dependencies use the shared external-resource boundary with BrowserCore request ids, redirects/final URLs, policy, profile cookies/cache, bounded diagnostics, graph limits, and cancellation before V8 evaluation.
- Cross-origin module roots, dependencies, and redirects pass CORS before V8 exposure; default graphs omit credentials and explicit credentialed graphs require exact origin/credential permission throughout the graph.
- Eligible exact-URL HTTP(S) module roots and dependencies conditionally revalidate profile entries; a 304 restores bounded source only before current CORS/status/strict-MIME policy, and cache-disabled contexts bypass reads and writes.
- Up to 64 bounded inline import maps merge into one 512 KiB normalized state.
Earlier imports/scopes/integrity rules win conflicts, a bounded successful
(referrer, specifier)set prevents later maps from changing prior results, and exact/prefix/URL-like/scoped dependencies use the same policy-bound loader. Exact-URL integrity verifies top-level fallback and static/dynamic dependency bytes before V8 or profile effects. Malformed maps, cumulative overflow, and external maps fail closed without partial registration. - Dynamic imports originating in page modules, parser classics, and BrowserCore automation retain exact source/graph/import-map/credentials policy, share cumulative bounds, resolve redirected children from accepted final URLs, and abort without late DOM/profile/lifecycle effects. Exact static/dynamic JSON import attributes require strict file/HTTP JSON typing; unknown keys and text/bytes/custom types fail before transport.
- External classic/module roots verify authored SHA-2 SRI over accepted raw bytes
before V8, response-cookie commit, or cache insertion; mismatch emits a stable
request-scoped
integrityfailure, and cross-origin classic SRI requires CORS. An authored root attribute takes precedence over import-map fallback metadata. - Policy runs before resource bytes/handles cross to Flutter.
- redb profile tables preserve partitioning, bounds, recovery, clear-data, and reopen behavior.
- Download transfer, filename, destination, cancellation, persistence, and UI handoff are complete for any download claim.
Accessibility
BrowserCore-authored roles/names/values/states/relationships/focus/actions combine with Flutter bounds/text geometry only for the displayed commit. Native AT smoke proves content and actions; pixels alone do not satisfy accessibility.
CLI, CDP, WPT, and automation
- Every documented flag in
SPEC.mdworks with stable errors. - Screenshot, visible extraction, coordinate input, layout CDP, and visual WPT use the chrome-less Flutter host; text-only fast paths fabricate no geometry.
- CDP supports the declared methods, independent contexts/targets, reliable waits, exact-commit input/layout/screenshots, runtime handles, network/lifecycle/ console/dialog events, permissions, downloads, and bounded traces.
- WPT reports overall/category/source/source×category counts and uses production BrowserCore plus the Flutter renderer for every geometry/pixel check.
- External Playwright smoke passes against the same renderer and BrowserCore.
Shell and Linux product
Manual and automated Linux smoke covers:
- tab create/close/duplicate/reopen and session restore;
- address/search, back/forward/reload/active stop;
- find, zoom, downloads/permissions, settings/privacy, diagnostics, and errors;
- visible Flutter-rendered page content, input, scrolling, text/IME, viewport/ scale, lifecycle, renderer loss, and recovery;
- native Wayland only; X11/XWayland fail explicitly;
- BrowserCore state ownership and exact displayed-commit input/Semantics; and
- native keyboard/IBus, virtual pointer, AT-SPI, Cage launch/capture, and release archive evidence.
FlatPark publication remains after basic browser behavior, host services, and release evidence. Registry reach never outranks browser correctness.
Platform gates
A framework-supported platform becomes Vixen-supported only after the latest
stable major OS gate in FLUTTER_SHELL.md:
- Linux first: final mutation/commit renderer, GUI and chrome-less host, Wayland input/IME/AT, host services, deterministic archive, compatibility, size, memory, startup, frame, and capture evidence.
- macOS/Windows: native BrowserCore/V8 and the same broker/formatter, input/ IME/accessibility, host services, signing/packaging, capture, and architecture- specific measurements.
- Android: pinned V8 source/toolchain, renderer broker, lifecycle/process recreation, touch/IME/accessibility, host services, capture, and split-ABI proof.
- iOS Simulator:
aarch64-apple-ios-simBrowserCore/V8/Flutter renderer, JavaScript/WebAssembly, simulated lifecycle/input/accessibility/host services, capture, and repeatable Xcode build. Physical iOS requires a new decision. - WebAssembly: the single V8 path passes the same API, malformed-module, resource-limit, and conformance evidence on every declared target.
Size and performance gates
Measure separately:
- like-for-like hello-Flutter;
- Flutter+Vixen GUI;
- chrome-less rendered automation host; and
- any text-only launcher/client.
Reports attribute Flutter engine/ICU, Dart AOT/formatter/assets, native runner/ plugins, BrowserCore/Rust, V8/ICU/snapshots, resources, packaging, and symbols. Deleted native renderer dependencies and symbols must remain absent. Reports include locks/revisions, commands, hashes, architecture, AOT/strip/LTO settings, compressed/unpacked/install sizes, startup, memory, layout/commit/frame/capture timings, and comparison statistics.
Adopt warnings before hard numeric budgets. Rebaseline only for a documented product/dependency tradeoff; never hide growth by changing attribution.
Release ladder
- Renderer transition: every R1–R8 gate passes and transitional renderer code is deleted.
- Alpha: one BrowserCore and Flutter renderer support independent contexts, live mutation, synchronous geometry, input, inspection, Semantics, and cancellation without stale commits.
- Beta: the controlled Linux corridor is usable in GUI and chrome-less automation with published compatibility/performance/recovery evidence.
- v1.0: daily-driver corridor, security/release operations, host integration, automation, accessibility, and every declared platform/capability claim satisfy their gates.
Post-v1 replacement work follows ROADMAP.md; no fixed version number overrides
measured user/site impact.
Vixen compatibility target
This is the honest v1.0 target matrix. It is not a claim of full Firefox or full WPT compatibility. Vixen delegates focused parser, cascade, runtime, and native rendering primitives where that improves correctness and size. All measured rendered compatibility now uses Flutter-owned formatting, geometry, semantics, and scenes. R7 deleted the native/Rust renderer path. Every supported CSS semantic remains fixture/WPT-gated.
Current measured committed fixture baseline
As of 2026-07-16, fixtures/manifest.json contains 70 local fixtures plus
200 imported smoke fixtures:
| Category | Fixtures |
|---|---|
| css | 17 |
| css-cascade/css-values | 50 |
| cssom-view | 1 |
| dom | 25 |
| dom-core | 50 |
| events | 1 |
| flexbox | 5 |
| forms | 28 |
| grid | 5 |
| layout | 9 |
| layout block/inline/position | 6 |
| network | 2 |
| paint | 4 |
| paint/ref-equivalent | 8 |
| security | 9 |
| selectors | 50 |
| Total | 270 |
Total manifest checks: 2027.
Current check mix:
| Check type | Count |
|---|---|
selector-count | 398 |
selectors-exact | 223 |
title | 269 |
js-eval | 597 |
computed-style | 173 |
element-attribute | 132 |
layout-box | 104 |
body-contains | 68 |
visual-hash | 25 |
no-critical-diagnostics | 22 |
ref-equivalent | 11 |
dom-nodes-range | 1 |
min-nodes | 1 |
selector-match | 3 |
This local fixture set is release-blocking and must remain 100 % green.
R8 native-path reproduction on clean revision e224bf6 ran just compat-report: all 270 fixtures and all 1,868 native-safe BrowserCore checks
passed. R8 release-host reproduction then ran just flutter-fixture-manifest's
exact command against the clean post-R7/Yaru release bundle: all 270 fixtures and
all 2,027 checks passed. The host summary reports 140 direct rendered checks
(104 layout-box, 25 visual-hash, and 11 ref-equivalent); the remaining 19
flutter-js-eval checks also run only in that Flutter host. None is relabeled as
native evidence.
The layout category currently includes normal-flow, inline-flow, positioned,
flex row/column, grid, overflow coordinate/paint, and fragment-backed text paint
fixtures with Flutter-commit layout-box and visual assertions. The paint
category includes 11 ref-equivalent checks against exact Flutter scene pixels.
The harness now reports overall, per-category,
and local/imported source×category pass rates. Its adapter now creates production
BrowserCore contexts, so fixture snapshots/selectors/styles/evaluation/reference
rendering/pixel capture share typed document/runtime generations and persistent
per-context V8 realms rather than constructing harness-owned Pages or runtimes.
Imported upstream WPT
layout/paint coverage is still tracked separately below. Imported selector smoke
has reached the
50-fixture target,
including focused :has() child/descendant/adjacent-sibling/general-sibling and
selector-list smoke plus attribute operators/flags, class/id matching,
structural and typed structural pseudos, link/form/read-write/autofill/defined
pseudos, negation/list pseudos, grouping de-duplication, and document-order
coverage. Local CSS computed-style coverage now includes the Milestone 1
advanced cascade seam: @media, @supports, @layer, inherited custom
properties, var() fallback, and CSS-wide keyword projection through Page.
Imported css-cascade/css-values smoke has reached the
50-fixture target, including specificity/source order, important and inline
precedence, combinator/attribute-operator matching, structural/link/form pseudo
selectors in cascade, :is()/:where()/:not()/:has() selectors,
selector-list splitting, custom properties, declaration recovery, comments,
math/color/gradient/transform/shorthand values, and quoted/nested/function
declaration values. Imported DOM-core smoke has reached the 50-fixture target,
including query/getElementById/querySelectorAll, document/root/body access,
tag/class/wildcard collections, attributes, reflected host properties, text
aggregation, parent/child/sibling traversal, null relation checks, document URL,
forms collection length, matches(), logical selectors, and :has()-backed
matching. Imported forms smoke has reached the 25-fixture target across
reflected/default form/control properties, labels, radio/checkbox/select states,
textarea text, form tree traversal, repeated names, and :has() form selectors.
Local Phase 6 fixtures now also assert runtime/Page-backed js-eval projections for
getComputedStyle(), document/navigator state (documentURI/baseURI,
focus, and active-element shape included), op-backed in-memory Web Storage
mutation with key/value validation and quota errors,
Event/CustomEvent/dispatchEvent() smoke, the pinned
focusout → focusin → blur → focus transition with relatedTarget,
Page-owned active-element restore, CSSOM CSS.supports() plus retained live
document.styleSheets / CSSStyleRule / CSSStyleDeclaration read-only objects,
viewport/window state, DOMRect geometry via getBoundingClientRect() /
getClientRects(), client/offset/scroll metrics, getBoxQuads(), Range
rectangles, Geometry
Interfaces value constructors (DOMPoint/DOMRect/DOMQuad/DOMMatrix), DOM
ancestry/core-node projections (closest(), nodeName/nodeType,
ownerDocument), anchor URL decomposition/reflection, DOMParser, atob/btoa, classList/
relList/sandbox, dataset, ValidityState/checkValidity(), FormData
entry-list and iterator projection plus runtime/CDP form submission by page node
id with successful submitter overrides, runtime form reset/default-state restore,
meta/content reflection,
innerHTML/outerHTML,
URL.canParse(), data: URL parsing, new URL()/URLSearchParams constructor and iterator seams,
TextEncoder/TextDecoder (encodeInto and constructor options included),
<img>.currentSrc plus image alt/dimension/loading/decode reflection, inert
media element state (HTMLMediaElement/audio/video constants included),
resource element reflection (link/style/script/source), single-range
Range/Selection state with Page-owned element-boundary restore, direction,
point queries, same-container clone/extract/delete/insert/surround operations,
and selectionchange delivery, live history accessors/actions,
details/dialog open-state reflection,
miscellaneous HTML reflected attributes for lists, quotes, embedded content, and
table cells,
progress/meter numeric state,
inert Canvas 2D context smoke,
form-associated reflected attributes and editing helpers,
read-only table collections/indexes,
HTMLElement interaction/global reflected attributes,
text track / track-element state,
inert OffscreenCanvas/ImageData/ImageBitmap/Path2D APIs,
minimal ShadowRoot/DocumentFragment smoke,
template content and slot assignment shape,
DOM construction/serialization helpers,
structuredClone, CDP Runtime.awaitPromise over stored promise handles,
MutationObserver lifecycle, TreeWalker/NodeIterator traversal, Headers
iteration, Blob/File, read-only Request/Response state with forbidden
header filtering, Response.error() / Response.redirect() / Response.json(),
op-backed fetch() HTTP(S) status/header/body reads plus URL-policy/private-host
rejection with CDP Network.loadingFailed diagnostics, credential-correct CORS,
bounded origin/target/credentials-partitioned preflight caching (including
effective CDP extra headers), strongest-algorithm Request SRI verification before
exposure/cache insertion, AbortSignal,
URLPattern, CDP lifecycle opt-in (init/commit/DOMContentLoaded/load),
Performance timing shape, matchMedia(), Permissions API query state,
Notification permission state, and StorageManager estimate/persisted state backed
by profile/storage records before the remaining host-object swap; Encoding API constructors,
Web Storage mutation, focused fetch() success/blocking checks, sequential
global/storage persistence across Runtime.evaluate, focused document/Element
snapshot host-object evals and read-only DOMTokenList property reads are also
exercised directly through the persistent deno_core runtime seam.
HTMLElement.dataset has since moved off that frozen projection: its stable live
DOMStringMap reflects external attributes and routes assignment/deletion through
the authoritative DOM mutation, Stylo, and renderer-source path.
Element.classList now also retains a stable live DOMTokenList identity across
external and list-driven class writes, with current token reflection and the
same authoritative mutation/cascade path. HTMLAnchorElement.relList retains
the same identity and behavior across rel writes, as does
HTMLIFrameElement.sandbox across valid sandbox-token writes. These are all
attribute-backed token-list families currently hosted by the runtime.
HTMLElement.style now likewise retains one live inline CSSStyleDeclaration
across external attribute replacement and declaration API writes, using the same
authoritative mutation/cascade path.
Element.attributes now retains a live NamedNodeMap, and attached Attr
objects retain identity, reflect external writes, and write through Attr.value.
Detached Attr values, Document.createAttribute, replacement, removal,
reattachment, and in-use rejection now share that same authoritative path.
Structural childNodes/children, document/form/select/table collections, and
getElementsBy* results are retained live objects; querySelectorAll remains a
static result by design.
Static parser-discovered PNG <img src> has one resource-to-pixel vertical.
BrowserCore applies exact generations, URL/CSP/mixed-content/redirect policy,
cookies/cache, response MIME/status, and compressed/dimension/decoded limits
before exposing accepted bytes to Flutter. A 2×2 four-colour fixture proves exact
Flutter scene pixels. This does not yet claim dynamic image loading, complete
srcset/picture, animated PNG, JPEG/WebP/GIF, SVG image documents, broad
intrinsic replaced-element sizing, or image events.
Runtime platform smoke now additionally covers secure crypto.getRandomValues() /
randomUUID(), async Clipboard text and ClipboardItem shape, MessageEvent,
MessageChannel, BroadcastChannel, first-callback IntersectionObserver /
ResizeObserver geometry, and a fail-closed WebSocket close path. Imported
smoke fixtures now also seed
block/inline/position layout, flexbox, grid, and Flutter-scene
ref-equivalent paint; imported layout smoke covers auto margins, border-box sizing, inline
flow, flex reverse/gaps, and grid minmax()/fractional row/gap cases. Imported
paint smoke now covers currentcolor, overflow clipping, positioned boxes,
flex/grid backgrounds, and nested background/text scene equivalence.
Current automation smoke baseline
The external Playwright smoke covers connect/target/page/runtime/DOM/input/
network/dialog/screenshot/history/content/script/style/binding paths plus
browser-context permission grant/reset, bounded Chromium JSON tracing through
CDP IO streams, idle stop-loading behavior, and stable protocol errors. CDP
permission overrides are exact-origin or wildcard scoped and do not mutate
persisted user decisions. Trace records contain method/timing/session/success
metadata only, not expressions, request headers, form values, or page text.
R8 reran the release/AOT Flutter-hosted smoke after cutover: two target
viewports remained isolated, Flutter geometry/input and before/after scene pixels
agreed, target switching preserved the first scene, and forced renderer reset
recovered by full resync to byte-identical pixels. The first A1 extension now
also writes a live dataset property, observes the attribute-selector-driven
140×32 box synchronously, matches later CDP DOM attributes/geometry, and pins
renderer-specific before/after Flutter scene hashes. The second through sixth A1
extensions retain classList through the click mutation, a real anchor's
relList through a visible rel-selector mutation, and a real iframe's sandbox
through valid token writes, then retain inline style through external and API
writes and attached attributes through Attr.value. Page and CDP views match
the resulting geometry; every exact scene recovers byte-identically after
renderer reset.
The seventh extension retains empty structural collections across the rendered
click mutation, observes the same dynamic node through live indexed/named access
and CDP, and preserves the existing exact click hash.
The eighth extension retains the author stylesheet/list/rule/declaration object
graph across every earlier stage, then reflects one style-element rewrite in
Stylo, synchronous geometry, CDP, and exact Flutter pixels at
b09bce0ee8acf5ac3b40a2190241a6592880a3e47615c030469b2a887d118f1d.
The ninth extension exercises detached Attr replacement/removal/reattachment,
in-use rejection, synchronous geometry, CDP agreement, and exact Flutter pixels
at 92181acffcd1e39ac9720c8edeeba2c148034a89f61297652dc948306f3af052.
The tenth extension executes parser-discovered inline/external modules after
classics with per-script/module/task microtask checkpoints, top-level await,
bounded real task queues, cancellation, and post-load pumping. Its module-owned
120×32 target agrees with CDP and exact Flutter pixels at
faa3c863350c742bdeb38338bca09307a4db49e6f7bb7a3f4e6d73eef60ae2fa.
The first A2 extension imports a real file dependency in that same rendered
corridor. Same-origin/file static dependency graphs now share BrowserCore
request ids, redirect/final-URL policy, profile cookies/cache writes, bounded
diagnostics, and stop cancellation while preserving the exact scene hash.
The second A2 extension applies CORS to cross-origin HTTP(S) module roots,
dependencies, and redirects before V8 exposure. Default graphs omit
cross-origin credentials; crossorigin="use-credentials" requires an exact
credentialed response and is inherited by dependencies. Cache
reads now conditionally revalidate eligible exact-URL HTTP(S) roots and
dependencies; matching 304 responses reuse bounded raw bytes only after current
CORS/status/strict-MIME policy, while cache-disabled contexts bypass reads and
writes. Freshness reuse, redirect aliases, and full Vary still fail closed.
One bounded inline import map before module discovery now supports exact, prefix,
URL-like, null-blocking,
and scoped mappings plus import.meta.resolve() through the same policy-bound
loader. Up to 64 inline maps may appear before or after module discovery and
merge into one normalized state capped at 512 KiB, 2,048 mappings, 128 scopes,
and 2,048 integrity entries. Earlier conflicting imports/scopes/integrity entries
win with bounded warnings. A shared 2,048-entry/1 MiB successful-resolution set
keeps each (referrer, specifier) result stable; parser-position static graph
snapshots cannot be rewritten, while later dynamic imports and automation use
the latest map. External, malformed, cumulative-overflow, and oversized maps
fail closed. URL-like relative integrity keys resolve from each map's base.
Static/dynamic graph dependencies and top-level modules without an authored
integrity attribute verify mapped SHA-2 metadata over accepted raw bytes before
V8, cookies, or cache insertion. Non-object/non-string/bare-URL forms and
normalized duplicates reject the whole map.
Dynamic import() from parser-discovered page modules, including later retained
module functions and document tasks, keeps the originating graph's import map,
credentials/policy, cumulative limits, redirect base, cache/profile effects, and
cancellation. Parser classics and BrowserCore automation evaluations now use
the same loader with exact source/document policy: mapped file imports execute,
redirected classics resolve relative imports from the accepted final URL, and
numeric request ids plus existing graph/cancellation bounds remain intact.
Committed source-only fixture identifiers remain usable for ordinary evaluation,
but dynamic imports from a non-URL realm fail without transport.
Exact static and dynamic type=json imports now use that loader with strict
.json file or JSON HTTP MIME policy. Unknown keys and text/bytes/custom types
fail before transport; a bad MIME creates no profile cache row. External
classic/module roots also enforce authored SHA-2 SRI over raw response bytes;
mismatch executes nothing and commits no response cookie/cache effect.
Cross-origin classic SRI sends document origin and requires CORS before hash
verification. Child frame globals and documents remain unavailable rather than
fabricated until A3.
Page fetch()/XHR and module resources now share bounded private-cache
decisions. Exact effective final-hop request headers select simultaneous
independently bounded Vary representations. Response Date/Age plus
max-age or Expires determine explicit freshness; request no-store,
no-cache, max-age, min-fresh, and max-stale constrain reuse and insertion.
Stale/no-cache entries require validators, and wildcard/malformed/oversized
variants plus no-store responses are not reused. Focused runtime
proof fetches en → fr → en, performs exactly two network requests, and
reuses the first representation; a fresh two-context module root/dependency
graph also reuses the profile cache. Current
CORS/integrity/status/MIME/body policy still runs before exposure. Heuristic
freshness remains unsupported. Fresh cacheable permanent same-origin 301/308
aliases reuse one final representation while preserving final URL, redirect count, module
relative-import base, and network diagnostics. Temporary/cross-origin redirects
remain live-only rather than bypassing response policy.
HTTP bodies are now read incrementally with the configured cap checked before
buffer growth. Ordered response/progress/completed diagnostics include exact
chunk/cumulative/final bytes and reach BrowserCore, C ABI output, module events,
and CDP Network.dataReceived/loadingFinished. Response.body and Blob expose
bounded ReadableStream readers with one-shot body-use behavior; XHR emits typed
upload/download progress and terminal events. Pre-aborted fetch rejects with the
first signal reason without network I/O. Active page abort now cancels the owned
transport and retains the exact first JS reason; XHR abort cancels the same
request and suppresses stale send completions. Realm teardown, BrowserCore
stop/navigation, and deadlines also cancel without partial profile effects.
For ordinary same-origin or CORS responses, fetch() now resolves after final
redirect URL/CSP/mixed-content/CORS policy accepts the response head. Its body
reader receives raw chunks before transport completion through an eight-message
backpressured channel; XHR exposes headers at that boundary and enters loading as
chunks arrive. Body completion waits for bounded cache/cookie commit, and an
abort after resolution disconnects transport and rejects the body with the exact
signal reason. Integrity-bearing fetches, conditional 304 revalidation, and
opaque no-cors fetches remain intentionally buffered. Cloning or teeing an
active network body is unsupported and throws rather than creating an unbounded
or policy-detached consumer.
CDP targets now map to independent BrowserCore contexts/runtimes and share only
profile-scoped state. BrowserCore source navigation is asynchronous,
generation-checked, and directly cancellable; deterministic stop/supersede,
redirect/stop, reload, history-traversal, and parser-stage race tests force stale
work and prove no stale document/history/cookie commit or terminal success event.
The CDP WebSocket path uses one event pump while navigation-producing requests are
pending. Page.navigate, Page.reload, Target.createTarget, cross-document
history traversal, and runtime/input-triggered navigation therefore leave the
same connection available for Page.stopLoading or unrelated commands. Exact
ordered BrowserCore navigation ids correlate multi-action evaluations, and claimed
abandonment records prevent late outcomes from affecting later requests. Gated
socket tests cover navigate/reload, history, multi-action runtime navigation, and
non-blocking target creation. Configured initial-URL loading still settles before
socket acceptance by design. Configured and parser-discovered scripts yield
between items; a committed author exception emits
Runtime.exceptionThrown, later independent scripts continue, and normal load
settlement follows. Individual V8 jobs are deadline-bounded, failed/timed-out
evaluations discard deferred DOM mutations before isolate reuse, and parser-discovered
external classic-script reads are generation-cancellable. Navigate/reload/stop/
close commands snapshot and interrupt the exact active runtime generation before
the deadline; interrupted mutations/effects are discarded, the cancellation is
not reported as a page exception, and the isolate remains reusable. Runtime
fetch() and CORS preflight waits also return promptly on that signal; the
worker-local cancellation path drops the in-flight reqwest future, joins the
worker, and cannot commit cookie/cache state. Gated peers observe the fetch and
preflight connections close before sending a response. Runtime construction and
other local native host calls remain open. Parser-discovered non-alternate
<link rel="stylesheet"> now uses the same cancellable bounded text-resource
worker before author scripts. Relative file and HTTP(S) sheets apply in document
order to Page cascade/renderer source and refreshed runtime computed-style hosts. Redirect hops recheck style-src, mixed-content, and URL policy; accepted
HTTP responses pass status/nosniff checks before cookie, bounded profile-cache,
or style commit. A checked-in file fixture proves visible red 120×40 output, and
gated HTTP/supersede tests prove request-id events and rejection of late cookie,
cache, and style commits. Link media is currently limited to absent/all/
screen; alternate sheets, dynamic links, @import, SRI, cache reuse/freshness,
and complete external-sheet CSSOM objects remain unsupported. There is still no
HTTP download manager or Playwright context-tracing archive implementation.
Current Flutter shell smoke baseline
The Linux shell uses one BrowserCore and one Flutter renderer under native Wayland. Normal GUI, page-only automation, rendered CDP, Playwright, and the fixture manifest share the same formatter/commit/painter implementation.
Current evidence covers exact source revisions, full resync and mutation batches, block/inline/flex/grid formatting, Paragraph geometry, accepted PNG resources, commit-bound hit testing, pointer/key/text input, semantic actions and bounds, root scroll commits, scene PNGs at multiple viewports, renderer reset/recovery, and same-task element/Range/caret CSSOM geometry. Hidden/stale/missing commits fail closed with no native pixel fallback.
The C ABI has no frame descriptor or raw coordinate input. Pointer commands must carry an exact displayed-commit query and optional Flutter hit target. BrowserCore accessibility snapshots carry semantic meaning but no fabricated layout bounds; Flutter commits supply displayed semantic geometry.
fixtures/manifest.json keeps source and rendered assertions together. Native
WPT runs source/runtime checks only. Renderer-dependent JavaScript is tagged
flutter-js-eval; layout-box, visual-hash, and ref-equivalent are also
Flutter-only. just flutter-fixture-manifest remains the complete rendered
compatibility measurement.
Known shell gaps remain broader device/IME/AT matrices, non-Linux production runners, full CSS and text shaping breadth, advanced nested/smooth scrolling, GPU/compositor recovery on physical systems, process isolation, performance/size budgets, packaging, and sustained release evidence.
WPT target profile
Full upstream WPT is too broad to summarize honestly with one percentage at
v1.0. The release contract is a curated, imported WPT profile with measured
pass counts by category. Small, Vixen-minimized upstream-derived smoke fixtures
may live beside local fixtures and remain recorded in fixtures/manifest.json.
Larger upstream slices should use committed WPT profile JSON plus an ignored,
pinned upstream checkout (for example .tmp/wpt/) so review diffs contain only
the selected paths/checks/provenance, not vendored WPT source files. Both paths
feed the same vixen-wpt check types and reporting.
| Area | v1.0 target | Expected achievability | Notes |
|---|---|---|---|
| HTML parsing/tree construction | Broad smoke subset green | High | html5ever carries parser behavior; Vixen must preserve node ids/tree shape. |
| Selectors | Modern selector subset green | High | Backed by Stylo/selectors; include combinators, attributes, :is, :where, :has, form/link pseudos. |
| CSS cascade/computed values | Inline plus one external stylesheet vertical green | High after full Stylo slice | Compact cascade is temporary; external-sheet media/import/CSSOM breadth and full Stylo remain. |
| CSS layout: block/inline | v1 visual/ref subset green | Medium | Flutter-hosted Vixen formatter; start with normal flow, margin/border/padding, Paragraph-backed inline lines. |
| CSS layout: flex/grid | Useful common-case subset green | Medium | Vixen Dart formatting contexts over Flutter primitives; full WPT edge coverage is post-v1. |
| CSS layout: tables/floats/fragmentation | Not v1 release-blocking | Low for v1 | Document as unsupported/partial until implemented. |
| DOM Core | Traversal, attributes, token lists, ranges, mutation observer subset green | Medium | Vixen-owned Web APIs over deno_core host extensions after the ADR-014 migration. |
| Events/forms/history/storage | Selected behavioral subset green | Medium | Gate by fixtures from SPEC invariants and imported WPT cases. |
| JS language | Use V8/deno_core language coverage, not WPT percentage | High for language | Web API exposure remains Vixen-owned and separately gated. |
| Paint/ref tests | Flutter scene/commit visual subset green | Medium | One formatter/Canvas path; pixels, geometry, hit/text/scroll, and semantic bounds share a commit. |
| Media/WebGPU/WebRTC/service workers | Out of scope for v1 | Not targeted | Deferred by ADRs / acceptance post-v1 scope. |
Release-blocking WPT goals
For v1.0, Vixen should be able to claim:
- 100 % pass on local
fixtures/manifest.json. - Green imported WPT smoke profile for parser, selectors, cascade, DOM core, forms, and the v1 layout subset.
- Measured pass counts published here for every imported category.
- No global full-WPT percentage claim until the harness imports and runs a representative upstream WPT checkout.
Initial import targets before v1.0:
| Imported WPT area | Minimum useful target |
|---|---|
| selectors/css-scoping/css-nesting selector behavior | 50 fixtures |
| css-cascade / css-values computed-value behavior | 50 fixtures |
| dom/nodes + traversal + ranges | 50 fixtures |
| html/semantics/forms basics | 25 fixtures |
| css/css-display + css-box + css-position normal-flow layout | 40 fixtures |
| css-flexbox common cases | 25 fixtures |
| css-grid common cases | 25 fixtures |
| paint/ref-equivalent smoke | 20 fixtures |
These are minimum profile sizes, not final compatibility claims. The measured
pass table below must be filled from vixen-wpt output as the fixtures land.
| Imported WPT area | Fixtures run | Checks run | Passed | Pass rate | Notes |
|---|---|---|---|---|---|
| selectors | 50 | 232 | 232 | 100.0% | Target reached: :has() child/descendant/adjacent-sibling/general-sibling and selector-list smoke, attribute operators/flags, class/id matching, structural and typed structural pseudos, link/form/read-write/autofill/defined pseudos, negation/list pseudos, grouping de-duplication, and document-order coverage. |
| css-cascade/css-values | 50 | 250 | 250 | 100.0% | Target reached: specificity/source order, importance/inline, combinator/attribute operator matching, structural/link/form pseudo cascade, functional pseudo specificity, selector-list splitting, custom properties, declaration recovery, comments, math/color/gradient/transform/shorthand values, and quoted/nested/function declaration values. |
| dom-core | 50 | 250 | 250 | 100.0% | Target reached: query/getElementById/querySelectorAll, document/root/body access, tag/class/wildcard collections, attributes, reflected host properties, text aggregation, parent/child/sibling traversal, null relation checks, document URL, forms collection length, matches(), logical selectors, and :has()-backed matching. |
| forms | 25 | 134 | 134 | 100.0% | Required/optional/disabled/checked controls, labels/buttons/form attributes, reflected/default input/form/select/option properties, textarea text, tree traversal, repeated names, and :has() form selectors. |
| layout block/inline/position | 6 | 30 | 30 | 100.0% | Block flow, margin/padding/border, auto margins, border-box sizing, inline flow, and relative/absolute positioned smoke. |
| flexbox | 5 | 25 | 25 | 100.0% | Row/column grow-basis, gap/padding, and reverse-axis smoke. |
| grid | 5 | 26 | 26 | 100.0% | Fixed, fractional, minmax(), row/column gap, and fixed-height fractional-row smoke. |
| paint/ref-equivalent | 8 | 24 | 24 | 100.0% | Display-list reference-equivalent background/text, currentcolor, overflow clipping, positioned, flex/grid, and nested-background smoke. |
Known v1.0 layout gaps
Expected unsupported or partial areas unless promoted by WPT/real-site evidence:
- table layout
- floats and float avoidance
- full vertical writing modes / vertical text shaping
- page fragmentation / pagination / print layout
- advanced intrinsic sizing cycles (
min-content/max-contentedge cases) - complete absolute/fixed/sticky interaction matrix
- full SVG layout integration
Each gap should fail closed where possible, emit diagnostics when visible to users/tests, and receive a WPT fixture before being marked supported.
Measurement baselines
Vixen's current baseline suite is a dependency-light Linux measurement foundation built with Node.js built-ins. It records observations; it does not enforce budgets or claim complete real-site behavior. The repository now has a checked-in hello-Flutter peer plus controlled Linux release/AOT raw-bundle build and comparison commands. Clean post-R7 exact-commit renderer and post-Yaru raw- bundle reports plus the historical pre-R7 raw-bundle report are checked in for reproduction. None is an accepted budget or FlatPark package baseline.
Commands
Build inputs are locked to Cargo.lock:
just build-release
Run the committed hermetic headless scenario suite in text or JSON form:
just baseline-headless
just baseline-headless-json 9 2 # 9 measured runs, 2 warmups per scenario
The suite in fixtures/performance/headless-local.json measures native process
startup/version and local navigation plus runtime evaluation. Rendered layout,
commit, and capture performance belongs to the Flutter GUI/chrome-less baselines.
Each scenario has its own output validation; warmups are discarded.
Measure profile growth through the release headless binary and its public
--profile-dir seam:
just baseline-profile-growth # 5 repeated and 5 unique visits
just baseline-profile-growth 12
The command creates a temporary explicit profile under workspace-local .tmp/,
closes each headless process before sizing, and records checkpoints after
initialization, repeated local-file visits, unique data: URL visits, and a
deterministic localStorage payload. A fresh process must reopen and read the
payload before the last checkpoint. The script treats the profile as an opaque
directory and does not depend on redb files, tables, or allocation internals.
baselines/profile-growth-2026-07-16.json
records the first post-R7 reproduction from clean revision 6a61897: five
repeated local visits, five unique data: visits, a 65,536-byte localStorage
payload, and a fresh-process persistence read all exited successfully. The
opaque profile's 3,686,400-byte logical file size remained constant; allocated
storage was 1,622,016 bytes after initialization, grew 8,192 bytes across
repeated visits, did not grow across unique visits, and grew 139,264 bytes for
the persisted payload to 1,769,472 bytes. This single-host measurement is not a
growth budget or a broad history/cache/storage workload.
Measure the headless binary and create the official compressed Linux archive:
just size-headless
just linux-release-archive
stat --format='%s' .tmp/release/vixen-linux-x86_64.tar.gz
Reports produced for the former native Flatpak remain historical GTK/Relm4 evidence and must not be relabeled. The FlatPark package needs its own reviewed compressed/install observation after registry publication; the GitHub Release archive does not include the separately supplied GNOME runtime.
Stage the pinned Linux Flutter/rusty_v8 inputs, then build and compare clean raw release bundles:
just flutter-size-prefetch # network-capable staging; never evidence
just flutter-size-check-inputs # revision/archive/namespace checks
just size-flutter-linux # controlled build and text report
just size-flutter-linux-json # controlled build and JSON report
just size-flutter-linux-existing # analyze existing release bundles only
The accepted report below is historical GTK3 evidence from Flutter
3.47.0-0.1.pre. The current release path uses the checksum-pinned official
3.47.1 stable, Dart 3.13, and a digest-pinned GNOME 50 builder. A
2026-07-18 migration check measured 21,384,740 logical bytes for
the hello bundle and 85,283,280 for Vixen, a 63,898,540-byte delta with no
native plugin ELFs; it remains an unaccepted dirty-worktree candidate until a
clean revision report is checked in. fixtures/artifact-size/flutter_hello
uses Material plus the standard GTK3 Linux runner without Vixen code. Both
controlled runners are stripped with the same policy.
The analyzer requires release bundle structure (libapp.so, Flutter engine,
and ICU), requires exactly one libvixen_ffi.so only in Vixen, rejects debug and
build artifacts, verifies byte-identical shared Flutter engine/ICU files, and
reports every file plus component and Vixen-minus-hello logical/allocated deltas.
The native Vixen library remains an aggregate because stripped static
BrowserCore/V8 attribution needs separate linker-map evidence. The recorded
pre-R7 artifact also includes now-deleted renderer code.
Historical post-R7/Yaru GTK3 raw-bundle reference
baselines/flutter-linux-x64-raw-2026-07-16.json
was produced from clean revision 4a12d26 with just build-flutter-size-linux followed by the JSON analyzer used by just size-flutter-linux-json. Both release/AOT builds ran in the GNOME 50 builder
container; the analyzer verifies byte-identical shared Flutter engine and ICU
files.
| Artifact | Logical bytes | Allocated bytes | Files |
|---|---|---|---|
| hello-Flutter | 21,398,668 | 21,434,368 | 12 |
| Flutter+Vixen | 85,377,960 | 85,430,272 | 27 |
| Vixen minus hello | 63,979,292 | 63,995,904 | 15 |
The current logical delta attributes 58,184,992 bytes to aggregate stripped
BrowserCore/Rust/V8 native code, 3,096,576 bytes to Dart AOT, 2,576,028 bytes to
Flutter assets, 121,624 bytes to four native plugins, and 72 bytes to the runner.
The deterministic release archive made from the same bundle is 31,913,890 bytes
with SHA-256
3eef1bbed0e8e79dd8a85602837d4a9217dfbb82193cfcb93b62ca8730bc7879.
The archive observation is documented separately because the raw-bundle schema
correctly leaves compressed download size null. Clean extraction and the bounded
linux-release-smoke Cage launch steps reported Impeller and presented a Flutter
commit from this exact archive; that is one controlled release launch, not
sustained host/GPU evidence.
Against the historical 2026-07-12 report, the Vixen bundle is 131,560 bytes smaller overall and the aggregate native library is 2,076,976 bytes smaller after R7 deletion, while Yaru/fonts/assets add a 2,576,026-byte delta, native plugins add 121,624 bytes, and Dart AOT adds 638,976 bytes. These are net component changes, not isolated causal attribution: the comparison also moves from Flutter 3.44 to 3.47 and normalizes runner stripping. The hello control is 1,380,082 bytes smaller, so Vixen-minus-hello grows by 1,248,522 bytes even though the Vixen bundle itself shrinks. No value is a budget, and this report has not yet been independently reproduced.
Historical pre-R7 Flutter raw-bundle reference
baselines/flutter-linux-x64-raw-2026-07-12.json
was produced from clean revision 5b1d0af with just size-flutter-linux-json. Both release/AOT builds ran in the GNOME 50 builder
container with --network=none; shared Flutter engine and ICU hashes match.
| Artifact | Logical bytes | Allocated bytes | Files |
|---|---|---|---|
| hello-Flutter | 22,778,750 | 22,814,720 | 12 |
| Flutter+Vixen | 85,509,520 | 85,540,864 | 13 |
| Vixen minus hello | 62,730,770 | 62,726,144 | 1 |
The historical logical delta attributes 60,261,968 bytes to the aggregate stripped
libvixen_ffi.so, 2,457,600 bytes to Dart AOT, 11,200 bytes to the native
runner, and 2 bytes to Flutter assets. These observations are not a budget and
have not yet been independently reproduced. Compressed download, installation,
Flatpak payload/runtime, symbols, and static native subcomponents remain null or
unattributed as recorded in the report.
Flutter renderer baseline protocol
For every target platform and shipped ABI/architecture, produce three controlled artifacts with the same Flutter version, build mode, runner configuration, plugins, architecture, signing mode where practical, and package format:
- hello-Flutter: the smallest representative native Flutter application;
- Flutter+Vixen GUI: release renderer/chrome plus BrowserCore; and
- chrome-less rendered host: the same formatter/commit path without chrome.
Both use Flutter release/AOT mode, Rust release mode with strip/LTO, and native dead-code stripping where reproducible. Record compressed download, unpacked or installed size, native executables/libraries, assets, and separately supplied runtime/shared-system costs. Attribute at least Flutter engine/ICU, Dart AOT formatter/assets, runner/plugins, BrowserCore/Rust, V8/ICU/snapshots, Vixen resources, packaging metadata, and symbols. R7 reports must verify the deleted WebRender/EGL/frame dependencies and symbols are absent. Report both the hello-Flutter delta and the delta from the prior accepted Vixen artifact.
Each report names platform, OS/toolchain, ABI/architecture, Flutter/Dart/Rust/V8 and lock/source revisions, exact command, clean revision, hashes, AOT/strip/LTO settings, package split strategy, and exclusions. Android reports each split ABI rather than hiding duplication in a universal package. macOS reports universal and per-architecture attribution when both are distributed.
GUI bundles are inspected for accidental debug Flutter engines, symbols,
duplicate ABIs, development snapshots, test data, headless/CDP/WPT executables,
source archives, build tools, and caches. Required symbols are stored separately.
Warnings may be proposed only after representative reports are reproduced. A
hard budget follows only after warnings establish normal variance, component
ownership, comparison statistics, platform/ABI scope, and an explicit override
policy in ACCEPTANCE.md. There is no accepted numeric Flutter budget today.
Run the complete hermetic local batch with:
just baseline-measure
This runs the headless scenarios, profile growth, and headless artifact size. It
is intentionally not part of gate-push.
Measure the final release/AOT Flutter renderer under one Cage headless-Wayland session in text or JSON form:
just baseline-flutter-linux
just baseline-flutter-linux-json 5 1
just baseline-flutter-linux-hardware
just baseline-flutter-linux-hardware-json 5 1
Every warmup and measured sample starts a fresh vixen_shell CDP-automation
process and profile at 320×240, loads fixtures/dom/basic.html, requires
Impeller and the renderer-specific pinned exact Flutter-scene PNG, and shuts
down cleanly. Version 2 then loads fixtures/cdp/playwright-smoke.html and
serializes eight direct attribute mutations plus one mouse release. Each
operation requests synchronous Flutter geometry and joins its exact commit,
coordinator acknowledgement, engine frame number, and FrameTiming raster
finish. The report records app-spawn → CDP-ready, app-spawn → first exact
presentation, capture dispatch/client latency, app-process Linux memory,
mutation/input → exact presented-commit frame endpoints, and build/raster/total
frame spans. Cage and Node are excluded; BrowserCore, V8, Flutter, and Dart AOT
remain inside the app process.
Software mode forces Mesa software rendering. Hardware mode removes that
override and fails closed unless eglinfo -B identifies a non-software OpenGL
ES renderer on the same Wayland display. Neither mode accepts a budget.
FrameTiming.rasterFinishWallTime is not compositor acceptance, scanout, or a
physical input timestamp, and Cage exposed no refresh rate in these runs, so
over-refresh-interval counts are correctly null.
Recorded Flutter renderer references
baselines/flutter-linux-renderer-2026-07-16.json
and
baselines/flutter-linux-renderer-hardware-2026-07-16.json
were produced from clean revision cddcb09 with five measured runs after one
discarded warmup. The release bundle had already been built by the integration
recipe; the recorded repetitions used the same Cage environment and direct
script invocation with --renderer software and --renderer hardware.
| Metric | Software median | Software p95 | AMD/Mesa median | AMD/Mesa p95 |
|---|---|---|---|---|
| CDP ready | 225.223 ms | 317.256 ms | 176.528 ms | 252.007 ms |
| first exact presented commit | 324.232 ms | 412.192 ms | 258.415 ms | 330.925 ms |
| exact-scene capture dispatch | 50.317 ms | 57.148 ms | 38.360 ms | 42.243 ms |
| capture client round trip | 50.642 ms | 57.567 ms | 38.699 ms | 42.543 ms |
app-process VmHWM | 300,240,896 B | 302,569,882 B | 208,400,384 B | 208,770,662 B |
| direct mutation → commit frame | 15.402 ms | 28.812 ms | 14.527 ms | 29.248 ms |
| mouse release → commit frame | 26.364 ms | 29.746 ms | 25.269 ms | 29.185 ms |
| exact-frame build span | 71 µs | 91 µs | 70 µs | 89 µs |
| exact-frame raster span | 492 µs | 561 µs | 350 µs | 442 µs |
| exact-frame total span | 2,587 µs | 3,461 µs | 2,590 µs | 3,338 µs |
Each report contains 45 measured interaction frames, five successful clean
exits, exact commit/frame identity, bounded diagnostics, artifact/fixture/lock
hashes, and renderer-specific repeated PNG hashes. The hardware probe identifies
AMD Ryzen 7 7700X ... (radeonsi, raphael_mendocino, ACO, DRM 3.64) with Mesa
26.0.4 and OpenGL ES 3.2. This is one integrated physical GPU/driver
reproduction, not a supported matrix. Version 2's longer interaction workload
also makes its memory samples unlike the earlier version-1 startup/capture-only
report. None of these observations is a warning or failure threshold.
The underlying scripts accept --help. Paths relative to the workspace are
resolved from the repository rather than the caller's current directory where
practical.
Report schemas
JSON reports are versioned independently:
| Report | Schema |
|---|---|
| Headless scenarios | vixen.headless-baseline-report version 1 |
| Profile growth | vixen.profile-growth-baseline-report version 1 |
| Artifact size | vixen.artifact-size-report version 1 |
| Flutter Linux raw bundles | vixen.flutter-linux-artifact-size-report version 1 |
| Flutter Linux renderer | vixen.flutter-linux-renderer-baseline-report version 2 |
| Scenario input | vixen.headless-scenario-suite version 1 |
Every report says measurement_only: true. Headless scenario reports include
per-scenario wall-time samples and min/median/p95/max/mean summaries, sampled
VmHWM/VmRSS/VmSize peaks where Linux exposes them, exit status, and bounded
stdout/stderr byte counts. Profile reports include logical and allocated bytes,
growth from the preceding checkpoint, file counts, process samples, and the
storage reopen result. Artifact reports include logical and allocated bytes,
file counts, SHA-256, presence state, and the runtime-exclusion method.
VmHWM is the kernel high-water resident set for the measured process. VmRSS
and VmSize are maxima observed by polling /proc/<pid>/status; very short
processes can exit before a field is sampled, in which case the field is null.
These values do not include separate descendant processes.
Artifact SHA-256 is the file digest for files. For directories it is a stable manifest digest over sorted relative paths, entry types, sizes, symlink targets, and file digests; it is not a Flatpak/Ostree commit checksum.
Host fingerprint
Reports include the binary and Cargo.lock hashes where applicable, git revision
and dirty state, Node/rustc/Cargo versions, kernel and distro, architecture, CPU
model and logical CPU count, total host memory, page size, and renderer-related
environment variables. Hardware Flutter reports additionally include the
fail-closed Wayland EGL vendor/renderer/version probe. Optional metadata is null
when unavailable. A host
fingerprint supports comparison; it does not make unlike hosts equivalent.
Controlled and live inputs
fixtures/realworld/ contains small static-document, form-workflow, and app-shell
controls. They are deterministic, committed, site-shaped inputs used to exercise
production paths. They are not captures of external sites and do not establish a
real-site compatibility corridor. The headless suite performs no external
network access. Live-site findings still require named URLs, dates, host details,
and preferably reduced local or pinned WPT cases.
Accepted reports
No numerical report or regression budget is currently accepted. To accept one,
a maintainer must publish the complete JSON report, exact command, clean git
revision, artifact hashes, supported host class, run/warmup counts, and relevant
renderer environment. The report must be reproduced on the declared host class
and reviewed for workload validity and noise before a warning or failure
threshold is proposed. Thresholds belong in ACCEPTANCE.md and must state their
comparison statistic and product override policy. A convenient sample, an
unreviewed CI run, or a value copied from another dependency graph is not a
budget.
Current limits
This batch completes the local latency, Linux process-memory, profile-growth, headless-path, historical native-shell artifact-size, Flutter raw-release-bundle comparison, exact-commit Flutter startup/capture, controlled exact-frame spans, synthetic mutation/input-to-commit endpoints, and one physical GPU/driver reproduction. It does not yet measure:
- representative external sites or complete external-site compatibility;
- an accepted/reproduced Flutter GUI size baseline or FlatPark package artifact;
- the GUI/FlatPark path across a supported Linux, GPU, driver, and renderer matrix;
- native macOS, Windows, Android, or iOS Simulator BrowserCore/V8/Flutter-renderer behavior;
- animation cadence/smoothness, dropped vsyncs, compositor/scanout presentation, isolated GPU raster cost, or physical-device input-to-paint latency;
- V8/JavaScript heap usage separately from process memory;
- HTTP transfer or download throughput; or
- installed GNOME runtime size and shared-system storage attribution.
Those remain beta measurement work. Reports must keep these gaps explicit.
Vixen architecture
This document describes Vixen's implemented subsystem boundaries, the target
browser ownership model, data flows, and migration constraints. Product scope is
defined in PROJECT_DIRECTION.md; delivery order is in
ROADMAP.md; accepted tradeoffs are in
DECISIONS.md.
Status language
Architecture documents can accidentally make planned integration sound landed. This document uses three explicit states:
- Implemented: present in production code and exercised by a checked-in path.
- Transitional: present, but ownership or duplication must change before alpha.
- Target: the required end state; not a claim that it exists today.
Vixen now has a production BrowserCore behind the browser-scoped vixen-api
command/event seam. It owns one profile Store/network/cookie service, one
DOM/V8 owner thread, typed context/document/runtime/navigation generations,
asynchronous source loading, and bounded ordered events. Flutter, headless, CDP,
and WPT are adapters over that owner. This completes the current BrowserCore
ownership migration, not ADR-022's renderer transition or the broader alpha
compatibility exit gate.
Flutter is the target rendered frontend on Linux, macOS, Windows, Android, and
the Apple Silicon iOS Simulator.
Linux is the highest-priority GUI and release target: architecture integration,
host services, packaging, accessibility, and performance evidence converge there
first, then the same boundary expands to the other committed platforms.
The checked-in Linux Flutter runner and release archive require native Wayland;
X11/XWayland is rejected. Rendered automation uses the same executable in
chrome-less mode under bounded Cage/headless Wayland. Native vixen-headless
is intentionally text/runtime-only.
Crates and responsibilities
| Crate | Current responsibility |
|---|---|
vixen-api | Browser lifecycle and bounded renderer revision/mutation/commit/query/input/semantic contracts; no implementation dependencies |
vixen-net | HTTP and URL/cookie/CSP/CORS/referrer/mixed-content/security policy |
vixen-store | Bounded redb profile persistence and clear-data operations |
vixen-engine | Sole BrowserCore owner for contexts, navigation, DOM/Page source, cascade, V8, history, input intent, resources, and accessibility meaning; no layout or paint backend |
vixen-cdp | Bounded target/session/runtime adapter over a non-owning BrowserCore subscription and injected rendered backend |
vixen-ffi | Safe one-owner controller, C ABI v1, renderer broker, in-host CDP composition, copied bounded JSON, and panic containment; no frame ABI |
vixen-headless | Native text/runtime/profile CLI and non-rendered CDP test composition; rendered methods fail closed |
vixen-wpt | Source-check manifest/runner/report schema; rendered checks remain in the schema but execute only in Flutter |
The packaged Linux composition root is the Flutter runner plus vixen-ffi into
one BrowserCore. There is no Rust GUI, native screenshot renderer, texture
fallback, or second browser core.
Dependency direction
Flutter renderer + chrome ─► vixen-ffi broker/controller ─┬─► vixen-api
├─► vixen-cdp
└─► vixen-engine
├─► vixen-net
└─► vixen-store
rendered automation/WPT ─► chrome-less Flutter host ─► same bridge/core
native text utilities ──► BrowserCore, with no invented geometry
vixen-wpt ───────────────► vixen-api
Rules:
vixen-api,vixen-net, andvixen-storeare implementation leaves.vixen-wptdepends only onvixen-apiamong Vixen crates.vixen-engineowns browser truth and renderer source generations, but no formatting, text measurement, hit testing, geometry, semantic bounds, or paint authority.- Flutter owns formatter state, Paragraph/Canvas/scene output, exact geometry, hit testing, scroll mechanics, semantic bounds, and rendered capture. Public Flutter APIs run over explicitly enabled Impeller.
- A rendered CLI/CDP/WPT session is hosted by Flutter and has one BrowserCore. Native-only sessions may inspect source/runtime state but renderer-dependent operations fail closed.
- Pointer input crosses the C ABI only with an exact displayed-commit query and optional Flutter hit target. The old raw coordinate-input command is deleted.
- Renderer-dependent fixture checks use
flutter-js-eval,layout-box,visual-hash, orref-equivalent; the native WPT runner excludes them and the Flutter fixture host executes them.
R1–R7 are implemented. just test-r7 enforces absence of WebRender/gleam,
GlContext, EGL, native frame/screenshot ownership, Rust layout/display-list/
paint modules, frame transport/texture presentation, and raw coordinate input.
just gate-r7 composes this with the R5/R6 rendered fixture/CDP/Cage evidence.
Authoritative ownership model (target)
BrowserCore (one per open profile)
├── ProfileServices
│ ├── Store / schema / bounded writes / clear-data coordinator
│ ├── Network client, cookie jar, cache, HSTS, proxy/cert configuration
│ ├── Permission decisions and prompt broker
│ ├── Download manager
│ └── platform host services (paths, fonts, portals/native pickers, GPU diagnostics)
├── BrowsingContextRegistry
│ └── BrowsingContext (one per top-level tab; frames form a child tree)
│ ├── SessionHistory + bounded root/nested scroll restoration state
│ ├── NavigationController + active NavigationId/cancellation
│ ├── active DocumentState
│ │ ├── DOM + style data + invalidation
│ │ ├── JsRuntime realms/resources/event loop
│ │ ├── render-source revision + accepted atomic render commit
│ │ └── scroll/selection/accessibility semantic state
│ └── viewport, input, dialog, and context-scoped storage state
└── EventHub / diagnostics / inspector routing
Ownership invariants
- A profile is opened once by
BrowserCore. Cookies, cache, localStorage, permissions, HSTS, download history, and durable settings are profile-owned. - Session history, sessionStorage, viewport/input, active navigation, runtime realms, and document state are browsing-context owned.
- DOM, style, renderer source revision, atomic commit, presented scene, and
runtime-visible page state identify the same committed
DocumentId. A navigation cannot partially replace one layer. - Every asynchronous result carries the ids/generation it was created for. Results for a closed context, cancelled navigation, or replaced document are discarded before mutation or success notification.
- Flutter owns formatting, paint, scene capture, chrome/widgets, Semantics presentation, and host-service UI over bounded revision/commit state. CDP may own sockets/session routing. Neither owns navigation, DOM, policy, or durable browser truth.
- Flutter Canvas/Paragraph is the sole target web-content renderer. BrowserCore accepts only exact-revision atomic commits and remains the source of accessibility meaning; Dart does not infer semantics from pixels or retain a mutable DOM.
Stable ids distinguish at least profile, context/tab, frame, navigation, document, request, runtime context, render revision/commit/node/fragment/resource, remote object, and download. Use typed ids even when adapters serialize them.
Threading and execution
deno_core::JsRuntime is !Send + !Sync, and the current DOM is Rc-backed.
Moving individual pages among arbitrary worker threads would add synchronization
without solving lifecycle ownership.
The execution model is one browser-core owner thread per open profile/process, plus bounded external workers for sendable I/O and host work:
- all DOM, V8, history, navigation-commit, style invalidation, render-generation, and context-registry mutation runs there;
- network and blocking host operations may run externally, but return typed messages carrying context/navigation/request generations;
- the target Flutter renderer isolates/platform thread own formatting, Paragraph, Canvas/scene paint and capture, chrome, Semantics, and host-service presentation;
- CDP sockets and CLI orchestration may use Tokio tasks, but dispatch browser commands to the core and consume ordered events;
- renderer interaction observes document/render generations; returned geometry cannot commit browser state or target input from a stale snapshot.
The implemented core confines every Page, V8 isolate, history mutation,
document commit, and context-registry mutation to its named owner thread.
rusty_v8 enters isolates for their lifetime, so context/runtime generations are
retained in a bounded 512-slot arena and destroyed in reverse construction order;
commands temporarily enter older isolates through one localized V8 boundary.
Main-document source reads run on a bounded two-worker Tokio runtime. Each task
owns only sendable network/input data and an isolated cookie snapshot; completion
returns a typed context/navigation message and a cookie delta. Stop, supersede,
context close, and shutdown abort the task and invalidate its generation. The
owner checks the generation before applying cookies, parsing, writing profile
history, replacing the document/runtime, or emitting success. HTML parsing runs
as bounded owner-thread quanta, checks commands between quanta, and drops stale
parser state after stop or supersede. Runtime construction and page-script/
resource execution remain owner-thread work. Configured and parser-discovered
scripts advance one item per generation-checked quantum, followed by separate
DOMContentLoaded, load, and settle quanta. Individual V8 execution, promise
pumping, microtask checkpoints, and runtime-effect drains share a five-second
production watchdog. Timeout terminates V8, unwinds the job, cancels the
termination state, and joins the exact watchdog before another job can start, so
a late timeout cannot poison or terminate the next evaluation. The command-side
control registry snapshots the exact context runtime before enqueueing an
accepted navigate/reload/stop/close intent; it can terminate only that active
generation, not a replacement runtime created by the command. Interrupted work
discards deferred DOM mutations and runtime outputs, and the owner checks queued
commands before advancing another navigation quantum. Runtime fetch() and CORS
preflight network calls return through cancellation-polled worker channels. On
cancel, a worker-local signal wins against and drops the reqwest future, aborting
the active transport before the owner joins the worker. Cookie/preflight/HTTP-
cache writes remain outside the worker under the exact still-active runtime guard.
Runtime construction, other local native host calls, and discovered resources
beyond the first external stylesheet still need interruptible paths.
Parser-discovered external classic scripts and non-alternate external stylesheets
are the first post-commit resources on that worker model. The owner resolves the
URL and current script/style policy, then sends only network/profile-cookie data
to the existing bounded Tokio runtime. Manual redirect handling validates URL
policy, destination CSP, and active mixed-content policy before every hop and
does not buffer redirect bodies. Completion
carries context, navigation, document, runtime, and resource request ids plus an
isolated cookie delta. The owner rechecks every id and final HTTP status/nosniff
before exposing source, updating the bounded profile cache, applying style to the
Page cascade/runtime hosts, or resuming script work. Accepted cookie deltas apply to
the core, active runtime, and each current profile-store origin partition; delta-
against-current persistence preserves unrelated writes from other contexts and
makes accepted cookies visible after profile reopen. Stop, supersede, close, and
shutdown cancel the task and emit one bounded request/failure sequence; late
completions are inert. File documents and file scripts share one async reader that
checks the configured body limit both before allocation and while reading;
external file stylesheets use that reader as well.
The Rust GTK shell has been removed. Every frontend has one browser adapter (or factory-injected browser handle), not an independent engine state machine per tab.
Command and event seam
The implemented dependency-free BrowserHandle, BrowserCommand, and
BrowserEvent contracts establish typed routing for context/navigation/document/
request/runtime/download generations. They replace the removed tab-shaped
callback API with a browser-scoped seam whose concepts are:
- Commands: create/close/activate context; navigate/reload/stop/traverse; evaluate; dispatch input; query/snapshot; set viewport/emulation; answer a permission/dialog; start/cancel a download; clear profile data.
- Events: context/document created/destroyed; navigation requested/started/ redirected/committed/cancelled/failed; DOMContentLoaded/load; URL/title/history/ progress changed; request/response/failure; console/exception; dialog/ permission/download; invalidation/frame-ready; diagnostic/profile-write error.
- Queries/snapshots: explicitly versioned, bounded views. Mutable behavior remains commands, not shared references into engine internals.
Every command and event names the relevant context and generation. Ordering is defined on the engine thread; adapters may translate but not reorder lifecycle within a context. Stable diagnostics and protocol errors are product contracts. Evaluation and input results include the exact ordered cross-document navigation ids they created. CDP stores those ids in per-request continuations and uses one production event pump to settle page, target-creation, history, runtime, and input requests while the socket remains readable. Earlier ids from one command are consumed as superseded; disconnected or timed-out requests retain claimed tombstones until their late terminal outcome can no longer be misattributed. Configured initial-URL startup remains a pre-connect readiness barrier rather than a concurrent event consumer.
Do not add a generic engine-selection abstraction. Vixen still has one engine and one JS runtime. The seam isolates product frontends and thread ownership, not alternate implementations.
Flutter bridge status and target
The implemented vixen-ffi::FlutterBrowserController is deliberately non-clone.
It owns one EngineBrowserHandle, returns navigation acceptance without waiting
for settlement, and exposes one nonblocking or timeout-bounded ordered event
receiver. Its isolated handwritten C ABI module wraps that exact controller in a
process registry: no Rust reference crosses the boundary, all pointer inputs are
bounded and copied before parsing, no callbacks exist, output allocations are
released only by opaque token, and each delivered event receives a monotonically
increasing per-handle sequence. The crate builds rlib, cdylib, and staticlib
forms. just gate-native-abi is native ABI/header/wire evidence only.
The Dart FFI binding and worker isolate are transport adapters over the same browser-scoped seam:
- opaque browser handles plus typed context/frame ids, explicit version negotiation and destruction, and no Rust references retained by Dart;
- checked owned buffers with one allocator/free contract;
- commands copied to BrowserCore and bounded ordered events copied to Dart with typed ids/generations;
- no synchronous Dart callback while Rust locks or V8 scopes are active;
- bounded mutation/commit/query channels plus explicit payload release; and
- stable structured errors rather than panic/exception-driven lifecycle flow.
A generated bridge is optional, not architectural. Adopt one only if its output, ownership, platform build behavior, and artifact cost remain inspectable. The Linux shell uses constructor-injected scripted tests without inventing production browser state; production always uses the native worker and fails closed.
The target renderer protocol never calls Dart directly while Rust locks are held.
Ordinary rendering is asynchronous: BrowserCore publishes a base/target revision
batch, Flutter lays it out and paints it, then returns one atomic commit. A
dedicated request/response broker remains serviceable while the command worker or
V8 evaluation waits for EnsureLayout; the renderer cannot re-enter BrowserCore.
Cancellation, deadlines, and exact revision/commit checks prevent deadlock and
late mutation.
Navigation and document commit
Target main-document flow:
frontend/page intent
→ BrowserCommand::Navigate(context, intent)
→ assign NavigationId; cancel/supersede prior provisional work
→ normalize URL + navigation/sandbox/permission policy
→ profile loader: HSTS/cookies/cache/referrer/request metadata
→ network request and redirect loop (policy on every hop)
→ response security checks and content classification
→ provisional DocumentState
→ atomic commit: URL/origin/history/document/runtime generation
→ parse + parser scripts + discovered subresources
→ Stylo update → renderer mutation → atomic Flutter commit
→ DOMContentLoaded → load → settled diagnostics
Before commit, failure normally preserves the current document. After commit,
failure belongs to the new document/error-page lifecycle. Redirects keep one
navigation lineage but distinct request ids. The bounded network worker reports
each redirect to BrowserCore as it occurs; the core generation-checks it,
advances the active request id, and emits it before final response completion.
Request-start and final-response progress map to the existing navigation phases
rather than creating duplicate lifecycle events. Same-document history changes
keep the document id and update URL/history/scroll state through the same
controller.
Implemented stop() invalidates the active generation and aborts source
transport/body reads. Forced late completions are rejected before cookie,
profile, history, document, runtime, or event mutation. HTML parsing is also
generation scoped and cooperatively interruptible between bounded source quanta;
stop, reload, and history-traversal parser races prove stale work cannot commit.
Configured/author scripts and pending lifecycle stages are generation-scoped
quanta as well: stop/supersede suppresses unstarted items and later lifecycle
success. The target still extends cancellation inside individual runtime and
resource jobs.
Document, runtime, and Web APIs
Page is the BrowserCore facade over parsed DOM, computed styles, accepted
resources, diagnostics, form/history state, runtime snapshots, and renderer
source projection. It owns no formatting, layout, hit testing, or paint state and
is not a profile/browser lifecycle coordinator.
JS uses deno_core directly:
- generated WebIDL describes interface/prototype shape;
- pure immutable/value APIs may be JS bootstrap code;
- stateful page/network/storage/security APIs cross narrow Rust ops/resources;
- validation and permission checks occur at the JS → Rust boundary and again at lower trust boundaries where necessary;
- resources carry document/context generations so navigation teardown revokes stale handles;
- parser scripts, static/dynamic module graphs, tasks, and microtasks join the document lifecycle. Every module URL retains its originating bounded graph policy; dynamic descendants cannot inherit whichever root happened to run last, and cancellation generation-checks profile effects. Parser classics use distinct document-base source identities, redirected classics use their accepted final URL, and automation registers the current document base, CSP/bypass decision, origin, and retained import map before dynamic import. Exact JSON import attributes select strict file/HTTP JSON response policy; unsupported attribute maps are bounded and rejected before transport. Authored external classic/module root SRI is verified over accepted raw bytes before source conversion, V8, response cookies, or cache insertion; cross-origin classic SRI first requires an explicit CORS response. The retained import map also owns a bounded exact normalized-URL integrity table. Graph roots use it only as fallback for an absent authored attribute; static/dynamic dependencies verify accepted raw bytes before graph publication or profile effects. Multiple inline maps merge first-wins into immutable parser-position snapshots over one bounded successful-resolution set. Earlier static graphs cannot be rewritten; unresolved dynamic imports and automation consult the latest merged snapshot without replacing the graph's other policy provenance.
The obsolete Page string-expression and headless classifier shims are deleted;
all evaluation adapters use BrowserCore/JsRuntime.
API surface alone is not support. Inert media/canvas/web-component objects may
help automation probes, but COMPAT.md must classify them as shape-only until
their observable subsystem behavior exists.
See RUNTIME_WEB_PLATFORM.md for host-module rules.
Style, layout, paint, and inspection
The implemented production path is:
BrowserCore DOM + Stylo computed styles + accepted resources/semantics
→ RenderMutationBatch(base_revision, target_revision)
→ Flutter Vixen formatter
CSS box/anonymous trees + formatting/fragmentation
dart:ui Paragraph/image measurement
Canvas paint order/clips/transforms/compositing
mechanical scroll geometry + hit testing
→ Flutter scene/layers
→ RenderCommit(commit_id, target_revision)
immutable basic geometry index
opaque Flutter-side hit-test handle
bounded text/caret query handle
scroll snapshot
semantic bounds
→ Presented(commit_id)
Source revisions and mutation recovery
RenderRevision includes context, document, source/style, viewport, and resource
generations. Every incremental batch names its exact base and target. Flutter
applies it only to that base; a gap requests a bounded full snapshot. Mutations
carry immutable styled render inputs, stable node/resource/semantic ids,
pseudo/generated content, scroll intent, and removals. They are not a second DOM.
Dart retains only bounded active generations/resources and releases superseded
payloads explicitly.
BrowserCore fetches and validates image/font bytes under URL/CSP/CORS/integrity/ cache policy before renderer exposure. Node count/depth, text, mutations, resources, decoded bytes, fragments, commits, and queues are bounded.
The full-snapshot source projects renderable DOM elements with stable BrowserCore ids, renderer-only text ids, resolved style properties, accepted PNG bytes, semantic descriptors, and root scroll intent. Non-rendered metadata/script/style subtrees still participate in DOM id allocation but are omitted from renderer payload. R6 diffs this source deterministically into exact same-document mutation batches and falls back to the full snapshot on first load, missed state, or resync. R7 deleted the former Rust layout/display-list projection.
Formatter and commit authority
Vixen implements web formatting semantics in Dart over Flutter primitives.
Ordinary widgets or Flutter Flex are not CSS. dart:ui Paragraph is authoritative
for shaping, fallback, bidi, line breaking, intrinsic text measurement, caret and
range geometry, and text hit testing. Canvas/scene APIs are authoritative for
paint order, clips, transforms, compositing, images, and capture.
RenderCommit atomically identifies a ready scene plus geometry, an opaque
Flutter-side hit-test handle, text query state, scroll state, semantic bounds,
and truncation. Presented is separate because input/accessibility must name
what is visible, not merely the newest layout. BrowserCore rejects stale or
mismatched commits before inspection, input, scroll events, or accessibility
publication.
Flutter returns immutable basic border/padding/content/fragment/clip/scroll/paint geometry to BrowserCore. BrowserCore validates and queries it cheaply for common synchronous DOM/CSSOM/CDP calls without reimplementing layout. Paragraph-specific offset/caret/range/affinity operations use a bounded batched renderer query service. Renderer-authoritative means Flutter computes every value; it does not require an FFI round trip for every rectangle read.
Dedicated renderer transport
R2 keeps renderer traffic outside serialized browser commands. BrowserCore-side
code publishes bounded asynchronous snapshot/mutation/handle-release updates;
Flutter submits bounded commit/presented/resync records. EnsureLayout, hit
tests, and Paragraph text queries alone occupy correlated in-flight request
slots. One mutex/condition queue atomically owns closure, deadlines, queue order,
and all pending slots, so polling does not free capacity before a response. C
output remains retained only by release token. The bridge can be shut down from
the Flutter/UI side to cancel requests and wake polls even if the command worker
is blocked. The small Dart service consumes records into the R3 formatter without
calling BrowserCore. Production now uses the service for one bounded R4
projection; source publication and submission draining remain control operations,
while renderer DTO payloads stay on the dedicated queues.
Synchronous layout broker
For same-task mutation followed by geometry, BrowserCore flushes DOM/Stylo,
publishes the required mutation batch, posts EnsureLayout(required_revision) to
a dedicated Flutter renderer broker, and waits without holding browser mutexes.
The Flutter UI/renderer isolate must remain serviceable while the originating
command/V8 evaluation waits, cannot re-enter BrowserCore, and returns through a
separate response channel. Navigation, stop, close, shutdown, and deadline cancel
the wait. Late commits are inert.
R6 implements this with one Page shared by BrowserCore and its page realm. The geometry op drains pending DOM mutations into that Page, refreshes cascade/source state, publishes the exact batch, and waits through renderer state isolated from the C controller lock. The normal Flutter shell runs a bounded broker pump on a separate UI-isolate service tail, so a blocked command cannot block its own renderer response. Basic geometry is read from the accepted commit; Range/caret queries use its Paragraph handle. One full-resync retry handles timeout, reset, missed state, and malformed commits without poisoning the next request.
Input, scroll, semantics, and automation
Flutter hit-tests the displayed commit and returns commit/revision plus stable
node/fragment ids and finite coordinates. BrowserCore validates the target and
owns DOM dispatch, cancellation, and default-action policy. Flutter owns live
scroll offsets/extents/clips; BrowserCore sends scroll commands after
preventDefault() and owns script intent, DOM scroll effects, history restoration,
and persistence.
BrowserCore authors accessibility role/name/value/state/relationships/focus/ actions. Flutter supplies accepted semantic bounds/text geometry and publishes Semantics only for the displayed commit. Actions return with exact commit and advertised action generation.
The same renderer runs without chrome for screenshots, layout/visual WPT, and rendered CDP. Linux hosts it in Cage/headless Wayland and captures an exact presented scene without compositor chrome.
Cutover invariant
R7 is complete. No fallback renderer or compatibility API may reintroduce WebRender/EGL/frame transport, Rust layout/paint authority, native screenshots, or raw coordinate input. Renderer-dependent evidence must use the same Flutter formatter/commit path as the GUI. A pure shared algorithm may be added only through an explicit stable formatter contract when measured reuse is simpler than a direct Dart implementation.
Resource loading, network, and policy
vixen-net owns pure transport/policy primitives. BrowserCore's profile loader
combines them with document and profile context. One loader must serve main
documents, scripts, styles, images, fonts, fetch/XHR, frames, and downloads.
For every request:
- derive source origin/partition, destination, credentials, referrer, CSP, sandbox, and permission context from authoritative state;
- validate URL/method/headers/body and private-network policy;
- apply HSTS, cookies, cache, redirect, mixed-content, CORS, and request metadata policy in a defined order;
- stream transport with request id, destination-specific limits, progress, and cancellation;
- apply response CORS/CORP/COEP/nosniff/integrity/content policy;
- only then expose, execute, decode, persist, cache, or create a download.
The current private-cache seam stores bounded bodies as independent variants
together with the effective final-hop request values named by Vary. A
versioned canonical selector preserves absent/empty values, caps selected header
data at 64 KiB, keeps legacy URL-only rows readable, and counts every variant
toward the 512-record table bound. One shared decision applies response
no-store/no-cache/must-revalidate, Date/Age plus max-age or Expires,
request no-store/no-cache/max-age/min-fresh/max-stale, exact
present/absent variant matching, validator revalidation, body limits, and
cache-disabled bypass for page fetch/XHR and module resources. Malformed dates,
numeric directives, wildcard/oversized variants, and conflicting values fail
closed. Heuristic freshness and general redirect-response caching remain loader
work rather than frontend-specific cache logic.
Cacheable permanent same-origin 301/308 redirect chains use a separate bounded
alias table so final representations remain single-copy; no-store redirects
are excluded. Aliases are limited to 512 records,
20 hops, and 64 KiB of target URLs; only a fresh matching final representation
can satisfy one. Lookup reruns URL and current resource policy for every hop and
computes cookies/Vary at the accepted final URL. Temporary and cross-origin
redirects remain live-only until redirect response headers and freshness can be
replayed without weakening policy.
Transport body reads are incremental and enforce the destination cap before
buffer growth. Ordered response, chunk progress, and completion records carry
exact byte counts through BrowserCore, C ABI diagnostics, and CDP. Ordinary page
fetch/XHR responses now cross a distinct head boundary: every redirect target is
rechecked against URL/CSP/mixed-content policy, and final CORS/header visibility
is fixed before V8 receives status, URL, or headers. The transport then feeds raw
body chunks through an eight-message backpressured channel while retaining the
same destination-capped body for cookie/cache commit. Stream reads and XHR
progress consume that channel; completion or failure carries the same request id
and occurs only after profile effects settle. A serialized fetch-generation gate
allows commit while V8 is idle between reads but rejects stale work after
cancellation. Integrity-bearing requests,
conditional 304 revalidation, and opaque no-cors responses remain
buffer-before-resolution because their exposure decision requires the complete
representation or deliberately exposes no body.
Page fetch/XHR use a per-realm host-owned asynchronous request table bounded to
32 entries. Explicit AbortSignal, body-stream cancellation, XHR abort, realm
teardown, BrowserCore stop, and runtime deadlines cancel the owned reqwest future;
an interrupt generation prevents cleared V8 termination state from authorizing a
late cookie/cache/preflight commit. Aborting after head exposure rejects pending
body reads with the exact JS reason and publishes one terminal failure. Active
network-body cloning/teeing remains fail-closed until one transport can safely
own multiple bounded consumers.
Policy failure, transport/TLS failure, protocol failure, decode failure, unsupported behavior, and cancellation have distinct stable diagnostics. CDP and shell translate the same underlying event; they do not infer failures from frontend-specific state.
Profile and storage
One Store is opened per profile. The implemented schema includes bounded
records for:
profile.redb
cookies
fetch-cache
fetch-cache-aliases
history
session
web-storage
downloads
permissions
hsts
downloads/
reports/
The filename and XDG/app-ID paths are selected by the composition/host service;
partition keys are produced by the engine/network layer and remain opaque to
vixen-store.
Before adding a durable table, define:
- engine owner and partition key;
- record and total-table limits plus eviction behavior;
- transaction/failure/recovery semantics;
- clear-data category and session-restore interaction;
- private/ephemeral profile behavior; and
- observability without leaking sensitive content.
Downloads, favicons/icons, settings, credentials/autofill, and future IndexedDB/ Cache Storage require purpose-built bounded schemas, not generic JSON dumping.
Platform host services
Platform compatibility is an engine input, not shell trivia. Small native host services provide:
- certificate roots and custom CA configuration;
- proxy/environment policy;
- bounded system/bundled/web-font descriptors and accepted bytes for Flutter's Paragraph font collection; BrowserCore retains web-font fetch/policy/cache;
- platform data/cache/config/download directories scoped by app id;
- Flatpak portals or native pickers/services for file access, downloads, permissions, and external opens;
- Flutter engine/Impeller backend and driver capability diagnostics as applicable; and
- safe file/download destination validation.
Path discovery may remain platform code, but profile state ownership stays in
BrowserCore. Flutter owns prompt/dialog presentation, not policy or durable
decisions. All host failures produce structured diagnostics usable by GUI error
pages, chrome-less renderer output, CDP, and smoke reports.
Trust boundaries and limits
Web content and protocol clients are untrusted. Validate as close as possible to entry, then preserve typed validated data internally.
| Boundary | Owner | Required behavior |
|---|---|---|
| CLI/CDP/GUI command → core | adapter + vixen-api DTO validation | Validate ids/options/sizes; reject unknown/stale targets with stable errors |
| navigation/resource request | browser loader + vixen-net | URL/private-network/header/body/policy checks on initial request and redirects |
| HTTP response → page/profile | browser loader | CORS/security/integrity/content checks before exposure, execution, decode, cache, or persistence |
| JS → Rust op/resource | runtime host module | WebIDL conversion, size/permission/origin checks, document-generation validation |
| DOM/style generation → Flutter mutations | BrowserCore + FFI | Exact base/target revision, bounded immutable data, known resources, deterministic resync |
| Flutter commit/query → browser inspection/input | renderer bridge + BrowserCore | Exact revision/commit, bounded finite geometry/ranges, known node/resource ids, reject stale or truncated-required answers |
| profile write/read | profile service + vixen-store | Partitioned normalized records, bounds, transactional failure diagnostics |
| file/portal/download | platform host service + download manager | Approved roots/handles, safe names, no ambient arbitrary write/open |
| inspector/snapshot | engine inspector | Bounded output; explicit update or stable stale-state error; no alternate model |
Content-controlled queues and data need explicit caps: redirects, headers/body, DOM nodes/depth, parser/script work, runtime handles, events/microtasks, decoded images/fonts/media, cache/profile records, downloads, traces, console/diagnostic buffers, snapshots, and protocol output. On limit breach, fail deterministically without exposing partially accepted unsafe state.
Diagnostics and observability
Observability is a product contract, not debug residue:
- lifecycle events name context/navigation/document/request ids;
- stable error codes separate policy, transport, protocol, unsupported, cancellation, stale-state, resource-limit, renderer/runtime reset, and profile failure;
- traces and logs are bounded and privacy-minimal by default;
- Flutter GUI, chrome-less rendered automation, CDP, WPT, and real-site reports translate the same engine and renderer-generation events;
- no adapter may require page text, JS expressions, credentials, form values, or full headers in a default trace.
Verification and reduction architecture
Evidence layers share production paths:
- leaf-unit tests for pure policy/data/formatting algorithms;
- engine integration tests for ownership, lifecycle, generations, and profile partitioning;
- committed local fixtures for focused regressions;
- pinned imported WPT profiles with source×category reports;
- Flutter GUI/chrome-less-host visual comparisons and external Playwright/CDP smokes;
- controlled real-site/platform-host corridor reports; and
- fuzz, audit, performance, memory, size, restart, and recovery gates.
Classify a real-site failure as navigation/network/security, DOM/runtime, style/layout/paint, storage/profile/download, media/accessibility, shell/platform, automation/inspection, or reliability/performance. Reduce it to the lowest layer that reproduces the production path. If it cannot yet be reduced, retain exact commands, platform, artifacts, and classification rather than a vague issue.
Build profile
The release profile remains:
[profile.release]
strip = true
lto = "thin"
codegen-units = 1
panic = "abort"
lto = "fat" is a measurement experiment, not the default. After cutover,
structured size commands measure the GUI, chrome-less Flutter host, and any
text-only launcher separately. Hard budgets
must be based on published reproducible baselines for the active
deno_core/V8/Flutter dependency graph. Pre-R7 measurements include deleted renderer costs and must be labeled historical.
Each Flutter GUI release uses release/AOT/strip/LTO controls and a per-platform/
ABI hello-Flutter versus Flutter+Vixen report with component attribution. Debug
engines/symbols, duplicate ABIs, headless tools, build tools, and caches do not
belong in GUI bundles. Warning and hard thresholds follow the evidence policy in
BASELINES.md; no numerical Flutter budget exists yet.
Flutter shell
The Flutter shell is Vixen's only rendered frontend. BrowserCore owns browser truth; Flutter owns formatter, paint, hit testing, scroll geometry, semantic bounds, and scene capture. R7 removed every native rendering fallback.
Composition
The Linux application is rooted at BrowserShell and uses locked pure-Dart
Yaru/Adwaita-blue styling. The page body is BrowserContentSurface, which
paints only a current FormatterCommitView through RenderCommitPainter.
A missing, stale, retired, hidden, or viewport-mismatched commit displays the
explicit Renderer commit unavailable surface. It does not request native
pixels or create a texture fallback.
The Linux runner:
- requires native Wayland and rejects X11/XWayland;
- uses Flutter's standard GTK3 embedder but owns no second Rust/GTK browser widget tree;
- owns the native GTK3 header bar/window controls while Flutter owns the tab strip and all browser chrome below it;
- uses Flutter's generated plugin registration without a downstream engine;
- has no pixel-buffer texture channel, native frame pool, EGL surface, or compositor-specific web-content path.
Browser bridge
NativeBrowserController owns one worker isolate and one opaque C ABI browser
handle. The worker serializes bounded copied JSON commands/events and releases
native output buffers by opaque token. No Rust pointer or callback crosses into
Dart.
The renderer uses separate bounded channels for:
- full source snapshots and exact incremental mutation batches;
- reset/resync;
- atomic commit submission and post-frame presentation acknowledgement;
- synchronous
EnsureLayout; - commit-bound Paragraph text queries; and
- commit-bound hit-test/input and semantic actions.
The UI isolate services renderer broker work independently of the browser
command worker. A V8 command blocked on EnsureLayout therefore cannot block the
Flutter work needed to answer it. Navigation, stop, close, shutdown, and V8
execution deadlines cancel pending renderer work; late replies are inert.
Renderer ownership
BrowserCore publishes immutable DOM topology, stable element ids, renderer-only text ids, computed styles, accepted resources, semantic descriptors, viewport, page zoom, and root scroll intent. Flutter validates the revision graph and owns:
- CSS block/inline/flex/grid formatting and fragmentation;
- Paragraph shaping, line breaking, caret/range geometry, and text hit testing;
- image measurement and decode at the renderer boundary;
- Canvas/Picture/Scene paint order and clipping;
- mechanical scroll extents/offsets;
- hit-test handles and local coordinates;
- semantic bounds; and
- direct scene PNG capture.
Only Flutter public dart:ui APIs are used. Impeller must be explicitly enabled
for accepted rendered evidence; a Skia-backed run is not renderer proof.
Input and accessibility
Pointer coordinates are normalized from Flutter logical space into the exact
commit viewport. Pointer input crosses the C ABI only as
dispatch_renderer_mouse_event, carrying the displayed commit revision, query
handle, query id, point, and optional Flutter hit target. BrowserCore validates
that target before DOM dispatch. The former raw coordinate-input command is
deleted.
Keyboard and text-input commands remain generation/viewport bound. Focused writable controls use BrowserCore-authored value/selection/input intent and the platform text-input connection. BrowserCore authors semantic role/name/value, relationships, focus, and permitted actions; the displayed Flutter commit owns semantic bounds. Stale action generations and stale commits fail closed.
Accessibility metadata refresh is independent of scene capture. There is no frame/Semantics pairing or BrowserCore layout bbox fallback.
The release harness observes the native accessibility tree by process id and
checks BrowserCore-derived names while /proc/<pid>/maps confirms the expected
GTK3 runtime. Native Wayland input remains the interaction path; broader AT-SPI
role, action, and transformed-bound claims require fresh evidence on the
official stable engine.
Lifecycle and recovery
Host-view commands carry a monotonic generation, physical viewport, scale, focus, visibility, and lifecycle state. Hidden/detached/paused views retire presentation; a late commit cannot reappear after resume. Renderer reset forces one bounded full-source resync. Timeout, malformed commit, missed state, and resync each receive at most one bounded recovery attempt.
Automation
Normal GUI, page-only automation, rendered CDP, Playwright smoke, and fixture manifest use the same formatter and painter. Chrome-less mode changes composition and output routing only; it does not select another browser core or renderer.
Renderer-dependent manifest checks are explicit:
flutter-js-evalfor JavaScript whose result needs commit geometry/scroll;layout-boxfor exact commit bounds;visual-hashfor Flutter scene hashes; andref-equivalentfor exact Flutter reference scenes.
The native WPT/headless runner executes source/runtime checks only.
Deleted R7 path
R7 deleted WebRender/gleam, GlContext, native-headless and FFI EGL, native
screenshots/incremental captures, Rust layout/display-list/paint and paint-helper
modules, RGBA frame ABI/tokens/pools, Dart frame transfer, Linux pixel-buffer
texture plugin/presenter, raw coordinate input, and obsolete recovery/gate tests.
Do not add compatibility shims for those details.
Verification
Focused commands:
just test-r6
just test-r7
Full rendered composition:
just gate-r7
test-r7 performs source/dependency absence scans, native tests, clippy, C header
syntax, manifest/script validation, Dart formatting/analyze, and the full
Impeller-requested Flutter suite. gate-r7 first preserves all R5/R6 release
Cage, fixture, CDP, mutation, synchronous geometry, cancellation, and recovery
evidence.
A release Linux runner build additionally requires CMake and the normal Flutter Linux toolchain.
Vixen specification
Vixen's contract. What this document captures:
- Vixen-specific surfaces (CLI, error codes, WPT check types, diagnostics shape).
- Vixen-specific configuration of upstream behaviour (URL policy blocklist, cookie defaults, CSP enforcement points).
- Behavioural invariants that must be reproduced exactly because they're easy to get subtly wrong (event dispatch order, paint rules, form-validation edge cases).
What this document deliberately does not capture:
- Restatement of web-platform specs. Vixen delegates spec-heavy behavior where
that improves correctness and size: Stylo/
selectorsfor CSS,html5everfor HTML,deno_core/V8 for JS execution and host packaging, and Flutter Paragraph/Canvas/scene/Semantics for cross-platform render primitives (seeDECISIONS.mdADR-001 / ADR-011 / ADR-014 / ADR-022). Vixen implements CSS formatting semantics in the Flutter-hosted renderer. Behavioural parity is measured by the WPT profile documented indocs/COMPAT.md; if a behaviour isn't called out below, follow the latest stable spec and document deviations indocs/COMPAT.md.
Headless CLI surface
The vixen-headless binary exposes this flag set. Flags and stable
error codes are a public contract — automation depends on them.
vixen-headless --url <URL> [options]
--url <URL> Load a URL (required).
--viewport <WxH> CSS inspection viewport (default 800x600).
--profile-dir <DIR> Persist profile state under DIR.
--extract-text Print document body text.
--extract-selector <css> Print JSON source snapshots for matching elements.
--eval <js> Execute JS, print result.
--dump-dom Dump the DOM tree.
--focus <id> Focus an element by id.
--submit-form <id> Submit a form by id.
--cdp Start text/runtime CDP on 127.0.0.1.
--cdp-port <N> CDP port (default 9222, with --cdp).
--memory-stats Print memory statistics.
Native headless is text/runtime/profile-only. The removed CLI surface includes screenshots, incremental frames, coordinate clicks, layout/display-list/line and paint-stat dumps, and font listing. Native CDP methods that require geometry, pointer hit testing, semantic bounds, or pixels fail closed.
Without --profile-dir, each invocation owns and removes an isolated temporary
profile. With it, BrowserCore stores profile data in <DIR>/profile.redb; this
also applies to native --cdp.
Rendered automation runs the chrome-less Flutter host for the entire logical session. It owns one BrowserCore and uses the same formatter/commit/painter as the GUI. On Linux it runs under Cage/headless Wayland. No native fast path may invent geometry or pixels, and callers cannot select a graphics backend.
Stable error codes (returned exactly as written):
| Code | When |
|---|---|
unsupported.screenshot | Native/text-only CDP receives a screenshot request |
invalid-selector | Malformed --extract-selector input |
CDP methods required at v1.0:
Browser.getVersionTarget.createTarget,Target.attachToTarget,Target.getTargetsPage.enable,Page.navigate,Page.reload,Page.stopLoading,Page.loadEventFired,Page.getFrameTree,Page.getResourceTree,Page.getResourceContent,Page.getLayoutMetrics,Page.getNavigationHistory,Page.navigateToHistoryEntry,Page.resetNavigationHistory,Page.setBypassCSP, andPage.captureScreenshot(PNG)Runtime.enable,Runtime.evaluate,Runtime.awaitPromise,Runtime.getProperties,Runtime.consoleAPICalled, andRuntime.exceptionThrownNetwork.enable, top-levelNetwork.*navigation notifications, and the Playwright network-toggle methods (setCacheDisabled,setBypassServiceWorker,setExtraHTTPHeaders; extra headers apply to runtimefetch()requests, cache-disabled bypasses runtimefetch()cache reads/writes)DOM.getDocument,DOM.querySelector,DOM.querySelectorAll,DOM.describeNode,DOM.resolveNode,DOM.getContentQuads,DOM.getBoxModel,DOM.getAttributes,DOM.getOuterHTML,DOM.setAttributeValue, andDOM.removeAttributePerformance.getMetrics,Security.getSecurityStateInput.dispatchMouseEvent(mouse move/press/release over the current full viewport),Input.dispatchKeyEvent, andInput.insertText
Flutter GUI shell contract
Flutter is the sole web renderer and native GUI shell target on Linux, macOS, Windows, Android, and the Apple Silicon iOS Simulator. The Linux alpha baseline implements chrome and BrowserCore FFI over the Flutter mutation/commit renderer. The Linux GUI requires a native Wayland display and rejects X11/XWayland; rendered automation/CDP uses the same host under Cage.
Platform validation follows a rolling contemporary baseline: the latest stable major release of Linux's reference distribution, macOS, Windows client, Android, and iOS Simulator at each release cutoff. Exact versions and toolchains are recorded in release evidence. Older majors are best-effort unless explicitly promoted to an additional tested tier.
- BrowserCore owns browser/profile/context/document/runtime/computed-style/ resource-policy/accessibility meaning. Dart owns bounded CSS formatting, Paragraph/Canvas scenes, renderer commits/queries, chrome, and host-service UI.
- The Dart FFI bridge carries bounded typed commands/events and opaque handles with explicit lifetime, allocation, version, sequence, and generation rules.
- BrowserCore sends exact bounded mutation/full-resync revisions; Flutter returns one atomic scene/basic-geometry/text/scroll/semantic-bound commit with an opaque Flutter-side hit-test handle and a separate presented acknowledgement.
- Flutter hit-tests the displayed commit and owns mechanical scroll geometry. BrowserCore validates targets and owns event cancellation/defaults, script scroll intent, history/persistence, selection meaning, and navigation effects.
- BrowserCore semantic meaning plus Flutter commit bounds publish one native Semantics generation; actions name the exact displayed commit.
- Rendered CLI/CDP/WPT use a chrome-less Flutter host. Text-only utilities may remain native and GUI bundles need not ship developer automation entrypoints.
Platform acceptance, Android V8/GLES/split-ABI gates, the iOS Simulator track,
Linux release/FlatPark packaging, and artifact policy are specified in
FLUTTER_SHELL.md. JavaScript and WebAssembly use the same
deno_core/V8 runtime path on every declared target.
WPT harness — check types
The WPT harness asserts document state against fixture manifests. The committed
fixtures/manifest.json remains the hermetic release-blocking smoke suite.
Larger upstream slices may instead be described by small JSON WPT profiles and
run against an ignored checkout such as .tmp/wpt/ via just wpt-profile fixtures/wpt-profiles/<profile>.json .tmp/wpt. The check types below are the
public contract for fixture/profile authors.
| Check type | Asserts |
|---|---|
title | Document <title> text |
selector-count | Number of elements matching a selector |
selectors-exact | Exact set of element ids matching a selector |
body-contains | Body text contains a substring |
js-eval | Evaluate JS, compare result to expected |
flutter-js-eval | Evaluate JS whose result requires an exact Flutter commit |
min-nodes | DOM has at least N elements |
no-critical-diagnostics | No critical EngineDiagnostic recorded |
visual-hash | Perceptual hash of rendered screenshot matches expected |
selector-match | Per-element selector match details |
computed-style | Per-element computed style value matches expected |
element-attribute | Element attribute value matches expected |
layout-box | Element border-box (x, y, w, h) matches expected |
dom-nodes-range | DOM node count is within [min, max] |
ref-equivalent | Rendered page matches a reference HTML fixture |
WPT target profile lives in COMPAT.md. End-to-end CSS/DOM/layout
behavior should move into fixtures when practical. Target Rust tests cover pure
logic such as URL/cookie/CSP parsing and redb round trips; a CSS algorithm remains
in Rust only through ADR-022's explicit stable formatter contract and
cross-language tests. The committed manifest's
document/runtime assertions and rendered assertions execute in order against the
same fixture target in the chrome-less Flutter host. Native wpt_runner retains
1,868 source/runtime checks only; Flutter commits are authoritative for
flutter-js-eval, layout boxes, visual hashes, and reference comparisons.
Diagnostics shape
#![allow(unused)] fn main() { pub struct EngineDiagnostic { pub category: EngineDiagnosticCategory, pub code: &'static str, // e.g. "parse-dom.budget" pub message: String, } pub enum EngineDiagnosticCategory { Network, ParseDom, ScriptRuntime, LayoutRender, StorageCache, } }
The GUI shell surfaces diagnostics in chrome; the WPT
no-critical-diagnostics check consumes them. Codes are stable contract.
URL policy
Every network fetch passes through validate_http_url. The blocklist is
Vixen's configuration of what counts as a "public" HTTP target.
#![allow(unused)] fn main() { use std::net::{Ipv4Addr, Ipv6Addr}; use url::{Host, Url}; #[derive(Debug, Clone)] pub enum UrlPolicyError { UnsupportedScheme(String), BlockedHost { host: String }, } pub fn validate_http_url(url: &Url) -> Result<(), UrlPolicyError> { if !matches!(url.scheme(), "http" | "https") { return Err(UrlPolicyError::UnsupportedScheme(url.scheme().to_owned())); } if let Some(host) = url.host() && is_private_host(&host) { return Err(UrlPolicyError::BlockedHost { host: host.to_string() }); } Ok(()) } pub fn is_private_host(host: &Host<&str>) -> bool { match host { Host::Ipv4(ip) => is_private_ipv4(*ip), Host::Ipv6(ip) => is_private_ipv6(*ip), Host::Domain(domain) => { let lower = domain.to_lowercase(); lower == "localhost" || lower == "localhost.localdomain" || lower.ends_with(".local") || lower.ends_with(".internal") || lower.ends_with(".onion") || lower.ends_with(".arpa") || lower.ends_with(".test") || lower.ends_with(".example") || lower.ends_with(".invalid") } } } fn is_private_ipv4(ip: Ipv4Addr) -> bool { ip.is_loopback() || ip.is_private() // 10/8, 172.16/12, 192.168/16 || ip.is_link_local() // 169.254/16 || ip.is_unspecified() // 0.0.0.0 (unspecified only) || ip.is_broadcast() // 255.255.255.255 || ip.is_documentation() // 192.0.2/24, 198.51.100/24, 203.0.113/24 || is_cgnat(ip) // 100.64.0.0/10 } fn is_cgnat(ip: Ipv4Addr) -> bool { let o = ip.octets(); o[0] == 100 && (o[1] & 0xc0) == 0x40 // 100.64.0.0/10 precisely } fn is_private_ipv6(ip: Ipv6Addr) -> bool { ip.is_loopback() // ::1 || ip.is_unspecified() // :: || ip.is_unique_local() // fc00::/7 || (ip.segments()[0] & 0xffc0) == 0xfe80 // link-local fe80::/10 || ip.to_ipv4_mapped().is_some_and(is_private_ipv4) } }
Cookie defaults
Cookies follow RFC 6265 with these Vixen-specific defaults:
- Default
SameSiteisLax(matches modern browsers, not strict RFC 6265 which has no default). - Storage cap: 512 entries per jar. Eviction is FIFO by insertion order (not the RFC's full eviction algorithm). This is a deliberate simplification.
HttpOnlyrejected fromdocument.cookiebut accepted fromSet-CookieHTTP response. This is RFC-correct but called out because it's a frequent bug source.- Outgoing
Cookieheader:SameSite=Laxcookies are sent cross-site only for safe methods (GET/HEAD/OPTIONS).SameSite=Strictcookies are sent only to same-host requests.HttpOnlycookies never appear indocument.cookiereads. - Domain policy uses the static Mozilla Public Suffix List, including its private section. Parent public-suffix attributes are rejected; an exact-host public suffix is converted to host-only as required by RFC 6265bis.
Everything else (domain matching, path matching, secure-gating,
expiry handling, Max-Age semantics) follows RFC 6265 exactly.
CSP enforcement points
CSP is parsed from Content-Security-Policy headers and
<meta http-equiv="Content-Security-Policy">. Enforcement happens at
three boundaries:
- Script execution —
script-src(ordefault-srcfallback). Inline scripts blocked unless'unsafe-inline'or a matching hash/nonce is present. - Fetch —
connect-src,img-src,style-src,font-src,media-src,object-src, etc. URLs matched against source-list. - Plugin content —
<embed>,<object>allowed only ifobject-srcpermits.
Source-list grammar follows the CSP spec exactly ('self', 'none',
'unsafe-inline', 'unsafe-eval', host/scheme sources, nonces,
hashes).
Form validation edge cases
These are pinned down because they're easy to get subtly wrong.
Email format (typeMismatch for type="email"):
- Exactly one
@. - Non-empty local-part.
- Domain contains at least one
..
URL format (typeMismatch for type="url"):
- Valid scheme (letters followed by
:). ://separator after the scheme.- Non-empty host.
Step arithmetic (stepMismatch):
- Step base =
minif present, else the type-specific default base. - Default step per type: number/range = 1; date = 1 day; time = 60 s; week = 1 week; month = 1 month; datetime-local = 60 s.
- Valid when
(value - step_base)is within float tolerance of an integer multiple ofstep. - Date/time values use integer arithmetic on canonical units:
date→ days since epoch,time→ seconds since midnight,week→ weeks since epoch,month→ months since year 0,datetime-local→ epoch seconds.
Everything else in constraint validation (valueMissing,
rangeUnderflow/rangeOverflow, tooLong/tooShort, badInput,
customError, willValidate) follows the HTML5 spec exactly.
Composed event dispatch invariants
Specific ordering invariants that must be reproduced exactly.
Focus transitions (when document._setActiveElement runs):
focusout → focusin → blur → focus
focusoutandfocusinbubble.blurandfocusdo not bubble.
composedPath() walks target → parentNode chain, returning a flat
JS array. Respects shadow DOM boundaries when composed: true on the
event.
Renderer commit and paint invariants
These rules apply to the Flutter-hosted formatter and every GUI/automation surface:
- Exact revisions — a mutation batch applies only to its named base and
target
RenderRevision; gaps request full resync. - Atomic commit — scene-ready layout, basic geometry, an opaque Flutter-side
hit-test handle, text query state, scroll snapshot, semantic bounds, and
truncation share one
commit_idand revision. - Presented identity — input and native accessibility identify the displayed commit, not merely the newest completed layout.
- Stable paint order — stacking contexts, z-index, positioned content, and document-order ties follow CSS; viewport background remains first.
- Clip and transform identity — paint, hit testing, text/caret queries, and semantic bounds consume the same clip/transform chain.
- Opacity and visibility — group opacity composes through ancestors;
opacity: 0and hidden/collapsed paint are omitted while required layout state remains queryable. - Scroll identity — renderer offsets/extents/clips and the scene share one commit; BrowserCore scroll events/history accept only that result.
- Finite bounded geometry — non-finite, oversized, unknown-node/resource, over-depth, or required-but-truncated geometry fails closed.
- No stale fallback — stale commits cannot target input, answer required geometry, publish Semantics, or become visible after replacement.
- One renderer — after cutover no WebRender/EGL/RGBA or second screenshot path remains. Text-only tools cannot fabricate geometry.
Runtime Web Platform strategy
Vixen exposes the browser runtime through deno_core/V8. This document defines
where Web API code should live so the runtime stays fast, small, and spec-driven.
Fixed constraints
deno_core/V8 is the only JS runtime target.- Do not add a generic JS-engine abstraction.
- Generated WebIDL substrate stays in
crates/vixen-engine/src/script/webidl.rs. - Host-family extensions adopt generated interfaces with
webidl.adoptInterface(...). - DOM, CSS cascade/computed styles, network policy, and storage remain BrowserCore source of truth. Flutter owns commit-bound CSS formatting/layout.
- Security-sensitive behavior validates near the host boundary and fails closed.
- Flutter/Dart is the web formatter/renderer and browser chrome. It does not implement page JavaScript, DOM/Web APIs, navigation, storage, policy, or a fallback runtime.
- The same BrowserCore/
deno_coreruntime is the target on all five GUI platforms. Platform support is evidence-gated, not inferred from Flutter.
Fidelity ladder
Use this ladder when adding or reviewing an API:
- Shape — constructor/prototype exists because WebIDL requires it.
- Pure value behavior — JS-only implementation is acceptable when it has no privileged state, I/O, persistence, origin policy, or layout dependency.
- BrowserCore op/resource backing — required for page DOM, CSSOM, network, storage, history, permissions, timers, and anything security-sensitive; layout-dependent APIs use the bounded Flutter renderer broker.
- Spec/WPT correctness — useful subset covered by local or imported WPT fixtures; this is the target for committed behavior.
Shape-only APIs are temporary compatibility scaffolding. Do not keep widening shape if the MVP needs deeper behavior in an already-exposed family.
JS bootstrap vs Rust ops
Choose the fastest/smallest correct implementation:
- Keep pure value objects in JS bootstrap when doing so avoids Rust/V8 glue and does not duplicate an authoritative source of truth. Examples: event objects, geometry value wrappers, iterator ergonomics, small serialization helpers.
- Use Rust ops/resources when behavior touches parsed page state, CSS cascade, network, storage, origin/security policy, long-lived handles, or mutable browser state. Layout-dependent ops query exact accepted Flutter commits rather than implementing layout in Rust.
- Avoid two authoritative paths. The obsolete Page string-expression evaluator is deleted; do not reintroduce expression classifiers or fallback eval paths.
Lessons from the first host-object migrations
- One eval path beats clever fallbacks. Headless
--eval, CDPRuntime.evaluate, and WPTjs-evalall use BrowserCore/JsRuntime. - Expose only after behavior exists. Replace generated WebIDL placeholders
with host behavior before claiming support; there is no legacy evaluator to
conceal
unsupportedMember. - Small vertical slices are safer than broad shape. A narrow family such as document metadata, collections, or form reflections should include the op/data source, JS bootstrap member, headless routing, and CDP/WPT-visible proof in the same change.
- BrowserCore remains authoritative for browser state. JS bootstrap may cache and compose objects inside one realm, but page mutations, navigation actions, storage, cookies, and fetch policy commit through Rust-backed ops/resources. Layout geometry comes only from an exact accepted Flutter commit surfaced through those ops.
- Fail-closed errors are part of compatibility. Unsupported selectors, bad storage keys, private-network fetches, malformed host operations, and missing elements should produce deterministic errors instead of silently widening the smoke surface.
Single-path evaluation rules
- New API families start in a
script::<family>extension or in a JS-only value bootstrap adopted onto generated WebIDL prototypes. - A supported behavior requires a focused
vixen-engineruntime test and one user-visible seam (--eval, CDP, or WPT fixture). - Never add expression classifiers or fallback evaluators. Converge transitional document snapshots with live page-backed resources instead.
Current direction
The broad runtime surface is useful for CDP/headless compatibility, but the next runtime work should deepen correctness and converge state:
- move backend-backed APIs from stubs/smoke shape to Rust-backed behavior,
- replace transitional runtime/document snapshots with live page-backed resources,
- preserve parser classics and deferred V8 modules with per-turn microtask checkpoints and bounded post-load/automation document tasks,
- import focused WPT cases for each widened API family,
- keep generated WebIDL prototype inheritance intact,
- keep CDP and headless
--evalconsuming the same runtime path.
Android requires a pinned rusty_v8/V8 source archive and toolchain with a proved
source cross-build for each shipped ABI. The iOS target is Apple Silicon Simulator
only and builds rusty_v8 for aarch64-apple-ios-sim, retaining the same V8
JavaScript and WebAssembly path. There is no JavaScriptCore, WKWebView, WebKit,
alternate Wasm runtime, or physical-device fallback.
WebAssembly remains V8-backed on every declared target. Widen it with identical module validation, memory/table limits, deadline cancellation, host-call policy, and conformance fixtures rather than adding a platform-specific implementation.
Required proof for a host-family change
Each non-trivial host-family change should include:
- a focused runtime test in
vixen-engineorvixen-headless, - one user-visible seam check when applicable (
--eval, CDP, or WPT fixture), - a note in
COMPAT.mdonly when support level or known gaps materially change, - green
just gate-phase6before push.
Decision records
Architecture decisions for Vixen, recorded ADR-style. Each entry carries context, the decision, the alternatives considered, and the consequences.
This file contains the current accepted decisions only. Superseded decision
bodies are removed rather than retained as competing guidance; Git history is
the historical record. When direction changes, consolidate the surviving
constraints into the replacement ADR and update PROJECT_DIRECTION.md,
ARCHITECTURE.md, and ROADMAP.md in the same batch.
ADR-001: Delegate spec-heavy primitives, own browser integration
Status: accepted; migration steps 1–7 implemented
Context. A modern browser cannot credibly reimplement every parser, cascade, JavaScript, text, and graphics primitive. Whole-engine embedding, however, would bring another product's navigation, network, persistence, and frontend ownership and fight Vixen's focused architecture.
Decision. Reuse focused upstream components behind Vixen-owned lifecycle and policy boundaries:
| Subsystem | Selected foundation |
|---|---|
| HTML parsing | html5ever |
| CSS cascade and selector matching | Stylo / selectors |
| JavaScript | deno_core / V8 |
| Native cross-platform scene, text, images, and accessibility | Flutter engine and dart:ui |
Vixen owns BrowserCore, navigation, network/security policy, persistence, Web APIs, CSS formatting semantics, the renderer mutation/commit protocol, and compatibility evidence. Flutter supplies the cross-platform rendering substrate; it is not treated as a CSS engine or a source of browser policy.
Alternatives considered.
- Build every primitive from scratch. Rejected as too slow and permanently trailing compatibility.
- Embed Servo, WebKit, or another whole browser engine. Rejected because it creates a second browser lifecycle and contradicts ADR-002.
- Use generic Flutter widgets as CSS layout. Rejected because widget layout is not web formatting; Vixen still implements and WPT-tests CSS semantics.
Consequences. Upstream crates/frameworks set important capability and binary costs, but Vixen owns their integration and support claims. Component API shape never counts as browser behavior without a production BrowserCore-to-renderer vertical and executable evidence.
ADR-002: Single-engine project, no fallback engine
Status: accepted
Context. A browser project can support multiple engines behind an abstraction (e.g. WebKit + custom, switchable at compile time or runtime). This doubles the maintenance surface for no end-user win: every shell change must be validated against both engines, dependency isolation requires constant auditing, and only one engine can be the production path anyway.
Decision. Vixen has exactly one engine: the component-backed BrowserCore described in ADR-001. There is no WebKit fallback, no compile-time engine selection, and no runtime engine switching.
Alternatives considered.
- WebKitGTK as production + custom engine as preview. Rejected: at that point the project is a WebKitGTK wrapper, not a browser engine project. If WebKitGTK is the goal, use GNOME Web directly.
- Compile-time engine feature flag (one binary, either engine). Rejected: adds dep-leak gates, doubles test matrix, no end-user benefit.
Consequences.
- One engine to test, one engine to ship, one engine to document.
- Replacing BrowserCore or adopting a whole engine requires a superseding ADR; it is not an adapter or fallback feature.
- Compatibility claims come from Vixen's measured production path, not from the lineage of individual parser, cascade, runtime, or renderer components.
ADR-004: Drop the multi-process JS sandbox
Status: accepted
Context. A previous design used a process-per-origin JS sandbox (spawned binaries communicating over IPC) for isolation. The embedded JS runtime already provides in-process context isolation, and out-of-process isolation (proper OOPIF) is a separate, much larger effort.
Decision. Single-process engine. JS isolation is via runtime contexts (one
per origin once host bindings are widened). No JsSandbox, no JsSandboxPool,
no process_pool, no ipc module.
Alternatives considered.
- Keep the multi-process sandbox. Rejected: the complexity (IPC framing, pool management, origin-keyed spawn) is not justified by the security payoff for a single-user browser. Site isolation, if ever needed, is a future Servo-style OOPIF effort.
Consequences.
- ~1.5 kLOC less code.
- A single malicious page can still OOM or hang the engine process. This matches every other browser's pre-OOPIF behaviour.
- If genuine site isolation becomes a v1.x goal, design it as OOPIF against the upstream Servo pattern, not as a forked-engine-per-origin approach.
ADR-008: WebGPU and media are post-v1.0
Status: accepted
Context. WebGPU and media playback are real features but require substantial integration work with Flutter's scene/platform-texture lifecycle, device policy, codecs, permissions, and security. Neither is on the critical path for the first useful browser corridor.
Decision. WebGPU and media are outside the v1.0 gate. Promote them after v1
by measured corridor impact. WebGPU uses one bounded native wgpu device/policy
integration presented through Flutter's scene; media uses platform codec/
GStreamer services and Flutter-compatible textures under BrowserCore autoplay,
permission, lifecycle, and resource policy. Neither adds a second page renderer.
Alternatives considered.
- Build WebGPU/media scaffolding now, fill in backends later. Rejected: scaffolding without backends is dead code that rots and misleads users.
Consequences. v1.0 does not claim WebGPU or media playback. API reflection stays explicitly inert/unsupported until the corresponding subsystem, policy, renderer integration, and compatibility evidence exist.
ADR-011: Stylo via the crates.io-published stylo crate
Status: accepted
Context. ADR-001 commits to Stylo (style) for the CSS cascade.
When Phase 0–2 landed, style was only available as a Servo git
dependency — a clone of https://github.com/servo/servo plus a
[patch.crates-io] table. That made the build non-reproducible from
crates.io alone and left Phase 3 marked "blocked" in docs/PLAN.md.
Since then, the Stylo team split the engine out of the Servo monorepo
into https://github.com/servo/stylo and now publish it on crates.io
as stylo (lib name style). All
subsystems Vixen needs — cascade, selector matching, rule tree,
computed values — are in that crate.
Decision. Depend on stylo = "0.18" (with the servo feature for
the non-Gecko config) directly. Do not pull a Servo git checkout, do
not patch crates.io, do not vendor the source. Implement
selectors::Element (and, for the cascade, TNode/TElement/
TDocument) over Vixen's html5ever RcDom in
crates/vixen-engine/src/style_dom.rs.
Alternatives considered.
- Hand-roll selector matching on top of
selectorsalone, defer the cascade. Rejected: doubles the selector-matching surface (Vixen's plus Stylo's), and the cascade is the actual reason we wanted Stylo in the first place. - Pin a Servo git revision of
style. Rejected: bigger dep surface (the wholeservorepo at that revision), non-reproducible from crates.io, blocks Phase 3 indefinitely. - Switch CSS engine to
taffyor another standalone cascade. Rejected per ACCEPTANCE.md hard gates (notaffy); also re-introduces the perpetual trailing-edge compatibility ADR-001 rejects.
Consequences.
- Phase 3 unblocks. The selector-matching surface (
vixen-engine:: style_dom) is live; the WPT selector fixtures pass end-to-end. - The crate ships with its lib name as
styleeven though the package isstylo; source usesuse style::…whileCargo.tomlsaysstylo = …. Documented instyle_dom.rsto head off confusion. - The dependency increase is an accepted trade for a real cascade. Dependency
and artifact costs remain measured; numerical limits become gates only from
reproducible baselines under
BASELINES.mdandACCEPTANCE.md. - Future Stylo releases may shift trait shapes (
TElementetc.). Pinstylo = "0.18"and bump deliberately; track upstreamhttps://github.com/servo/stylo/releases.
ADR-014: Move JS runtime to deno_core
Status: accepted
Context. The first Phase 2 implementation used mozjs because the original
plan optimized for Firefox-family components end-to-end. The later Phase 6 work
showed that Vixen's actual risk is the Rust-side host API layer: object
registration, bootstrap JS packaging, resource/permission boundaries, testing,
and long-term maintenance of many Web API families. The deno_core crate solves
that packaging problem directly. It brings a well-maintained Rust embedding layer
for V8, explicit extension/op registration, module loading, resource tables,
structured errors, and the runtime architecture Deno uses to expose large Web API
surfaces from Rust.
deno_core does mean Vixen no longer uses a Firefox-family JS engine. That is an
acceptable trade: JS language compatibility comes from V8, Web API compatibility
remains Vixen-owned and fixture/WPT-gated, and Rust host-layer velocity matters
more for alpha progress than preserving SpiderMonkey specifically.
Decision. Migrate Vixen's JS runtime from mozjs/SpiderMonkey to
deno_core/V8 and use deno_core directly inside vixen-engine::script. Do
not introduce a generic JS-engine abstraction or a dyn JavaScriptRuntime layer:
Vixen has one JS runtime target, and deno_core already provides the embedding
API shape we want. The migration has landed behind the existing
JsRuntime/JsValue, headless --eval, and CDP Runtime.evaluate seams.
The target JS architecture is Deno-shaped:
- Host API families live in small modules under
vixen-engine::scriptor pure sibling modules, not as one ever-growingscript.rsfile. - Each family has a Rust op/resource surface, a JS bootstrap surface, and focused tests. The Rust side owns validation and stable errors; JS glue owns Web-shaped object ergonomics only.
- Registration uses a Deno-style extension list: ordered, explicit, testable,
and feature-family scoped (
encoding,dom,url,fetch,storage, etc.). - Long-lived host state should use explicit resource IDs/handles and permission
checks near the op boundary, following
deno_core/Deno resource-table and permissions patterns rather than ad-hoc globals. - Bootstrap JS is packaged as static assets or generated strings owned by the feature module, with Rust tests proving the installed surface.
Alternatives considered.
- Stay on SpiderMonkey and only mimic Deno packaging. Rejected: it keeps the
hard part — building and maintaining a browser-scale Rust host layer — while
missing the maintained
deno_coreabstractions that solve that exact problem. - Abstract over
mozjsanddeno_corebehind an internal JS-engine trait. Rejected: it would preserve two runtime mental models, hide usefuldeno_coreconcepts like extensions/resources/ops behind a leaky common denominator, and create a test matrix Vixen does not intend to support. - Keep all host glue inside
script.rs. Rejected: it does not scale past the first few host-object slices and hides feature-family boundaries. - Adopt Deno wholesale, including CLI/npm/Node compatibility. Rejected: Vixen
needs
deno_core, not the Deno product surface. Node/npm semantics are not part of the browser runtime. - Copy Firefox WebIDL binding generation immediately. Deferred: Firefox's
binding stack is authoritative for many DOM semantics, but
deno_coreis the better Rust embedding/runtime substrate for Vixen.
Consequences.
deno_coreis thevixen-engine::scriptdependency;mozjsis no longer in the active engine dependency graph.- Internal host modules may depend on
deno_coreAPIs directly. The stable seam is the Vixen product API (JsRuntime,JsValue, headless/CDP behavior), not a portable JS-engine adapter. - Binary-size gates must be remeasured for V8. The old system/static mozjs split no longer applies.
docs/REFERENCES.mdpins Deno as the primary JS runtime/host packaging reference. Firefox remains a DOM/Web API semantic reference, but not the JS engine target.- New JS host families should be reviewed for module size, bootstrap locality, explicit registration, and permission/resource boundaries.
- Existing Page string-smoke projections and bootstrap snapshot pilots should
migrate into explicit
deno_coreop/resource extensions one family at a time, while still reusing the same pure Rust modules.
ADR-016: hk owns git lifecycle gates
Status: accepted
Context. The previous gate story mixed raw cargo commands, many just gate-*
recipes, manual pre-push habits, and ad-hoc agent summaries. Iteration speed is a
north-star concern, but work leaving the machine still needs consistent checks.
The project already uses mise, and hk is built by the same toolchain ecosystem
for fast git hook orchestration.
Decision. Add checked-in hk.pkl and make hk the git lifecycle enforcement
layer. just remains the project command library; hk decides when those recipes
run. Pre-commit stays quick and mostly local: formatting, merge-conflict/private
key scans, and staged diff whitespace. Long gates run only pre-push through one
recipe, just gate-push.
The standard pre-push gate is:
just gate-alpha
just gate-phase6
just gate-smoke
git diff --check
git diff --cached --check
Alternatives considered.
- Keep manual gate discipline. Rejected: too easy for long autonomous sessions to drift.
- Run all long gates pre-commit. Rejected: hurts iteration speed and produces small, slow commits.
- Replace
justwith hk commands. Rejected:justrecipes are still useful as explicit project actions and documentation anchors.
Consequences.
- Agents may commit and push automatically when hk gates pass.
- Hook setup is part of normal mise/bootstrap workflow.
- If pre-push becomes too slow or misses an important area, change
just gate-pushfirst; keep hk pointing at that stable recipe.
ADR-017: One engine-owned browser, profile, and context lifecycle
Status: accepted
Context. Sharing component types is not the same as sharing a browser. Profile sharing, independent tabs, navigation cancellation, stale-result rejection, downloads, renderer commits, and runtime recovery need one owner above an individual document, renderer, or protocol session.
Decision. vixen-engine owns one BrowserCore per open profile. It runs on
an engine-owned thread/local executor suitable for the non-Send DOM and
deno_core::JsRuntime, and owns:
- profile storage, cookies/cache, permissions, HSTS, downloads, clear-data policy, and host configuration;
- the top-level browsing-context registry and future child frames;
- context-scoped history, sessionStorage, viewport/input intent, active navigation, runtime realms, and committed document state; and
- document-scoped DOM, computed style, render-source revisions, accepted renderer commits, script resources, accessibility meaning, and inspector state.
Commands and events cross a browser-scoped vixen-api seam and carry typed
context/navigation/document/request/runtime/download/render ids. Asynchronous
work carries its creation generation. Cancellation or supersession invalidates
that generation; late network, script, renderer, geometry, or persistence results
are rejected before mutation, side effects, input targeting, or success events.
The Flutter GUI/chrome-less renderer, text CLI, CDP, and WPT harness are adapters over BrowserCore. They may own bounded ephemeral renderer state/resources, scenes, widgets, sockets, and protocol routing, but not alternate navigation, history, page-runtime, permission, cookie/cache, or profile state. Vixen has one concrete engine; this seam is not an engine-plugin abstraction.
Alternatives considered.
- One independent engine per tab with a shared store. Rejected: profile state, downloads, renderer scheduling, target routing, and clear-data operations need coordinated in-memory ownership.
- Keep frontend coordinators and share helpers. Rejected: helpers cannot define atomic commit, cancellation, ordering, or teardown across independent owners.
- Move DOM and V8 into Flutter. Rejected: browser truth would depend on renderer scheduling and protocol adapters would acquire divergent behavior.
- Make every subsystem
Send + Syncand distribute it immediately. Rejected: it adds locking/reentrancy before measured isolation or throughput requires it.
Consequences.
just gate-architectureforbids frontend direct composition of network/store leaves and independent browser orchestration.- Two contexts can own independent documents/runtimes/render revisions while sharing only intended profile state.
- Navigation, stop, history, downloads, error pages, and renderer reset use one lifecycle and diagnostic model.
- The owner thread is a reliability boundary. Long script and synchronous
EnsureLayoutwork need cancellation, deadlines, and deadlock-safe scheduling.
Implementation status (2026-07-14). BrowserCore owns production contexts, profiles, navigation generations, DOM/V8 state, and ordered events for Flutter, headless, CDP, and WPT. Main-document, external-script/stylesheet, and bounded PNG loads are generation-cancellable; V8 jobs and runtime fetch waits are deadline- bounded and interruptible. ADR-022's render mutation/commit ownership is the next architecture migration.
ADR-019: Validate Flutter targets on explicit supported OS baselines
Status: accepted
Context. Carrying a broad legacy OS matrix before Vixen has one supported release multiplies native runner, graphics, accessibility, signing, and CI work without compatibility evidence. Flutter's own support range is not evidence that BrowserCore, V8, Vixen's Flutter renderer, or packaging works throughout that range.
Decision. At each release cutoff, Vixen validates one explicit supported baseline for each target OS. Linux development and host-side CI use x86_64 Ubuntu 24.04 (host-side CI on GitHub runners; a matching Distrobox is optional for local development), while distribution uses the current pinned Flatpak/GNOME runtime. macOS uses the latest stable macOS major; Windows uses the latest stable client release and feature update; Android uses the latest stable major/API; and iOS Simulator uses the latest stable simulator major in the latest stable Xcode on current macOS. Release evidence pins exact versions and architectures. Fedora and newer Ubuntu releases are forward-compatibility checks; preview releases do not satisfy gates.
Older releases are best-effort unless a release explicitly adds them as tested tiers. This policy may move forward at any release after native build, rendering, input, accessibility, lifecycle, packaging, and performance gates pass on the new baseline.
Consequences. Vixen can adopt current platform APIs and security behavior without promising an untested legacy matrix. Users receive an exact release manifest rather than an ambiguous “Flutter supports it” claim. Expanding backward compatibility remains possible, but requires measured demand and its own ongoing gate capacity.
ADR-020: Linux Flutter GUI is native-Wayland-only
Status: accepted
Context. Supporting both native Wayland and X11/XWayland duplicates native window, compositor, input/IME, accessibility, lifecycle, GPU, and release-smoke matrices while Linux browser usability is still converging. Ubuntu 24.04 and the pinned GNOME distribution runtime provide the controlled native Wayland targets. ADR-022's Linux rendered automation also benefits from one controlled native Wayland environment under Cage.
Decision. The packaged Linux Flutter GUI requires GTK to select a native Wayland display. Startup on X11 or XWayland exits nonzero with an explicit diagnostic. Local isolated GUI testing, release archive launch evidence, and native AT-SPI evidence use Cage with wlroots' headless Wayland backend. FlatPark permissions will expose Wayland and will not request X11 or fallback-X11. Rendered CLI, CDP, WPT, and screenshot automation use the chrome-less Flutter host under Cage after ADR-022 cutover. Text-only native utilities need no display.
Consequences. Vixen has one Linux GUI display-server matrix and can focus native work on Wayland input, IME, accessibility, portals, scaling, and surface recovery. X11-only sessions cannot launch the supported GUI, and XWayland is not a compatibility fallback. Reintroducing X11 requires a new ADR plus dedicated window/input/IME/accessibility/GPU/release gates; framework capability alone is not sufficient.
ADR-022: Flutter owns web layout, paint, and rendered automation
Status: accepted
Context. Vixen targets one focused browser across Linux, macOS, Windows, Android, and the Apple Silicon iOS Simulator, with Linux first. The implemented WebRender plus offscreen-EGL path gives GUI and native headless one paint backend, but Vixen still owns GPU context creation, frame readback/transport, font shaping/fallback, renderer recovery, and platform texture integration before it improves web compatibility. Flutter then presents those pixels through another graphics stack.
Flutter already supplies the supported cross-platform scene, Canvas, Paragraph, font, image, accessibility, lifecycle, and capture substrate on all five targets. Using it only for chrome leaves that leverage unused. Flutter is not a CSS engine, so Vixen must still implement and WPT-test web formatting, fragmentation, scroll, and inspection semantics.
Decision. Flutter is Vixen's sole rendered frontend: it owns web formatting, text/image measurement, paint, hit testing, semantic geometry, scene presentation, and rendered automation as well as browser chrome. BrowserCore remains the sole owner of profile, contexts, navigation, committed DOM, V8, Stylo cascade/computed styles, resource/security policy, storage, history, downloads, web-event semantics, and durable accessibility meaning.
Vixen targets Flutter's public Canvas/Paragraph/scene APIs with Impeller as the required engine rendering backend. The runner enables Impeller explicitly; a Skia-backed launch does not satisfy renderer, release, or platform evidence. The pinned Flutter stable SDK is deliberate: Linux Impeller reached stable 3.47 (Impeller is the default desktop renderer), so the pin supplies the required Linux Impeller support without a beta channel. Vixen does not call private Impeller APIs or add a backend-specific paint path; Flutter remains the boundary.
The product targets are Linux, macOS, Windows, Android, and the Apple Silicon iOS Simulator. Linux is the first renderer, GUI, chrome-less automation, packaging, and release gate. Physical iOS and App Store distribution require a later runtime/distribution decision. Flutter is the only GUI and the only rendered headless substrate; no fallback native renderer is retained after cutover.
Renderer source protocol
BrowserCore publishes bounded mutation batches over an exact compound revision:
RenderRevision {
context_id
document_id
source_revision
style_revision
viewport_revision
resource_revision
}
RenderMutationBatch {
base_revision
target_revision
mutations
}
Mutations describe immutable styled render inputs, stable DOM/resource/semantic ids, accepted text/image/font resources, pseudo/generated content, scroll intent, and removals. They are not a Dart DOM and cannot be mutated into browser state. The renderer builds CSS box and anonymous trees from them. A missed base revision fails closed and requests a bounded full snapshot; batches are never guessed, reordered, or applied to another document.
Dart may retain only bounded active renderer generations and resources. Node, mutation, string, depth, image/font byte, fragment, query, and queue limits apply at the bridge. BrowserCore owns web-font fetch, CSP/CORS/integrity/cache policy and passes only accepted resources to Flutter's font collection.
Flutter renderer ownership
Vixen implements CSS block, inline, flex, grid, positioned, overflow, replaced- element, table, and fragmentation behavior in Dart against computed BrowserCore inputs. Ordinary Flutter widgets, Flutter Flex, and third-party UI layout packages are not treated as CSS implementations. A widget-per-DOM model is neither required nor allowed to become durable browser state.
Flutter's dart:ui Paragraph is authoritative for shaping, fallback, bidi, line
breaking, intrinsic text measurement, caret/range geometry, and text hit testing.
Canvas/scene APIs own clipping, transforms, paint order, compositing, images, and
capture. BrowserCore must not retain competing approximate layout/text metrics
after cutover.
Atomic renderer commit
A renderer commit means layout, query data, semantic bounds, and a scene are ready for the same source revision:
RenderCommit {
commit_id
render_revision
viewport
geometry_index
hit_test_handle
text_query_handle
scroll_snapshot
semantic_bounds
truncation_state
}
Basic immutable border/padding/content/fragment/clip/scroll/paint-order geometry is returned to BrowserCore so ordinary synchronous DOM/CSSOM/CDP queries do not cross FFI repeatedly. Flutter remains authoritative because it produced that index; BrowserCore only validates and queries it. The hit-test handle names an immutable Flutter-retained index and is opaque to BrowserCore: Flutter resolves it and returns a bounded target for validation. Paragraph-specific offset, caret, range-box, affinity, and selection operations may use a bounded batched renderer query service.
RenderCommit and Presented(commit_id) are distinct. Geometry can be accepted
before presentation, but visible input and native accessibility identify the
actually displayed commit. BrowserCore rejects commits, queries, input targets,
scroll results, and semantic bounds whose context, document, viewport, resources,
or revision no longer match.
Synchronous layout broker
A script can mutate style and synchronously call getBoundingClientRect() in the
same task. BrowserCore therefore exposes bounded EnsureLayout(required_revision)
and geometry-query operations through a request/response broker:
V8 geometry read
→ BrowserCore flushes DOM + Stylo
→ publishes required RenderMutationBatch
→ posts EnsureLayout to the Flutter renderer
→ waits for matching RenderCommit or cancellation/deadline
→ answers from committed geometry
The BrowserCore owner thread may wait without holding browser mutexes. The Flutter UI/renderer isolate processes the request without re-entering BrowserCore and returns through a separate response channel. Navigation, stop, close, and shutdown cancel the wait; late commits are inert. The current polling event worker cannot be the only broker if it is blocked on the originating evaluation. Cutover is blocked until this path is deadlock-safe, bounded, and tested with same-task mutation plus geometry reads.
Input, scroll, and accessibility
Flutter performs hit testing against the displayed commit and sends a bounded
target containing commit/revision, stable node/fragment ids, and coordinates.
BrowserCore validates that target before DOM dispatch. It owns cancelable event
semantics and default-action policy. Flutter owns mechanical scroll geometry,
offsets, clips, and clamps; BrowserCore sends an accepted scroll command only
after preventDefault() and owns DOM scroll events, script intent, history
restoration, and persistence. Renderer scroll results return in a new exact
commit.
BrowserCore authors accessibility role, name, value, state, relationships, focus, policy, and actions. Flutter contributes accepted semantic bounds/text geometry, combines them only for the displayed commit, and publishes native Semantics. Actions route back with exact document, commit, semantic node, and advertised action generation.
Rendered automation
A minimal chrome-less Flutter host creates BrowserCore, accepts CLI/CDP/WPT requests, and captures the exact Flutter scene/commit. Linux runs it under Cage/wlroots headless Wayland. Other platforms use their native Flutter runner when their rendered gates begin. Text-only utilities may remain native clients when they do not invent geometry or pixels. One logical rendered session owns exactly one BrowserCore inside the host; a native launcher never splits fast DOM/runtime commands into a second core. GUI bundles need not ship developer automation entrypoints.
Migration and deletion policy
The former Rust layout/display-list/WebRender/EGL/RGBA path was transitional and was deleted by R7. Do not recreate it or preserve compatibility shims merely because historical scaffolding or plans exist.
Flutter renderer work remained test-only until one controlled vertical proved
layout, pixels, input, geometry, text ranges, scroll, Semantics, and scene
capture from one commit. There are never two supported production renderers. R7
removed WebRender/gleam, GlContext, headless/frame EGL,
image upload, RGBA frame ABI/pools, the Dart frame worker, pixel-buffer texture
plugin/presenter and recovery tests, superseded Rust paint/layout modules or
DTOs, duplicate scale/hit/scroll/text/semantic projections, obsolete fixtures/
gates/docs/dependencies, and renderer-internal CLI flags. Pure CSS algorithms may
be moved/reused only when the Dart formatter consumes them through an explicit
stable data contract and that is simpler than reimplementation.
Alternatives considered.
- Keep WebRender and Flutter only for chrome. Rejected: it preserves the cross-platform GPU/font/surface burden and duplicates graphics stacks.
- Capture the current Flutter window under Cage. Rejected as a migration: the current window already contains WebRender/EGL pixels, so capture removes nothing.
- Use Flutter only as a painter over Rust final geometry. Useful only as the first proof; rejected as the destination because layout, text, hit testing, semantics, and pixels could diverge.
- Map every DOM node to ordinary Flutter widgets. Rejected because widget layout is not CSS and a mutable widget tree would become a second DOM.
- Keep separate Flutter GUI and Rust headless renderers. Rejected because visual and geometry fixes would retain two acceptance paths.
Consequences.
- Flutter is required for screenshots, visual/layout WPT, rendered CDP, and GUI. Linux rendered automation requires Cage/headless Wayland rather than surfaceless EGL.
- Flutter SDK promotion must preserve Impeller scene, capture, recovery, and driver evidence; “a Flutter window opens” is not renderer proof.
- CSS layout remains a major Vixen subsystem, now concentrated on Flutter's cross-platform text/scene substrate.
- The FFI boundary carries render mutation/commit/query traffic and no frame buffers or native renderer dependencies. It is a primary content-controlled trust and performance boundary.
- Rendered headless startup may grow while total GUI/native complexity and platform-specific dependencies shrink. Measurements compare the chrome-less host, GUI, and removed WebRender/EGL costs honestly.
- Every platform still earns support through native BrowserCore/V8, renderer, input, accessibility, lifecycle, host-service, package, size, and performance evidence under ADR-019.
Migration gates. Execute in order:
- Define and test bounded
RenderRevision, mutation/full-snapshot, commit, presented, geometry, target, scroll, semantic-bound, and query DTOs invixen-api, including semantic-action targets bound to document, displayed commit, semantic node, and advertised action generation. - Carry them through the C ABI and handwritten Dart models with malformed, stale, truncation, release, and resync tests; production still uses the old frame during this protocol-only step.
- Render one controlled background/text/image document with a test-only Flutter formatter/Canvas/Paragraph path and atomically return geometry, hit/text queries, scroll state, and semantic bounds.
- Prove same-commit pixels, input targeting, find/caret ranges, scrolling, Semantics, and scene capture in the Linux shell.
- Add the chrome-less Flutter host under Cage and move visual/layout fixtures plus screenshot/CDP capture to it.
- Implement and race-test bounded synchronous
EnsureLayout, cancellation, resync, renderer loss, and same-task mutation-to-geometry behavior. - Cut production over; aggressively delete WebRender/EGL/RGBA/texture and superseded Rust layout/paint code, dependencies, tests, gates, and docs.
- Reproduce Linux compatibility, interaction, accessibility, size, memory, startup, and release gates, then continue the full browser roadmap and expand the same renderer contract to the other four targets.
Pinned reference-browser revisions
Every implementation decision that touches CSS, DOM, JS, layout, or paint semantics must cite a path in one of these trees, plus the pinned revision below. The reference trees are large; pinning prevents non-reproducible consultations ("latest main" drifts).
These pinned revisions are the canonical reference set for Vixen's implementation work. Update a pin deliberately with the corresponding toolchain/ architecture change so citations continue to name an exact tree state.
Pin table (reviewed 2026-07-14)
| Reference | Upstream | Pinned revision | Branch | Used for |
|---|---|---|---|---|
| Firefox | https://github.com/mozilla-firefox/firefox.git | 46e9f12a8f9b | main | CSS formatting/property semantics, DOM API behavior, JS/realm discipline, accessibility behavior, and WPT selection. Also hosts the servo/ Stylo subtree. |
| Servo Stylo (under Firefox tree) | vendored at firefox/servo/ @ 46e9f12a8f9b | (same as Firefox) | — | Primary CSS reference. Stylo (components/style/), selectors (components/selectors/), and supporting Servo crates. Current Firefox HEAD does not carry the old Servo script/layout crates. |
| Ladybird | https://github.com/LadybirdBrowser/ladybird.git | 0de15a5dd2a9 | master | CSS box-tree/formatting/fragmentation architecture reference; Vixen reimplements required semantics in Dart. |
| Flutter | https://github.com/flutter/flutter.git | 6655482ec06e | stable | Primary renderer and GUI reference. dart:ui Paragraph/Canvas/scene, Impeller, Semantics, platform channels, runners, capture, lifecycle, and tests. |
| GNOME Web (Epiphany) | https://gitlab.gnome.org/GNOME/epiphany.git | 21e02b9a272d | main | Linux browser AppStream/desktop metadata, portal behavior, and Flatpak manifest conventions. |
| Obscura | https://github.com/h4ckf0r0day/obscura.git | ca71ce3c2da9 | main | Headless CLI design, CDP server patterns, single-binary distribution. |
| Deno / deno_core | https://github.com/denoland/deno.git | 83c50b1da61e | main | Primary JS runtime packaging reference. deno_core embedding, extension/op boundaries, bootstrap JS packaging, resource tables, permissions, and test layout. |
How to consult each
Firefox / Servo Stylo subtree (firefox/ checkout)
The Firefox checkout is large. For Vixen, use a sparse checkout containing the Rust-facing pieces we can cite directly plus the Firefox C++ seams that show API contracts:
firefox/servo/components/style/ ← Stylo. Read this for CSS cascade/computed values.
firefox/servo/components/selectors/ ← selector engine used by Stylo.
firefox/dom/bindings/ ← WebIDL binding and wrapping discipline.
firefox/dom/webidl/ ← DOM API surface contracts.
firefox/dom/base/ ← DOM API behavior and selector delegation.
Current Firefox HEAD (46e9f12a8f9b) does not include
servo/components/layout_2020/, servo/components/layout/, or
servo/components/script/. Do not cite those removed historical paths.
Vixen's Flutter-hosted formatter may use current Firefox formatting behavior and
Ladybird's readable box-tree architecture as references; neither provides code
ownership or a second renderer.
When in doubt about a CSS computed value, search
firefox/servo/components/style/properties/ for the property name —
longhands, shorthands, and computed-value logic all live there.
Ladybird (ladybird/)
Use Ladybird when a question is architectural ("how do other engines seam X from Y?") rather than specification-level. Vixen's Dart formatter may follow its box/formatting-context decomposition, not its C++ ownership model.
ladybird/Libraries/LibWeb/ ← DOM, CSS, layout, paint (cleanly seamed)
ladybird/Libraries/LibWeb/CSS/ ← cascade + stylesheet model
ladybird/Libraries/LibWeb/Layout/TreeBuilder.cpp ← styled DOM → layout tree seam
ladybird/Libraries/LibWeb/Layout/ ← formatting contexts
ladybird/Libraries/LibWeb/Painting/ ← display-list construction
ladybird/Libraries/LibGfx/ ← rasteriser fallback
Flutter (flutter/)
Consult for dart:ui Paragraph/Canvas/Picture/Scene, Impeller, the rendering
pipeline, Semantics, platform-channel, native-runner, lifecycle, capture, and test
behavior. The SDK pin is a substrate reference, not evidence of Vixen CSS or
platform support.
flutter/packages/flutter/lib/ ← widgets, services, Semantics
flutter/engine/src/flutter/lib/ui/ ← dart:ui Canvas/Paragraph/scene implementation
flutter/engine/src/flutter/flow/ ← layer/scene composition
flutter/engine/src/flutter/impeller/ ← required graphics backend
flutter/packages/flutter_test/ ← widget/test harness patterns
flutter/packages/flutter_tools/templates/app/linux.tmpl/ ← Linux runner boundary
flutter/examples/ ← focused framework examples
GNOME Web (gnome-web/)
Consult only for Linux browser metadata, portal expectations, and Flatpak conventions. It is not a GUI architecture reference; Vixen renders its sole GUI with Flutter.
gnome-web/data/ ← gschema, metainfo, desktop
gnome-web/flatpak/ ← runtime/portal conventions useful for the FlatPark submission
Obscura (obscura/)
Consult for CDP/session and CLI ergonomics only. Rendered automation uses Vixen's chrome-less Flutter host and does not inherit another renderer or CLI verbatim.
Deno (deno/)
Consult Deno for JS runtime embedding and Rust host packaging, per ADR-014.
The target crate is deno_core. Use this
tree for extension/op organization, resource-table shape, permission checks near
host boundaries, bootstrap script packaging, and feature-family test layout. Do
not cite Deno for DOM/Web API semantics over Firefox/specs; Deno is the runtime
substrate reference, while Web-facing behavior remains WPT/spec-gated.
Import-map matching uses Deno's
import_map 0.25.0 resolver with
default logging disabled. Vixen owns parser ordering, bounds, diagnostics,
scheme/policy checks, and a bounded exact-URL integrity table because the crate
has no integrity model. Vixen also owns current-standard first-wins merging,
parser-position snapshots, and the bounded successful-resolution set because the
crate only parses and resolves individual maps.
deno/core/ ← op/extension/runtime core patterns
deno/runtime/ ← permissions, workers, bootstrap packaging
deno/ext/ ← feature-family JS/Rust extension layout
deno/cli/ ← integration tests and permission plumbing examples
Re-cloning fresh
If .tmp/ref/ is unavailable, clone each at the pinned revision:
mkdir -p .tmp/ref && cd .tmp/ref
git clone --depth 1 --filter=blob:none --sparse --branch main https://github.com/mozilla-firefox/firefox.git
git -C firefox sparse-checkout set servo gfx/wr gfx/layers/wr gfx/webrender_bindings dom/webidl dom/base dom/bindings js/public
git -C firefox checkout 46e9f12a8f9b
git clone --depth 1 --filter=blob:none --sparse --branch master https://github.com/LadybirdBrowser/ladybird.git
git -C ladybird sparse-checkout set Libraries/LibWeb Libraries/LibGfx
git -C ladybird checkout 0de15a5dd2a9
git clone --depth 1 --filter=blob:none --sparse --branch stable https://github.com/flutter/flutter.git
git -C flutter sparse-checkout set packages/flutter packages/flutter_test packages/flutter_tools/templates/app/linux.tmpl engine/src/flutter/lib/ui engine/src/flutter/flow engine/src/flutter/impeller examples
git -C flutter checkout 6655482ec06e547f90abf8ae7590466f4415978d
git clone --depth 1 --filter=blob:none --sparse --branch main https://gitlab.gnome.org/GNOME/epiphany.git gnome-web
git -C gnome-web sparse-checkout set data flatpak
git -C gnome-web checkout 21e02b9a272d
git clone --depth 1 --filter=blob:none --branch main https://github.com/h4ckf0r0day/obscura.git
git -C obscura checkout ca71ce3c2da9
git clone --depth 1 --filter=blob:none --sparse --branch main https://github.com/denoland/deno.git
git -C deno sparse-checkout set core runtime ext cli
git -C deno checkout 83c50b1da61e
Disk budget depends on sparse settings. Keep the checkouts in .tmp/ref/
or another ignored workspace; avoid committing reference trees.
Citation discipline
Vixen's tick-tock rules (each phase is a tick — capability lands; the post-phase cleanup is a tock — dead-code removal, ≤ 1 kLOC modules, reference citations):
- Every implementation commit cites at least one path + commit hash from a reference tree explaining why the behaviour is correct.
- Every tock (post-phase hardening) cites at least four reference paths.
- Commit hashes are the short form of the pin above (
46e9f12a8f,0de15a5dd2, etc.), neverHEADormain. - When a reference path goes stale, refresh the affected checkout to the current branch HEAD and update this file in the same change; do not leave implementation comments pointing at historical paths that no longer exist.
Vixen development mode
This document defines dev for this repo: how to move quickly during alpha without creating long-term maintenance debt.
Project focus is defined in PROJECT_DIRECTION.md.
Autonomous commit/push policy is defined in
AUTONOMOUS_WORK.md. Git lifecycle gates are enforced by
hk via the checked-in ../hk.pkl.
Definitions
- Dev / alpha means partial browser capability is allowed when it is executable, tested, fail-closed, and honestly documented. Alpha work may be incomplete; it must not be vague, hidden, or unbounded.
- A slice is the smallest reviewable unit that makes one browser-visible
seam better: usually one
Page/headless/CDP/WPT fixture path plus the pure engine code it consumes. - A tock is a cleanup-only follow-up after capability work: delete dead shims, split modules nearing 1 kLOC, move duplicated parsing to one helper, tighten docs, and retire stale fixtures.
- Release mode is stricter than dev mode and is governed by
ACCEPTANCE.md. Do not use this document to lower release gates.
Alpha development contract
Every alpha slice should satisfy these rules:
- Visible seam first. Prefer code that reaches the engine-owned browser/
context/document path,
vixen-headless, CDP, or a committed WPT/fixture check. APageslice must preserve BrowserCore ownership and name the live document seam it advances. Pure prep is fine only when the next visible seam is named. - One trust boundary at a time. For security-sensitive paths, name the boundary, validate near it, fail closed, and surface stable error codes.
- Reuse pure modules without duplicating ownership. JS host objects, Page projections, CLI, and CDP should call the same Rust implementation, but only the browser core decides lifecycle, commit, cancellation, and persistence.
- Partial APIs must be explicit. A subset may ship in alpha if unsupported
inputs fail closed and the supported behavior is documented in
COMPAT.md. Interface shape without a backing subsystem must be labeled shape-only. - No silent architecture drift. New dependencies, crate edges, rendering
paths, process boundaries, or storage/network policy changes must be backed by
an ADR/update in
DECISIONS.mdor an explicit plan note. - Tests travel with behavior. Unit tests prove pure logic; one integration check proves the user-visible seam. If a fixture manifest assertion is the seam, keep it committed.
- Flutter is the renderer, not the browser owner. Dart owns bounded formatting/Paragraph/Canvas state, atomic renderer commits, chrome, Semantics presentation, and host-service UI. BrowserCore owns DOM/runtime/navigation, computed styles, policy/persistence, web-event semantics, accepted resources, and accessibility meaning. Bridge payloads, queues, commits, queries, and handles are bounded with explicit revision/lifetime tests.
Gate tiers
Use the cheapest gate that matches the risk, then escalate before review or push.
| Tier | Use when | Command shape |
|---|---|---|
| Inner loop | Editing one crate/module | cargo check -p <crate> plus focused cargo test ... <name> |
| Pre-commit | A commit is being made | hk pre-commit: cargo fmt, merge-conflict/private-key scan, staged diff whitespace check |
| Alpha slice | A coherent partial capability is ready | focused tests + relevant just gate-phaseN |
| Pre-push | Work is ready to leave the machine | hk pre-push: just gate-push |
| Release | Versioned release readiness | every gate in ACCEPTANCE.md |
just gate-push is the long integration gate. Keep long gates out of the inner
loop and pre-commit path so iteration stays fast.
Current pre-push composition:
just gate-alpha
just gate-phase6
just gate-smoke
git diff --check
git diff --cached --check
Adjust just gate-push as the alpha architecture changes; hk should keep
calling that single recipe.
GUI shell environment blockers
Ubuntu 24.04 is the CI and release baseline; a Distrobox is not required. On a
host with the native Linux packages listed in
guidance/mise.md, run the shell recipes directly. Debian,
Fedora, and Ubuntu 26.04 runs are compatibility evidence only unless the Linux
baseline and CI move in the same reviewed change.
The Linux Flutter project and focused gate are checked in. Install the exact
official Flutter 3.47.1 stable archive declared in .mise.toml, then
run its gate. The pin supplies Dart 3.13, the standard GTK3 Linux embedder, and
required Linux Impeller support (Impeller is the default desktop renderer since
stable 3.47); do not replace it with another SDK or accept a
Skia-backed smoke without updating the renderer decision and evidence:
just setup-flutter
just gate-flutter-shell
just build-flutter-linux and just run-flutter additionally need CMake, Ninja,
pkg-config, and GTK3 development headers. Missing host packages are an
environment limitation; they do not turn Rust or Dart-only checks into Linux
bundle proof. The direct build and run recipes use release mode because the
official stable archive contains the reviewed Linux engine; the pinned GNOME
builder remains authoritative for distributable artifacts.
The Linux runner requires native Wayland. just run-flutter-cage additionally
uses Cage with wlroots' headless backend for isolated local Wayland testing;
X11 and XWayland are intentionally unsupported.
The released Linux shell is Flutter. Local and CI release builds use the same
plain Docker workflow and digest-pinned GNOME builder image. CI runs host-only
Wayland, IME, AT-SPI, and archive smoke checks on Ubuntu 24.04.
Flutter is the sole rendered frontend target; the Rust workspace has no GTK/
libadwaita/Relm4 feature or fallback GUI. just check and just clippy cover
every Rust target and feature without GNOME development packages. Verify Linux
Flutter release changes with:
just docker-builder-pull
just linux-release-prefetch
just linux-release-smoke
just linux-at-spi-smoke
just linux-interaction-smoke
Native GTK3 development packages are needed only for direct host Flutter Linux
builds; the pinned release container supplies them for official archive work.
scripts/docker-flutter.sh uses only docker pull, docker image inspect, and
docker run: prefetch is network-capable, release/size builds use
--network=none, the source mount is read-only, and no host Flutter/Rust
toolchain is mounted.
GitHub Releases publish the deterministic x86_64 archive built with the SHA-256-pinned official Flutter stable SDK, locked application/Cargo dependencies, and pinned rusty_v8 input. FlatPark repackages those bytes as a signed convenience Flatpak. The release validator requires GTK3 linkage and rejects debug/JIT artifacts; direct GTK code remains limited to Flutter's native runner boundary.
The safe Rust controller and handwritten C ABI can be developed without Flutter installed:
just test-flutter-controller
just gate-native-abi
just gate-architecture
These gates prove the bounded JSON/renderer wire, registry, worker, commit-bound
input, native smoke, and architecture rules. just test-api proves the renderer
protocol model; just test-flutter-formatter-impeller proves formatter/scene
behavior. just test-r7 adds the final deletion scan and native/Flutter suites;
just gate-r7 composes all rendered transition evidence.
Larger alpha batches
Larger batches are encouraged when they reduce handoff overhead and stay coherent. A batch is coherent if it has:
- one feature family or one host-object family,
- one primary visible seam,
- one docs/compat story,
- one verification story.
Stop and split when the next addition would introduce a second trust boundary, a second unrelated feature family, or a second independent rollback concern.
Maintainability budget
Alpha speed is acceptable only while these budgets stay visible:
- Non-test modules should stay below 1,000 lines. If a module crosses that while moving fast, create the split in the next tock before widening the feature.
- Prefer boring data flow over framework gravity: DTOs in
vixen-api, lifecycle and pipeline state in the engine-owned browser/context/document graph, and browser-facing adapters in headless/CDP/shell. - Keep Dart DTOs and native bridge code mechanical. Renderer box/fragment/scene state is ephemeral and commit-bound; do not mirror profile, navigation, DOM, permissions, policy, script state, or accessibility meaning in Flutter.
- Avoid duplicate parsers/matchers. Runtime host objects and BrowserCore/Page operations must extract or call the same Rust implementation.
- Do not reintroduce string-expression shims. Retire transitional runtime/ document snapshots as live resources replace them.
- Keep
COMPAT.mdhonest: partial support is fine, overclaiming is not.
Alpha definition of done
A dev/alpha slice is done when:
- the supported subset is named,
- unsupported inputs fail closed,
- docs mention the current state and next widening step,
- focused tests and the relevant gate pass,
- hk pre-commit/pre-push gates are clean before commit/push,
- any known debt is either removed immediately or named as the next tock.
Autonomous work contract
This document exists so agents and maintainers can make progress without re-asking project-direction questions.
Decision policy
Until alpha, continue without asking unless a change would alter architecture. Architecture changes include:
- a new JS runtime target or abstraction,
- a rendered frontend other than ADR-022's Flutter formatter/shell/automation host,
- a second render/paint path,
- a bridge that moves navigation, DOM/runtime, policy, persistence, web-event semantics, or accessibility meaning out of BrowserCore,
- a layout architecture outside ADR-022's mutation/commit ownership,
- a core dependency that changes binary-size or subsystem ownership materially,
- a security-policy change that makes behavior less fail-closed.
For ordinary implementation details, choose the safest path aligned with
PROJECT_DIRECTION.md, document assumptions briefly, and keep moving.
Commit and push policy
- Automatic commits are allowed when the batch is coherent and gates pass.
- Automatic pushes are allowed when hk pre-push gates pass.
- Prefer milestone commits over tiny churn commits.
- Do not bypass hk. If hk fails, fix the issue or report the blocker.
Gate policy
Linux agents and maintainers run on a host with the native packages documented
in docs/guidance/mise.md; a Distrobox is not required. Before reporting a
local Linux gate, record PRETTY_NAME from /etc/os-release and ensure Mise
was activated in the shell that ran it. Debian, Fedora, and Ubuntu 26.04 are
optional compatibility runs; release-builder recipes retain their separately
pinned environment.
- Inner loop: focused
cargo check/cargo test/just gate-phaseNas needed. - Before commit: hk pre-commit hook; it should stay quick and fix formatting.
- Before push: hk pre-push hook; long gates run here because iteration speed matters.
- Release:
ACCEPTANCE.mdgates plus measured size/compatibility reports.
The checked-in Linux Flutter slice uses an exact ignored SDK checkout and
just gate-flutter-shell; bootstrap it with just setup-flutter. Do not report
Rust/GTK checks as Flutter proof, or Dart/widget checks as Linux package proof.
Platform work follows ROADMAP.md and FLUTTER_SHELL.md: ADR-022 R1–R8 are
complete. Post-R7 compatibility/release/frame/GPU checkpoints and the real Linux
Mozc corridor are recorded. The official stable SDK migration preserves the
process-filtered AT-SPI name gate and native-pointer focus on GTK3; broader
role/state/bounds/action claims require fresh migrated evidence. Keep the
executable gates intact while shared-core reductions proceed one proven host
family at a time before broader shell/platform expansion. A2 static same-origin/file module
dependency graphs now use the shared resource loader; continue that family with
CORS enforced before V8 exposure and eligible HTTP(S) cache entries
conditionally revalidated under current policy. One bounded pre-module inline
import map now resolves through the same policy-bound loader. Page-module
dynamic imports retain bounded graph provenance and cancellation across later
roots/tasks. Parser classics and BrowserCore automation now also register exact
source/document policy before dynamic import, including accepted redirect URLs
and retained import maps. A shared bounded private-cache decision now gives page fetch/XHR and
module loads Date/Age plus max-age/Expires freshness, effective request
directives, simultaneous exact Vary variants, and bounded permanent
same-origin redirect aliases. The transport now performs bounded incremental
body reads and publishes progress/completion through ReadableStream, XHR,
BrowserCore/C ABI, and CDP. Page fetch/XHR now use a bounded asynchronous realm owner; active
AbortSignal, XHR abort, stop/deadline, and realm teardown drop transport without
late profile effects. Ordinary policy-accepted response heads now resolve before
body completion and feed an eight-message backpressured stream; abort after
resolution rejects body reads with the same JS reason, while integrity, 304
revalidation, and opaque responses remain buffered. Exact static/dynamic JSON
import attributes now use that source, policy, profile, limit, and cancellation
provenance plus strict JSON response typing; unsupported keys and types are
rejected before transport. External classic/module root SRI now verifies raw
bytes before V8, cookies, or cache insertion. Bounded import-map integrity now
applies exact normalized-URL metadata to root fallback and graph dependencies at
the same boundary. Up to 64 inline maps now merge first-wins under a 512 KiB
normalized-state cap and a 2,048-entry/1 MiB successful-resolution set; static
parser-position snapshots remain stable while later dynamic imports see the
latest map. Continue A2 by carrying authored script/module referrer policy and
fetch priority through roots, redirects, dependencies, and diagnostics.
The project owns hook definitions in hk.pkl. just owns command recipes; hk
owns when those recipes run in the git lifecycle.
Reporting format
Final handoff should be terse and evidence-first:
- objective completed,
- changed files,
- checks run and pass/fail status,
- commit hash and push status when applicable,
- remaining known gaps or next slice.
For large compatibility work, update COMPAT.md from actual fixture/WPT output
rather than prose guesses.
Documentation rule
Prefer ADR-style docs that explain why and point to code for how. Avoid parallel prose that must be maintained beside source unless it records product direction, architecture constraints, compatibility results, or gate policy.
CDP / Playwright smoke
Run the rendered product smoke with:
mise install
just flutter-cdp-playwright-smoke
Cage launches the release Flutter executable in chrome-less CDP mode. That host
owns the sole BrowserCore, one vixen-cdp subscriber, and the same formatter and
commit painter used by the GUI. There is no native rendered Playwright smoke.
The smoke proves:
playwright-coreconnects throughchromium.connectOverCDP(...).- Target/page/runtime/network/DOM enable and navigation methods route to the selected BrowserCore context.
- Playwright obtains layout from an exact Flutter commit.
- One stable live
DOMStringMapwrite reflects todata-layout-mode, advances the normal mutation/cascade path, returns 140×32 synchronous page geometry, then matches CDP DOM attributes/geometry and a pinned distinct Flutter PNG. - One retained live
classListsurvives the pointer-drivenclassmutation, reflectsclicked, agrees with 140px synchronous/CDP geometry, and produces a second pinned exact Flutter PNG. - One retained live
relListsurvives external and list-drivenrelwrites on a real anchor, reflects ordered tokens, agrees with 120×32 synchronous/CDP geometry, and produces a third pinned exact Flutter PNG without changing the earlier hashes. - One retained iframe
sandboxlist survives external and list-driven writes, reflects valid ordered tokens, agrees with 120×32 synchronous/CDP geometry, and produces a fourth pinned exact Flutter PNG without changing earlier hashes. - One retained inline
CSSStyleDeclarationsurvives external and declaration API writes, reflects current serialized declarations, agrees with 120×32 synchronous/CDP geometry, and produces a fifth pinned exact Flutter PNG. - One retained
NamedNodeMapand attachedAttrsurvive external andAttr.valuewrites, preserve indexed/named identity, agree with 120×32 synchronous/CDP state, and produce a sixth pinned exact Flutter PNG. - Retained structural collections start empty, reflect the pointer-created
rendered
#dynamic.badgethrough indexed/named access, and agree with an authoritative CDP node while a preexisting selector-all list remains static. - One retained author
StyleSheetList/sheet/rule/declaration graph survives every earlier mutation, reflects a style-element rewrite, agrees with 120×32 synchronous/CDP state, and produces a seventh pinned exact Flutter PNG. - A detached Attr replaces, detaches from, and reattaches to one retained
NamedNodeMap; in-use rejection, 120×32 synchronous/CDP state, stable identity, and an eighth pinned exact Flutter PNG agree. - Parser classics, per-script microtasks, a deferred top-level-await module, one real static dependency, one real dynamic dependency, and a post-load document task execute in pinned order; the module-owned 120×32 target agrees with CDP and a ninth pinned exact Flutter PNG.
- Pointer input uses the displayed commit's Flutter hit-test handle and target; the C ABI has no raw coordinate command.
- Later DOM/style mutation produces a new source revision and distinct exact scene.
Page.captureScreenshotand high-level screenshot return direct Flutter scene PNGs without browser/compositor chrome.- Simultaneous 320×240 and 480×300 targets keep source, viewport, input, and scene state independent.
- Switching targets does not lose presentation state.
- Forced renderer reset requests a full snapshot and recovers a byte-identical scene.
- Runtime, network, permissions, tracing, history, dialog, form/text input, and stable protocol-error slices remain available through the shared CDP core.
Native vixen-headless --cdp is text/runtime-only. Screenshot, layout geometry,
and pointer hit testing fail closed there. Rust CDP tests cover dispatcher,
session, lifecycle, cancellation, network, profile, and runtime behavior without
inventing renderer output.
Add methods only when this smoke or the Flutter fixture manifest demonstrates a real product gap.
Vixen guidance
How-to guides for specific workflows. The spec/architecture/plan docs say what and why; these guides say how, step by step.
| Guide | When to read it |
|---|---|
mise.md | Activating the project-managed toolchain and using just recipes correctly. Start here for local setup. |
cargo-home.md | Why CARGO_HOME points at <workspace>/.cargo and how recipe-installed Cargo tools stay local. |
deno-core.md | deno_core/V8 embedding, host-extension shape, and cache/pinning notes. |
flatpark-release.md | Building the official Linux release archive and publishing it through FlatPark. |
(Add new guides here as standalone files. Keep each guide focused on one workflow, with copy-pasteable commands that have been verified to run.)
mise + just workflow
Vixen uses two tools with separate jobs:
misepins tool versions and exports the project environment from.mise.toml(RUSTUP_TOOLCHAIN,CARGO_HOME,HK_MISE, andPATH).justowns repository actions. Add or update ajustfilerecipe instead of copyingcargo ...command lines into docs, scripts, or CI.hkowns git lifecycle hooks. The project config ishk.pkl; install hooks withjust hooks-installor through the mise postinstall hook.
The intended workflow is an activated shell where cargo, rustfmt, clippy,
rustup, cargo-binstall, hk, and just come from the versions pinned in
.mise.toml.
Linux environment
Ubuntu 24.04 is the CI and release baseline; a Distrobox is not required. Install the native Linux build and host-smoke dependencies once on the host:
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
at-spi2-core binutils build-essential cage clang cmake curl dbus-daemon git \
gir1.2-atspi-2.0 ibus ibus-gtk3 ibus-mozc libegl-dev libgl-dev \
libgtk-3-dev libsecret-1-dev libwayland-dev mesa-vulkan-drivers ninja-build \
pkg-config python3 python3-gi ripgrep wayland-protocols wtype
An x86_64 Ubuntu 24.04 Distrobox that replicates CI exactly remains an optional convenience:
distrobox create \
--name flutter-dev \
--image quay.io/toolbx/ubuntu-toolbox:24.04
distrobox enter flutter-dev
Do not replace the versioned image with latest; Debian, Ubuntu 26.04, and
Fedora runs are compatibility checks rather than the CI baseline. Distrobox
shares the host home by default, so an existing Mise installation and this
checkout remain available. Do not add project activation blindly to the shared
shell startup files.
GPU-less hosts (no /dev/dri) run the headless Wayland smokes with the
wlroots software compositor instead of the default GLES2 renderer:
WLR_RENDERER=pixman just linux-release-smoke
First setup
mise trust
mise bootstrap --yes
mise bootstrap installs pinned tools, then runs just setup, which installs
the optional Cargo tools used by just audit / just fuzz-security, installs a
nightly Rust toolchain for cargo-fuzz, and finishes with just check-all-host.
mise's postinstall hook also runs hk install --mise so git hooks execute in
the project tool environment.
For tools-only CI images, mise install is enough; run project checks through
just after activating the shell.
Daily shell setup
Activate mise once per shell, then run recipes directly:
eval "$(mise activate bash)" # bash
# eval "$(mise activate zsh)" # zsh
# mise activate fish | source # fish
just check
just test
just smoke
just hooks-install
Do not hard-code paths to Cargo, and do not wrap every build command in
mise exec. If cargo is missing, the shell is not activated or mise install
has not completed.
Common recipes
| Recipe | What it does |
|---|---|
just setup | Optional dev tools + nightly for fuzzing + check-all-host |
just hooks-install | Install/update hk hooks through hk install --mise |
just check / just check-all-host | Type-check the host-runnable workspace |
just test / just test-host | Run host-runnable tests |
just smoke / just gate-smoke | Formatting check, clippy, check, tests |
just gate-push | Long hk pre-push gate |
just audit | cargo audit and cargo deny check |
just fuzz-security | Phase 1 fuzz targets at 1 M iterations |
just linux-release-prefetch / just linux-release-smoke | Build and verify the official Linux archive consumed by FlatPark |
Use just --list for the full recipe list.
One-shot commands
For automation that cannot keep an activated shell, prefer a single activated
subshell and still call just recipes:
bash -lc 'eval "$(mise activate bash)" && just smoke'
Avoid tool-specific invocations like mise exec rust@... -- cargo ...; Rust is
special in mise because the Rust backend delegates to rustup. In an activated
shell, mise sets RUSTUP_TOOLCHAIN and exposes Cargo through the workspace
CARGO_HOME (.cargo/bin).
Verifying the active toolchain
eval "$(mise activate bash)"
mise ls --current
command -v cargo
cargo --version
command -v just
just --version
command -v hk
hk --version
printenv CARGO_HOME
Expected properties:
cargoresolves under<workspace>/.cargo/bin.justresolves under mise's install directory.hkresolves under mise's install directory.cargo --versionmatches the Rust version pinned in.mise.toml.CARGO_HOMEis<workspace>/.cargo.
Updating versions
Update shared tool versions with mise use so .mise.toml remains the source
of truth:
mise use rust@<version>
mise use just@<version>
mise use hk@<version>
mise use cargo-binstall@<version>
Then verify in a freshly activated shell and run just smoke before committing
the version change.
Cargo home lives in the workspace
Vixen points CARGO_HOME at <workspace>/.cargo instead of the default
~/.cargo. Everything Cargo would normally write to the user's home — the
registry index, downloaded crate sources, git checkouts, cargo-binstall-ed
tooling — stays inside the workspace tree.
Why
- Workspace is the unit of trust. The registry cache, git checkouts,
and installed binaries are all inputs to the build; keeping them under
<workspace>/.cargomakes that explicit and lets the reviewer audit them alongside the source. - Reproducibility. A fresh contributor gets the same view of the dep
tree as CI; nothing depends on whatever happens to live in their
~/.cargofrom other projects. - No cross-project leakage. Vixen's
cargo-binstallpackages don't shadow globally-installed copies on the user's machine, and vice versa.
How it's wired
.mise.toml[env]exports:CARGO_HOME = "{{ config_root }}/.cargo"_.path = ["{{ config_root }}/.cargo/bin"](mise's PATH-prepend directive, so Cargo itself pluscargo-audit,cargo-deny, andcargo-fuzzinstalled bymise bootstrap/just setup-dev-toolsare runnable from an activated shell)
.gitignoreignores everything under.cargo/exceptconfig.toml, which is the project-pinned Cargo config and ships with the repo..cargo/config.tomlis checked in. It doubles as the CARGO_HOME config (Cargo reads the same physical file in both roles) — keep it limited to project-pinned settings, never cache state.
mise exports these vars to any mise-active shell. Use the workflow in
mise.md: activate once per shell, then run cargo / just
directly. Do not hard-code paths to Cargo or wrap every command in mise exec.
Verifying it took effect
mise trust
eval "$(mise activate bash)"
echo "$CARGO_HOME" # → /path/to/vixen/.cargo
command -v cargo # → /path/to/vixen/.cargo/bin/cargo
ls .cargo/ # → config.toml, plus registry/, bin/, ... after first build
cargo itself reports the resolved home:
cargo config get # honors CARGO_HOME from the env
Disk
The cache for Vixen's dep tree (Stylo + JS runtime artifacts + reqwest + …) is several
hundred MiB. It's all under .cargo/ and git-ignored, so it costs nothing
in the repo; treat it like target/. rm -rf .cargo is safe and Cargo
will repopulate on the next build.
Updating tooling installed via cargo-binstall
mise bootstrap delegates to just setup-dev-tools, which uses
cargo-binstall for cargo-audit, cargo-deny, and cargo-fuzz (falling
back to cargo install where possible). Because CARGO_HOME is
workspace-local, those binaries land in .cargo/bin/. To install or re-check
them:
eval "$(mise activate bash)"
just setup-dev-tools
or rerun mise bootstrap --yes.
Caveats
- Editor integration. rust-analyzer and IDEs spawn
cargothemselves; make sure they inherit the mise env (direnvintegration, or launch the editor from a mise-active shell). If they don't, they'll fall back to~/.cargoand re-download the registry there. Harmless, just slow. - Other projects on the host. They keep using
~/.cargoas before; Vixen's CARGO_HOME only applies inside a mise-active shell in this workspace.
deno_core runtime target
ADR-014 makes deno_core the target JS
runtime substrate for Vixen. The Phase 2 eval gate and focused Phase 6 host smoke
checks now run behind deno_core.
Migration shape
Keep the public engine seam stable:
vixen_engine::script::JsRuntime::new()JsRuntime::evaluate(...)JsRuntime::evaluate_with_page(...)JsValuevixen-headless --eval- CDP
Runtime.evaluate
The implementation underneath is a deno_core::JsRuntime; host slices should
keep moving bootstrap-only surfaces into explicit feature-family extensions. Do
not add a generic JS-engine
trait around deno_core; use deno_core APIs directly inside
vixen-engine::script. The abstraction boundary is the Vixen-facing API above,
not portability to another JS engine.
Extension layout target
Each host family should be small and explicit:
crates/vixen-engine/src/script/
runtime.rs # deno_core runtime construction + eval bridge
webidl.rs # generated interface/prototype substrate
encoding.rs # TextEncoder/TextDecoder ops + bootstrap JS
dom.rs # document/Element snapshot extension + bootstrap JS
cssom.rs # getComputedStyle/CSS.supports/styleSheets ops + bootstrap JS
url.rs # URL/URLSearchParams extension
fetch.rs # Headers/Request/Response/Blob/File extension
Current state:
runtime.rsownsdeno_core::JsRuntimeconstruction and V8 value conversion.webidl.rsrenders the first generated binding substrate from a Rust-owned WebIDL-shaped manifest. It installs browser interface constructors/prototypes plus__vixenWebidl.adoptInterface(...), so feature-family bootstraps attach concrete Vixen implementations to generated prototype chains instead of hand-rolling every constructor shape.encoding.rsregisters the first op-backed host extension; JS constructors delegate UTF-8 encode/decode work tovixen-engine::text_codecthrough ops.dom.rsregisters the compatibility projection while host families move to live resources. Page data crosses thedeno_coreop boundary throughop_vixen_dom_snapshot; element data is loaded throughop_vixen_dom_element_snapshot; selector lookup,Element.matches(), element text/attribute reads, and read-only token surfaces delegate through focused DOM ops.HTMLElement.datasetis the first host-family convergence: one stable Proxy-backedDOMStringMapreflects live attributes and routes assignment/deletion through the shared Rust name conversion and normal Page mutation path.Element.classListis the second convergence slice: its stableDOMTokenListidentity reads the currentclassattribute and writes through the same Page mutation path instead of being discarded after each mutation.HTMLAnchorElement.relListandHTMLIFrameElement.sandboxnow follow the same retained live-object path, completing the token-list attributes currently hosted by the runtime.HTMLElement.stylealso retains one live inlineCSSStyleDeclarationacross external and declaration-API writes.Element.attributesretains a liveNamedNodeMapand stable attachedAttrnodes whose values read and write the same Page state. Detached Attr values and create/replace/remove/reattach operations preserve identity and join the same mutation path. Structural Node/Element/document/form/select/table collections use retained resolver-backedNodeList/HTMLCollectionobjects; selector-all results intentionally remain static. Element geometry reads (getBoundingClientRect()/getClientRects()/getBoxQuads()), Range rectangles, and client/offset/scroll metrics now cross a DOM rect op and materialize Web-shaped rect/list/quad objects on generated WebIDL prototypes.cssom.rsregisters the focused read-only CSSOM extension.CSS.supports,getComputedStyle, and retained livedocument.styleSheets/CSSRule objects cross explicit CSSOM ops and attach to generated CSSOM prototypes instead of being synthesized by the headless/Page string projection. The resource refreshes after ordinary mutation drains and same-task synchronous-layout flushes.just gate-webidlis the focused regression gate for this layer: generated interface/prototype coverage,JsRuntimeeval, headless--eval, and CDPRuntime.evaluatemust stay green together.- Parser classics and modules share the persistent document realm. Modules use
V8's native module evaluator, defer after parser classics, and checkpoint
microtasks before the next script. BrowserCore pumps a bounded document task
queue after load and automation turns. A2's first loader checkpoint lets V8
discover same-origin/file static dependency graphs while BrowserCore's shared
resource boundary owns request ids, redirects, CSP/mixed-content and response
policy, profile cookie/cache writes, diagnostics, limits, and cancellation.
The second checkpoint applies CORS to cross-origin HTTP(S) roots,
dependencies, and redirects before V8 exposure, omits default cross-origin
credentials, and inherits explicitly credentialed root policy through the
graph. The third checkpoint conditionally revalidates eligible exact-URL
profile entries for roots and dependencies through live requests, restores
bounded raw source only after a matching 304, and reruns current
CORS/status/strict-MIME policy before V8 exposure. Cache-disabled contexts
bypass reads and writes. Freshness reuse and full
Varyremain fail closed. The fourth checkpoint uses the Deno-maintainedimport_mapcrate insidePageModuleLoaderfor one bounded inline map registered before module discovery. Exact, prefix, URL-like, null-blocking, and scoped mappings plusimport.meta.resolve()feed the existing resource/policy path; modulesrcis not remapped. External/multiple/late/integrity maps remain fail closed. The fifth checkpoint retains a bounded graph context for every specified/final module URL so static and dynamic descendants inherit the originating root's import map, CSP, credentials, request ids, cache/profile path, and cancellation even after a different root executes. Module-owned delayed functions/tasks pump dynamic work to bounded event-loop quiescence; interruption aborts tracked transport, generation-rejects profile effects, and rebuilds the page realm after leaving the entered isolate. Direct classic/automation dynamic imports now register equivalent source/document policy provenance when the document has an exact absolute base URL; source-only relative fixture realms retain ordinary eval but no dynamic-import capability. Exact static/dynamic JSON import attributes select strict JSON response policy; a V8 validation callback rejects unsupported keys and types before module resolution or transport. External classic/module root integrity is verified over raw accepted bytes before source conversion, V8 evaluation, profile cookies, or cache insertion. A bounded Vixen-owned exact-URL table adds import-map integrity for static and dynamic dependencies plus top-level fallback through the same boundary; it is deliberately separate becauseimport_map0.25.0 has no integrity model. Vixen also merges up to 64 maps first-wins under cumulative normalized-state and successful-resolution bounds. Static roots retain their parser-position snapshot; dynamic imports and automation use the latest merged map without replacing the graph's source/security/profile provenance.
Rules:
- Rust validates near the op boundary and returns stable
EngineErrorcodes. - JS bootstrap exposes Web-shaped objects but delegates behavior to Rust ops or shared pure modules.
- Long-lived host state uses
deno_coreresources/handles, not ad-hoc globals. - Permissions and origin policy checks stay near the operation that crosses the trust boundary.
DOM maintenance comparison
Staying on deno_core is still the lower-maintenance path for a stable browser
DOM surface. Deno's runtime is built from the same extension/op/WebIDL pattern
Vixen already uses, and Deno publishes separable extension crates for many
state-light Web APIs (deno_web, deno_fetch, deno_webstorage, etc.). Those
crates can be evaluated family-by-family to reduce Vixen-owned code for value
objects, streams, fetch plumbing, storage scaffolding, and WebIDL conversions.
Neither Deno nor Bun gives Vixen a drop-in browser document tree: Vixen still
owns Document/Node/Element/HTMLElement, selector integration, mutation
commit, layout-backed geometry, CSSOM, navigation, and origin/security policy
because those APIs must talk to Vixen's Page, Stylo/layout, vixen-net, and
vixen-store state. The best code-reduction strategy is therefore to reuse
Deno-style non-DOM host families where they fit while keeping the DOM tree
Vixen-owned.
Bun/JSC does not lower that maintenance burden today. Bun has substantial
WebCore-flavoured implementations for runtime APIs such as Blob, Request,
Response, streams, encoding, and fetch, but its Rust crates are internal to the
Bun executable: they depend on generated code, Bun-specific globals/event-loop
state, C++ WebKit/JSC shims, and Node/Bun compatibility layers. Adopting them
would replace Vixen's current op modules with a larger forked embedding surface
without providing a maintained browser DOM tree for Vixen's Page model.
Cache and size notes
Expect V8/rusty_v8 artifacts to dominate JS runtime packaging. Keep Cargo and
runtime caches inside the workspace via the existing CARGO_HOME guidance, then
remeasure just size-headless and just size-flutter-linux before release.
Linux release archive and FlatPark packaging
Vixen publishes one official x86_64 Linux archive on GitHub Releases. FlatPark repackages those unchanged, checksum-pinned bytes as a signed Flatpak. Vixen no longer builds or hosts its own OSTree repository.
Priority gate: this document is a deferred release runbook. Keep the official archive reproducible, but do not submit, review, or publish through FlatPark until the Linux Flutter shell passes the basic-browser gate defined in
../ROADMAP.md(visible navigation/rendering, scrolling, text/IME, core navigation controls, find/zoom, and bounded recovery).
This deliberately separates two responsibilities:
- Vixen CI builds and tests
vixen-linux-x86_64.tar.gzfrom the tagged source revision. - FlatPark pins that public release asset by URL, size, and SHA-256, adds the minimal wrapper/metadata/permissions, and signs and hosts the Flatpak.
FlatPark is an independent community repository, not Flathub. Its runtime dependencies are supplied through Flathub.
Pinned build inputs
The release archive uses:
- official Flutter stable
3.47.1from the checksum-pinned Linux x64 archive declared in.mise.toml; - Flutter revision
6655482ec06e547f90abf8ae7590466f4415978d; - engine revision
5d531788691ec3404cac0cee66ead4007b177363and engine content hash11d79658c444477b06513d32b52c8c4ccb7276b0; - SDK archive SHA-256
a1d8166c0309267cb7dc99f1424eecf08b86946ad3b50723c6f59945964aea45; - GNOME 50 Linux builder image digest
sha256:a2b78890f165cd5b5c6a8629c5f6cb293e64d1bf523ca6662fac8ca8e247f8b0; - Rust
1.96.1; Cargo.lockandflutter/vixen_shell/pubspec.lock; and- rusty_v8
v149.4.0archive SHA-256aa30f198b6e7be2188df6498f95053c4c052f212037a01f2c31414d7aca84b53.
The Linux runner explicitly enables Impeller and links
libflutter_linux_gtk.so; archive validation requires GTK3 linkage and rejects
debug/JIT artifacts. A version string alone is not runtime evidence.
Local build and smoke
On a host with the native packages from mise.md (or inside the
optional Ubuntu 24.04 Distrobox), install the Mise tools, ensure Docker is
reachable, pull the GNOME builder image, stage locked inputs, then build the
exact release archive:
mise install
just docker-builder-pull
just linux-release-prefetch
just linux-release-smoke
The host needs Docker plus Cage with wlroots' headless backend; the packaged
Linux GUI supports native Wayland only. The script pulls/inspects/runs the
existing image and does not build a custom image. Prefetch is network-capable;
the release build runs with --network=none from workspace-local caches.
linux-release-smoke:
- builds Flutter in release/AOT mode;
- builds the BrowserCore-backed
libvixen_ffi.sothrough the Flutter runner; - verifies GTK3 linkage and the expected release bundle structure;
- creates a deterministic archive with normalized ownership and timestamps;
- extracts that exact archive into a clean directory;
- launches it under Cage's headless Wayland backend on the Linux host;
- requires survival to the bounded timeout; and
- requires
Using the Impeller rendering backend (...)in the engine log.
Generated assets are:
.tmp/release/vixen-linux-x86_64.tar.gz
.tmp/release/vixen-linux-x86_64.tar.gz.sha256
The archive has one top-level vixen/ directory containing the Flutter runner,
AOT app, Flutter engine, ICU/assets, and libvixen_ffi.so. It excludes source,
build tools, caches, JIT snapshots, and debug payloads.
CI and tagged releases
The Linux release archive CI job invokes the same digest-pinned, offline
Docker build recipe as local development. Ubuntu 24.04 owns only the host smoke
environment. CI creates the archive twice and byte-compares the outputs,
extracts and launch-smokes the exact archive, and uploads it as a workflow
artifact. On a tag, the release job attaches the archive and checksum to the
GitHub Release.
A release is not ready merely because the archive exists. The framework and engine revisions, native layout, deterministic archive, bounded launch, and Impeller log must all pass.
FlatPark submission
This section is intentionally inactive while the basic-browser gate is open.
The FlatPark registry entry uses extra-data and an immutable GitHub Release
asset URL. Its update resolver reads Vixen's latest GitHub Release and selects
vixen-linux-x86_64.tar.gz; FlatPark computes and reviews the new size and
SHA-256 before publishing.
The package may install wrapper, desktop, icon, and AppStream files around the archive, but it must not patch or replace Vixen's binaries. Permissions stay at the minimum needed for browser operation: the Wayland socket (without X11 or fallback-X11), GPU, IPC, network, and the explicit download directory grant. Optional broader host access is not enabled.
Before submitting an update:
- install and launch the release archive locally;
- cut and verify the GitHub Release;
- update the FlatPark registry entry to that immutable asset;
- run FlatPark's descriptor validator and
publish.sh --verify; - install from the isolated test repository and exercise startup/navigation;
- record tested and untested behavior in the pull request.
FlatPark's publishing and review requirements are authoritative: https://flatpark.org/contributing/.
Evidence boundary
FlatPark simplifies packaging and signed repository maintenance; it does not build Vixen or prove browser correctness. GitHub Releases remain the authoritative upstream bytes. Platform parity, portals, complete accessibility/native AT, IME, performance, and accepted size baselines remain separately gated.