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

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.