Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Vixen

CI Pages Docs License Rust GUI target

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.

Vixen browsing Example Domain on Linux

Start here

Repository

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:

  1. smaller runtime/binary footprint,
  2. lower memory use,
  3. faster local builds,
  4. fewer moving parts,
  5. 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:

  1. 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.
  2. DOM/WebIDL/Web API runtime — modern pages need correct host APIs over deno_core/V8.
  3. Network/security/fetch/cookies — real browsing needs safe, fail-closed loading before breadth.
  4. Storage/history/session — required for real browsing and app-like sites.
  5. 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.
  6. Flutter-hosted headless + CDP/Playwright-compatible seams — rendered automation and text reports are product features, not test-only scaffolding.
  7. WPT/imported fixture coverage and reports — correctness driver for every item above. Treat it as cross-cutting, not optional polish.
  8. HTML parsing/serialization — essential but mostly delegated to html5ever; Vixen must preserve tree shape and integration semantics.
  9. CLI ergonomics — keep commands stable, scriptable, and useful.
  10. 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:

  1. 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.
  2. A component seam is not lifecycle integration. Sharing Page, Network, or JsRuntime types is insufficient if frontends decide independently when to create, commit, cancel, persist, or destroy them. Those decisions remain in the production engine-owned lifecycle.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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.
  10. 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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/just gates, 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-api for 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 BrowserCore owner 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

  1. 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.
  2. 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.
  3. Mutations are not a second DOM. Dart receives bounded immutable RenderMutationBatch data with stable ids and exact compound revisions. It cannot mutate navigation, DOM, policy, or durable state.
  4. Commits are atomic. One RenderCommit pairs 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.
  5. 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.
  6. Synchronous layout is explicit. Same-task mutation followed by geometry uses deadlock-safe, cancellable, deadline-bounded EnsureLayout; stale approximations cannot become the permanent behavior.
  7. 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.
  8. 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.
  9. 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.
  10. Generations reject late work. Navigation, runtime, resource, renderer, query, scroll, input, and semantic results cannot affect a replacement document or commit.
  11. 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:ui Paragraph/Canvas/scene primitives.
  12. 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 RenderRevision with context, document, source/style, viewport, and resource generations;
  • incremental RenderMutationBatch, exact base_revision, bounded full snapshot, and resync request;
  • stable render node/resource/fragment/commit ids;
  • atomic RenderCommit and separate Presented acknowledgement;
  • 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 EnsureLayout and 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 DOM scroll effect;
  • 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 click on 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. mousedown no longer publishes a replacement source before its matching mouseup, 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-contains and 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.md only 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 enfren 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:

  1. macOS and Windows: same render mutation/commit broker, native Flutter runner, fonts, input/IME, accessibility, host services, signing/packaging, capture, size, and performance evidence.
  2. 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.
  3. 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.
  4. 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:

  1. Accessible browser: complete semantics, screen-reader interaction, keyboard, caret/selection, forced colors, reduced motion, and native controls.
  2. Media: Flutter-compatible platform media integration, codecs, controls, tracks, fullscreen/PiP, autoplay/permissions, Media Source, and WebAudio.
  3. Offline applications: IndexedDB, Cache Storage, service workers, workers, file/blob streaming, notifications, installability, and offline lifecycle.
  4. Communications: production WebSocket/EventSource, WebRTC/device permissions, richer streaming/compression, and justified WebTransport.
  5. Graphics/documents: Canvas 2D, SVG breadth, WebGL/WebGPU, print/PDF, color management, advanced typography/writing modes, and CSS long tail.
  6. User ecosystem: scoped extensions, content blocking, password/autofill, import/export, developer tools, and policy controls.
  7. Defense in depth: renderer/content sandboxing, site isolation/OOPIF, brokered host access, crash containment, update/signing hardening.
  8. 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:

  1. 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.
  2. 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-push once 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/just gate.

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-headless is 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.

  1. 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.
  2. 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-credentials requires 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 module src or 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 bounded Date/Age plus max-age/Expires freshness, request cache directives, and exact simultaneous Vary variants plus bounded permanent same-origin redirect aliases. Transport body reads, exact progress/completion diagnostics through BrowserCore/C ABI/CDP, retained ReadableStream chunks, 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

  1. There is one production renderer: Flutter.
  2. BrowserCore never fabricates rendered geometry, hit tests, semantic bounds, or screenshots.
  3. Pointer input names an exact displayed commit and Flutter hit target.
  4. Render commits and queries are bounded, generation checked, cancellable where blocking, and fail closed when stale.
  5. Renderer-dependent fixture checks run in the Flutter host; native runners do not claim rendered evidence.
  6. No compatibility shim may restore deleted WebRender/EGL/frame/texture/Rust layout-paint details.
  7. 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

CommandCurrent evidence
just test-apiR1 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-impellerR3 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-alphaformatting, 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-architectureleaf-crate/frontend dependency rules; CDP has no renderer owner and rendered composition belongs only to Flutter
just test-flutter-controllerSafe 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-abiBuilds 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-shellpinned 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-smokereviewer baseline: formatting, clippy, host checks, and all host-runnable tests
just gate-pushhk pre-push integration point: alpha, phase-6 runtime, smoke, and diff checks
just gate-webidlgenerated WebIDL constructor/prototype coverage plus headless/CDP runtime-host integration
just gate-phase0workspace/API DTO and trait-shape foundation
just gate-phase1network/store tests, audit, and security fuzz targets
just gate-phase2deno_core runtime and headless eval seam
just gate-phase3HTML/selector/cascade behavior and CSS fixture profile
just gate-phase6engine 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 testbounded-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-coreBrowserCore 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-reportcurrent BrowserCore-backed committed fixture/profile counts and per-source/category output
just fuzz-securityURL, CSP, cookie, and HTML parser fuzz targets at the configured run count
just auditcargo audit plus cargo deny check
just linux-release-smokepinned 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-smokereal 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-smokereal 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-smokesame 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-smokerelease/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-manifestone 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-r5complete 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-r6focused 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-r6complete R6 gate: every R5 rendered fixture/CDP/Cage proof plus test-r6 synchronous layout and recovery evidence
just test-r7R7 absence scans plus native tests, Rust clippy, C header syntax, manifest/script validation, Dart format/analyze, and full Impeller-requested Flutter tests
just gate-r7complete renderer-transition gate: every R5/R6 rendered product proof plus R7 cutover/deletion evidence
just size-headlessstructured logical/allocated size, file count, and SHA-256 for the headless release binary
just size-flutter-linuxcontrolled 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-jsonper-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-jsonrelease/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-jsonsame 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-growthopaque temporary profile growth at init/repeated/unique/storage checkpoints with localStorage reopen proof
just baseline-measurehermetic 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.md from just 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-abi proves the handwritten C ABI/header/wire/buffer ownership milestone over the same safe controller. just gate-flutter-shell adds 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-smoke adds first native Linux AT evidence; linux-interaction-smoke adds the controlled native IME and basic-navigation vertical. Neither proves complete screen-reader coverage, packages, broader release behavior, or non-Linux GUI support; use FLUTTER_SHELL.md for remaining gates.
  • Size/performance thresholds become gates only after a representative baseline, environment, and comparison method are committed.
  • Hosted ci.yml runs 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 runs cargo audit and cargo deny check. fuzz.yml runs all four existing fuzz targets on a bounded weekly/manual CI budget and retains crashes. The one-million-iteration local/release command remains just 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.md is 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.md for 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 EnsureLayout and 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.json and every declared external profile are green; COMPAT.md publishes measured counts and limitations.
  • just audit, just check, hk pre-push, relevant fuzz targets, and git diff --check pass 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_core realm and BrowserCore document.
  • Script mutation drives a visible Flutter commit and CDP observes the same nodes.
  • Focus/event/form-validation ordering pinned by SPEC.md remains 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-net policy 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 integrity failure, 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.md works 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-sim BrowserCore/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:

  1. like-for-like hello-Flutter;
  2. Flutter+Vixen GUI;
  3. chrome-less rendered automation host; and
  4. 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:

CategoryFixtures
css17
css-cascade/css-values50
cssom-view1
dom25
dom-core50
events1
flexbox5
forms28
grid5
layout9
layout block/inline/position6
network2
paint4
paint/ref-equivalent8
security9
selectors50
Total270

Total manifest checks: 2027.

Current check mix:

Check typeCount
selector-count398
selectors-exact223
title269
js-eval597
computed-style173
element-attribute132
layout-box104
body-contains68
visual-hash25
no-critical-diagnostics22
ref-equivalent11
dom-nodes-range1
min-nodes1
selector-match3

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 focusoutfocusinblurfocus 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 enfren, 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.

Areav1.0 targetExpected achievabilityNotes
HTML parsing/tree constructionBroad smoke subset greenHighhtml5ever carries parser behavior; Vixen must preserve node ids/tree shape.
SelectorsModern selector subset greenHighBacked by Stylo/selectors; include combinators, attributes, :is, :where, :has, form/link pseudos.
CSS cascade/computed valuesInline plus one external stylesheet vertical greenHigh after full Stylo sliceCompact cascade is temporary; external-sheet media/import/CSSOM breadth and full Stylo remain.
CSS layout: block/inlinev1 visual/ref subset greenMediumFlutter-hosted Vixen formatter; start with normal flow, margin/border/padding, Paragraph-backed inline lines.
CSS layout: flex/gridUseful common-case subset greenMediumVixen Dart formatting contexts over Flutter primitives; full WPT edge coverage is post-v1.
CSS layout: tables/floats/fragmentationNot v1 release-blockingLow for v1Document as unsupported/partial until implemented.
DOM CoreTraversal, attributes, token lists, ranges, mutation observer subset greenMediumVixen-owned Web APIs over deno_core host extensions after the ADR-014 migration.
Events/forms/history/storageSelected behavioral subset greenMediumGate by fixtures from SPEC invariants and imported WPT cases.
JS languageUse V8/deno_core language coverage, not WPT percentageHigh for languageWeb API exposure remains Vixen-owned and separately gated.
Paint/ref testsFlutter scene/commit visual subset greenMediumOne formatter/Canvas path; pixels, geometry, hit/text/scroll, and semantic bounds share a commit.
Media/WebGPU/WebRTC/service workersOut of scope for v1Not targetedDeferred by ADRs / acceptance post-v1 scope.

Release-blocking WPT goals

For v1.0, Vixen should be able to claim:

  1. 100 % pass on local fixtures/manifest.json.
  2. Green imported WPT smoke profile for parser, selectors, cascade, DOM core, forms, and the v1 layout subset.
  3. Measured pass counts published here for every imported category.
  4. 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 areaMinimum useful target
selectors/css-scoping/css-nesting selector behavior50 fixtures
css-cascade / css-values computed-value behavior50 fixtures
dom/nodes + traversal + ranges50 fixtures
html/semantics/forms basics25 fixtures
css/css-display + css-box + css-position normal-flow layout40 fixtures
css-flexbox common cases25 fixtures
css-grid common cases25 fixtures
paint/ref-equivalent smoke20 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 areaFixtures runChecks runPassedPass rateNotes
selectors50232232100.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-values50250250100.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-core50250250100.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.
forms25134134100.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/position63030100.0%Block flow, margin/padding/border, auto margins, border-box sizing, inline flow, and relative/absolute positioned smoke.
flexbox52525100.0%Row/column grow-basis, gap/padding, and reverse-axis smoke.
grid52626100.0%Fixed, fractional, minmax(), row/column gap, and fixed-height fractional-row smoke.
paint/ref-equivalent82424100.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-content edge 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.

ArtifactLogical bytesAllocated bytesFiles
hello-Flutter21,398,66821,434,36812
Flutter+Vixen85,377,96085,430,27227
Vixen minus hello63,979,29263,995,90415

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.

ArtifactLogical bytesAllocated bytesFiles
hello-Flutter22,778,75022,814,72012
Flutter+Vixen85,509,52085,540,86413
Vixen minus hello62,730,77062,726,1441

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:

  1. hello-Flutter: the smallest representative native Flutter application;
  2. Flutter+Vixen GUI: release renderer/chrome plus BrowserCore; and
  3. 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.

MetricSoftware medianSoftware p95AMD/Mesa medianAMD/Mesa p95
CDP ready225.223 ms317.256 ms176.528 ms252.007 ms
first exact presented commit324.232 ms412.192 ms258.415 ms330.925 ms
exact-scene capture dispatch50.317 ms57.148 ms38.360 ms42.243 ms
capture client round trip50.642 ms57.567 ms38.699 ms42.543 ms
app-process VmHWM300,240,896 B302,569,882 B208,400,384 B208,770,662 B
direct mutation → commit frame15.402 ms28.812 ms14.527 ms29.248 ms
mouse release → commit frame26.364 ms29.746 ms25.269 ms29.185 ms
exact-frame build span71 µs91 µs70 µs89 µs
exact-frame raster span492 µs561 µs350 µs442 µs
exact-frame total span2,587 µs3,461 µs2,590 µs3,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:

ReportSchema
Headless scenariosvixen.headless-baseline-report version 1
Profile growthvixen.profile-growth-baseline-report version 1
Artifact sizevixen.artifact-size-report version 1
Flutter Linux raw bundlesvixen.flutter-linux-artifact-size-report version 1
Flutter Linux renderervixen.flutter-linux-renderer-baseline-report version 2
Scenario inputvixen.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

CrateCurrent responsibility
vixen-apiBrowser lifecycle and bounded renderer revision/mutation/commit/query/input/semantic contracts; no implementation dependencies
vixen-netHTTP and URL/cookie/CSP/CORS/referrer/mixed-content/security policy
vixen-storeBounded redb profile persistence and clear-data operations
vixen-engineSole BrowserCore owner for contexts, navigation, DOM/Page source, cascade, V8, history, input intent, resources, and accessibility meaning; no layout or paint backend
vixen-cdpBounded target/session/runtime adapter over a non-owning BrowserCore subscription and injected rendered backend
vixen-ffiSafe one-owner controller, C ABI v1, renderer broker, in-host CDP composition, copied bounded JSON, and panic containment; no frame ABI
vixen-headlessNative text/runtime/profile CLI and non-rendered CDP test composition; rendered methods fail closed
vixen-wptSource-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, and vixen-store are implementation leaves.
  • vixen-wpt depends only on vixen-api among Vixen crates.
  • vixen-engine owns 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, or ref-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

  1. A profile is opened once by BrowserCore. Cookies, cache, localStorage, permissions, HSTS, download history, and durable settings are profile-owned.
  2. Session history, sessionStorage, viewport/input, active navigation, runtime realms, and document state are browsing-context owned.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

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:

  1. derive source origin/partition, destination, credentials, referrer, CSP, sandbox, and permission context from authoritative state;
  2. validate URL/method/headers/body and private-network policy;
  3. apply HSTS, cookies, cache, redirect, mixed-content, CORS, and request metadata policy in a defined order;
  4. stream transport with request id, destination-specific limits, progress, and cancellation;
  5. apply response CORS/CORP/COEP/nosniff/integrity/content policy;
  6. 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.

BoundaryOwnerRequired behavior
CLI/CDP/GUI command → coreadapter + vixen-api DTO validationValidate ids/options/sizes; reject unknown/stale targets with stable errors
navigation/resource requestbrowser loader + vixen-netURL/private-network/header/body/policy checks on initial request and redirects
HTTP response → page/profilebrowser loaderCORS/security/integrity/content checks before exposure, execution, decode, cache, or persistence
JS → Rust op/resourceruntime host moduleWebIDL conversion, size/permission/origin checks, document-generation validation
DOM/style generation → Flutter mutationsBrowserCore + FFIExact base/target revision, bounded immutable data, known resources, deterministic resync
Flutter commit/query → browser inspection/inputrenderer bridge + BrowserCoreExact revision/commit, bounded finite geometry/ranges, known node/resource ids, reject stale or truncated-required answers
profile write/readprofile service + vixen-storePartitioned normalized records, bounds, transactional failure diagnostics
file/portal/downloadplatform host service + download managerApproved roots/handles, safe names, no ambient arbitrary write/open
inspector/snapshotengine inspectorBounded 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:

  1. leaf-unit tests for pure policy/data/formatting algorithms;
  2. engine integration tests for ownership, lifecycle, generations, and profile partitioning;
  3. committed local fixtures for focused regressions;
  4. pinned imported WPT profiles with source×category reports;
  5. Flutter GUI/chrome-less-host visual comparisons and external Playwright/CDP smokes;
  6. controlled real-site/platform-host corridor reports; and
  7. 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-eval for JavaScript whose result needs commit geometry/scroll;
  • layout-box for exact commit bounds;
  • visual-hash for Flutter scene hashes; and
  • ref-equivalent for 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/selectors for CSS, html5ever for HTML, deno_core/V8 for JS execution and host packaging, and Flutter Paragraph/Canvas/scene/Semantics for cross-platform render primitives (see DECISIONS.md ADR-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 in docs/COMPAT.md; if a behaviour isn't called out below, follow the latest stable spec and document deviations in docs/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):

CodeWhen
unsupported.screenshotNative/text-only CDP receives a screenshot request
invalid-selectorMalformed --extract-selector input

CDP methods required at v1.0:

  • Browser.getVersion
  • Target.createTarget, Target.attachToTarget, Target.getTargets
  • Page.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, and Page.captureScreenshot (PNG)
  • Runtime.enable, Runtime.evaluate, Runtime.awaitPromise, Runtime.getProperties, Runtime.consoleAPICalled, and Runtime.exceptionThrown
  • Network.enable, top-level Network.* navigation notifications, and the Playwright network-toggle methods (setCacheDisabled, setBypassServiceWorker, setExtraHTTPHeaders; extra headers apply to runtime fetch() requests, cache-disabled bypasses runtime fetch() cache reads/writes)
  • DOM.getDocument, DOM.querySelector, DOM.querySelectorAll, DOM.describeNode, DOM.resolveNode, DOM.getContentQuads, DOM.getBoxModel, DOM.getAttributes, DOM.getOuterHTML, DOM.setAttributeValue, and DOM.removeAttribute
  • Performance.getMetrics, Security.getSecurityState
  • Input.dispatchMouseEvent (mouse move/press/release over the current full viewport), Input.dispatchKeyEvent, and Input.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 typeAsserts
titleDocument <title> text
selector-countNumber of elements matching a selector
selectors-exactExact set of element ids matching a selector
body-containsBody text contains a substring
js-evalEvaluate JS, compare result to expected
flutter-js-evalEvaluate JS whose result requires an exact Flutter commit
min-nodesDOM has at least N elements
no-critical-diagnosticsNo critical EngineDiagnostic recorded
visual-hashPerceptual hash of rendered screenshot matches expected
selector-matchPer-element selector match details
computed-stylePer-element computed style value matches expected
element-attributeElement attribute value matches expected
layout-boxElement border-box (x, y, w, h) matches expected
dom-nodes-rangeDOM node count is within [min, max]
ref-equivalentRendered 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)
}
}

Cookies follow RFC 6265 with these Vixen-specific defaults:

  • Default SameSite is Lax (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.
  • HttpOnly rejected from document.cookie but accepted from Set-Cookie HTTP response. This is RFC-correct but called out because it's a frequent bug source.
  • Outgoing Cookie header: SameSite=Lax cookies are sent cross-site only for safe methods (GET/HEAD/OPTIONS). SameSite=Strict cookies are sent only to same-host requests. HttpOnly cookies never appear in document.cookie reads.
  • 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:

  1. Script executionscript-src (or default-src fallback). Inline scripts blocked unless 'unsafe-inline' or a matching hash/nonce is present.
  2. Fetchconnect-src, img-src, style-src, font-src, media-src, object-src, etc. URLs matched against source-list.
  3. Plugin content<embed>, <object> allowed only if object-src permits.

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 = min if 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 of step.
  • 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
  • focusout and focusin bubble.
  • blur and focus do 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:

  1. Exact revisions — a mutation batch applies only to its named base and target RenderRevision; gaps request full resync.
  2. 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_id and revision.
  3. Presented identity — input and native accessibility identify the displayed commit, not merely the newest completed layout.
  4. Stable paint order — stacking contexts, z-index, positioned content, and document-order ties follow CSS; viewport background remains first.
  5. Clip and transform identity — paint, hit testing, text/caret queries, and semantic bounds consume the same clip/transform chain.
  6. Opacity and visibility — group opacity composes through ancestors; opacity: 0 and hidden/collapsed paint are omitted while required layout state remains queryable.
  7. Scroll identity — renderer offsets/extents/clips and the scene share one commit; BrowserCore scroll events/history accept only that result.
  8. Finite bounded geometry — non-finite, oversized, unknown-node/resource, over-depth, or required-but-truncated geometry fails closed.
  9. No stale fallback — stale commits cannot target input, answer required geometry, publish Semantics, or become visible after replacement.
  10. 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_core runtime 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:

  1. Shape — constructor/prototype exists because WebIDL requires it.
  2. Pure value behavior — JS-only implementation is acceptable when it has no privileged state, I/O, persistence, origin policy, or layout dependency.
  3. 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.
  4. 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, CDP Runtime.evaluate, and WPT js-eval all 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-engine runtime 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 --eval consuming 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-engine or vixen-headless,
  • one user-visible seam check when applicable (--eval, CDP, or WPT fixture),
  • a note in COMPAT.md only when support level or known gaps materially change,
  • green just gate-phase6 before 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:

SubsystemSelected foundation
HTML parsinghtml5ever
CSS cascade and selector matchingStylo / selectors
JavaScriptdeno_core / V8
Native cross-platform scene, text, images, and accessibilityFlutter 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 selectors alone, 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 whole servo repo at that revision), non-reproducible from crates.io, blocks Phase 3 indefinitely.
  • Switch CSS engine to taffy or another standalone cascade. Rejected per ACCEPTANCE.md hard gates (no taffy); 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 style even though the package is stylo; source uses use style::… while Cargo.toml says stylo = …. Documented in style_dom.rs to 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.md and ACCEPTANCE.md.
  • Future Stylo releases may shift trait shapes (TElement etc.). Pin stylo = "0.18" and bump deliberately; track upstream https://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::script or pure sibling modules, not as one ever-growing script.rs file.
  • 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_core abstractions that solve that exact problem.
  • Abstract over mozjs and deno_core behind an internal JS-engine trait. Rejected: it would preserve two runtime mental models, hide useful deno_core concepts 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_core is the better Rust embedding/runtime substrate for Vixen.

Consequences.

  • deno_core is the vixen-engine::script dependency; mozjs is no longer in the active engine dependency graph.
  • Internal host modules may depend on deno_core APIs 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.md pins 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_core op/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 just with hk commands. Rejected: just recipes 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-push first; 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 + Sync and distribute it immediately. Rejected: it adds locking/reentrancy before measured isolation or throughput requires it.

Consequences.

  • just gate-architecture forbids 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 EnsureLayout work 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:

  1. Define and test bounded RenderRevision, mutation/full-snapshot, commit, presented, geometry, target, scroll, semantic-bound, and query DTOs in vixen-api, including semantic-action targets bound to document, displayed commit, semantic node, and advertised action generation.
  2. 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.
  3. 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.
  4. Prove same-commit pixels, input targeting, find/caret ranges, scrolling, Semantics, and scene capture in the Linux shell.
  5. Add the chrome-less Flutter host under Cage and move visual/layout fixtures plus screenshot/CDP capture to it.
  6. Implement and race-test bounded synchronous EnsureLayout, cancellation, resync, renderer loss, and same-task mutation-to-geometry behavior.
  7. Cut production over; aggressively delete WebRender/EGL/RGBA/texture and superseded Rust layout/paint code, dependencies, tests, gates, and docs.
  8. 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)

ReferenceUpstreamPinned revisionBranchUsed for
Firefoxhttps://github.com/mozilla-firefox/firefox.git46e9f12a8f9bmainCSS 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.
Ladybirdhttps://github.com/LadybirdBrowser/ladybird.git0de15a5dd2a9masterCSS box-tree/formatting/fragmentation architecture reference; Vixen reimplements required semantics in Dart.
Flutterhttps://github.com/flutter/flutter.git6655482ec06establePrimary 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.git21e02b9a272dmainLinux browser AppStream/desktop metadata, portal behavior, and Flatpak manifest conventions.
Obscurahttps://github.com/h4ckf0r0day/obscura.gitca71ce3c2da9mainHeadless CLI design, CDP server patterns, single-binary distribution.
Deno / deno_corehttps://github.com/denoland/deno.git83c50b1da61emainPrimary 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.), never HEAD or main.
  • 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:

  1. Visible seam first. Prefer code that reaches the engine-owned browser/ context/document path, vixen-headless, CDP, or a committed WPT/fixture check. A Page slice must preserve BrowserCore ownership and name the live document seam it advances. Pure prep is fine only when the next visible seam is named.
  2. One trust boundary at a time. For security-sensitive paths, name the boundary, validate near it, fail closed, and surface stable error codes.
  3. 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.
  4. 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.
  5. 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.md or an explicit plan note.
  6. 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.
  7. 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.

TierUse whenCommand shape
Inner loopEditing one crate/modulecargo check -p <crate> plus focused cargo test ... <name>
Pre-commitA commit is being madehk pre-commit: cargo fmt, merge-conflict/private-key scan, staged diff whitespace check
Alpha sliceA coherent partial capability is readyfocused tests + relevant just gate-phaseN
Pre-pushWork is ready to leave the machinehk pre-push: just gate-push
ReleaseVersioned release readinessevery 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.md honest: 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-phaseN as 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.md gates 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:

  1. playwright-core connects through chromium.connectOverCDP(...).
  2. Target/page/runtime/network/DOM enable and navigation methods route to the selected BrowserCore context.
  3. Playwright obtains layout from an exact Flutter commit.
  4. One stable live DOMStringMap write reflects to data-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.
  5. One retained live classList survives the pointer-driven class mutation, reflects clicked, agrees with 140px synchronous/CDP geometry, and produces a second pinned exact Flutter PNG.
  6. One retained live relList survives external and list-driven rel writes 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.
  7. One retained iframe sandbox list 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.
  8. One retained inline CSSStyleDeclaration survives external and declaration API writes, reflects current serialized declarations, agrees with 120×32 synchronous/CDP geometry, and produces a fifth pinned exact Flutter PNG.
  9. One retained NamedNodeMap and attached Attr survive external and Attr.value writes, preserve indexed/named identity, agree with 120×32 synchronous/CDP state, and produce a sixth pinned exact Flutter PNG.
  10. Retained structural collections start empty, reflect the pointer-created rendered #dynamic.badge through indexed/named access, and agree with an authoritative CDP node while a preexisting selector-all list remains static.
  11. 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.
  12. 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.
  13. 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.
  14. Pointer input uses the displayed commit's Flutter hit-test handle and target; the C ABI has no raw coordinate command.
  15. Later DOM/style mutation produces a new source revision and distinct exact scene.
  16. Page.captureScreenshot and high-level screenshot return direct Flutter scene PNGs without browser/compositor chrome.
  17. Simultaneous 320×240 and 480×300 targets keep source, viewport, input, and scene state independent.
  18. Switching targets does not lose presentation state.
  19. Forced renderer reset requests a full snapshot and recovers a byte-identical scene.
  20. 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.

GuideWhen to read it
mise.mdActivating the project-managed toolchain and using just recipes correctly. Start here for local setup.
cargo-home.mdWhy CARGO_HOME points at <workspace>/.cargo and how recipe-installed Cargo tools stay local.
deno-core.mddeno_core/V8 embedding, host-extension shape, and cache/pinning notes.
flatpark-release.mdBuilding 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:

  • mise pins tool versions and exports the project environment from .mise.toml (RUSTUP_TOOLCHAIN, CARGO_HOME, HK_MISE, and PATH).
  • just owns repository actions. Add or update a justfile recipe instead of copying cargo ... command lines into docs, scripts, or CI.
  • hk owns git lifecycle hooks. The project config is hk.pkl; install hooks with just hooks-install or 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

RecipeWhat it does
just setupOptional dev tools + nightly for fuzzing + check-all-host
just hooks-installInstall/update hk hooks through hk install --mise
just check / just check-all-hostType-check the host-runnable workspace
just test / just test-hostRun host-runnable tests
just smoke / just gate-smokeFormatting check, clippy, check, tests
just gate-pushLong hk pre-push gate
just auditcargo audit and cargo deny check
just fuzz-securityPhase 1 fuzz targets at 1 M iterations
just linux-release-prefetch / just linux-release-smokeBuild 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:

  • cargo resolves under <workspace>/.cargo/bin.
  • just resolves under mise's install directory.
  • hk resolves under mise's install directory.
  • cargo --version matches the Rust version pinned in .mise.toml.
  • CARGO_HOME is <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>/.cargo makes 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 ~/.cargo from other projects.
  • No cross-project leakage. Vixen's cargo-binstall packages 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 plus cargo-audit, cargo-deny, and cargo-fuzz installed by mise bootstrap / just setup-dev-tools are runnable from an activated shell)
  • .gitignore ignores everything under .cargo/ except config.toml, which is the project-pinned Cargo config and ships with the repo.
  • .cargo/config.toml is 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 cargo themselves; make sure they inherit the mise env (direnv integration, or launch the editor from a mise-active shell). If they don't, they'll fall back to ~/.cargo and re-download the registry there. Harmless, just slow.
  • Other projects on the host. They keep using ~/.cargo as 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(...)
  • JsValue
  • vixen-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.rs owns deno_core::JsRuntime construction and V8 value conversion.
  • webidl.rs renders 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.rs registers the first op-backed host extension; JS constructors delegate UTF-8 encode/decode work to vixen-engine::text_codec through ops.
  • dom.rs registers the compatibility projection while host families move to live resources. Page data crosses the deno_core op boundary through op_vixen_dom_snapshot; element data is loaded through op_vixen_dom_element_snapshot; selector lookup, Element.matches(), element text/attribute reads, and read-only token surfaces delegate through focused DOM ops. HTMLElement.dataset is the first host-family convergence: one stable Proxy-backed DOMStringMap reflects live attributes and routes assignment/deletion through the shared Rust name conversion and normal Page mutation path. Element.classList is the second convergence slice: its stable DOMTokenList identity reads the current class attribute and writes through the same Page mutation path instead of being discarded after each mutation. HTMLAnchorElement.relList and HTMLIFrameElement.sandbox now follow the same retained live-object path, completing the token-list attributes currently hosted by the runtime. HTMLElement.style also retains one live inline CSSStyleDeclaration across external and declaration-API writes. Element.attributes retains a live NamedNodeMap and stable attached Attr nodes 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-backed NodeList/HTMLCollection objects; 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.rs registers the focused read-only CSSOM extension. CSS.supports, getComputedStyle, and retained live document.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-webidl is the focused regression gate for this layer: generated interface/prototype coverage, JsRuntime eval, headless --eval, and CDP Runtime.evaluate must 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 Vary remain fail closed. The fourth checkpoint uses the Deno-maintained import_map crate inside PageModuleLoader for one bounded inline map registered before module discovery. Exact, prefix, URL-like, null-blocking, and scoped mappings plus import.meta.resolve() feed the existing resource/policy path; module src is 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 because import_map 0.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 EngineError codes.
  • JS bootstrap exposes Web-shaped objects but delegates behavior to Rust ops or shared pure modules.
  • Long-lived host state uses deno_core resources/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:

  1. Vixen CI builds and tests vixen-linux-x86_64.tar.gz from the tagged source revision.
  2. 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.1 from the checksum-pinned Linux x64 archive declared in .mise.toml;
  • Flutter revision 6655482ec06e547f90abf8ae7590466f4415978d;
  • engine revision 5d531788691ec3404cac0cee66ead4007b177363 and engine content hash 11d79658c444477b06513d32b52c8c4ccb7276b0;
  • SDK archive SHA-256 a1d8166c0309267cb7dc99f1424eecf08b86946ad3b50723c6f59945964aea45;
  • GNOME 50 Linux builder image digest sha256:a2b78890f165cd5b5c6a8629c5f6cb293e64d1bf523ca6662fac8ca8e247f8b0;
  • Rust 1.96.1;
  • Cargo.lock and flutter/vixen_shell/pubspec.lock; and
  • rusty_v8 v149.4.0 archive SHA-256 aa30f198b6e7be2188df6498f95053c4c052f212037a01f2c31414d7aca84b53.

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.so through 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:

  1. install and launch the release archive locally;
  2. cut and verify the GitHub Release;
  3. update the FlatPark registry entry to that immutable asset;
  4. run FlatPark's descriptor validator and publish.sh --verify;
  5. install from the isolated test repository and exercise startup/navigation;
  6. 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.