Roadmap
This roadmap moves Vixen from its original WebRender/RGBA prototype to the full project goal: a credible Firefox replacement with one Flutter-hosted web renderer and browser shell on Linux, macOS, Windows, Android, and the Apple Silicon iOS Simulator, plus first-class rendered CLI/CDP/WPT automation through the same Flutter renderer.
Linux is the first renderer, GUI, automation, integration, packaging, and release target. The other platforms remain committed, but they reuse the BrowserCore/renderer contract proven on Linux rather than delaying it.
Product direction lives in PROJECT_DIRECTION.md, the
current architecture in ARCHITECTURE.md, accepted decisions
in DECISIONS.md, measured support in
COMPAT.md, and executable commands in
MILESTONES.md. PLAN.md is historical only.
Destination and release ladder
The stages are capability gates, not dates:
- Renderer transition — one cross-platform visual truth. BrowserCore emits bounded render mutations; Flutter commits layout, scene, geometry, hit testing, text queries, scroll state, and semantic bounds. Rendered GUI/headless/CDP/WPT use it. WebRender/EGL/RGBA and superseded Rust layout/paint are deleted.
- Alpha — one browser architecture. BrowserCore owns one profile/context/ document/runtime lifecycle; the Flutter renderer owns no browser truth; live script mutation, inspection, input, and pixels converge on exact render commits.
- Beta — a measured useful browser. A controlled real-site corridor works in the Linux GUI and chrome-less renderer with representative layout, interaction, persistence, downloads, diagnostics, accessibility, and host integration.
- v1.0 — an honest daily-driver minimum. The published corridor is reliable enough for focused daily use, release/security operations are credible, and every supported capability and platform has reproducible evidence.
- Replacement horizon — broad modern-browser capability. Media, offline applications, richer graphics/communications, extensions, accessibility, and stronger isolation widen ordinary use until “Firefox replacement” is an honest default description.
No stage implies global Firefox or WPT parity. Every compatibility claim names the profile, platform, renderer host, command, and measured result.
Current baseline and transition debt
As of 2026-07-16 the repository has:
- one eight-crate Rust workspace with hk/
justgates, stable diagnostics, fuzz targets, a fixture/WPT harness, and a committed 270 fixture / 2,027 check 100% baseline; - dependency-free renderer protocol v1 DTOs and reference validation in
vixen-apifor exact revisions, bounded source snapshots/mutations/resync, atomic commit/presented state, geometry/text/scroll queries, displayed-commit input, semantic actions, replay rejection, and explicit handle retirement; - one
BrowserCoreowner for profile services, contexts, navigation generations, DOM/Page state, V8 runtimes, history, input intent, inspection, and ordered events used by Flutter, native text utilities, CDP, and WPT; html5ever, Stylo selector/cascade integration,deno_core/V8, shared network/security policy, and bounded redb profile tables;- generation-cancellable main-document, external-script, stylesheet, and bounded PNG loading plus deadline-bounded V8/runtime-fetch cancellation;
- a useful CDP/Playwright slice and a Linux Flutter shell with native Wayland chrome, input/IME, Semantics, scrolling/find/zoom, recovery, and deterministic release/Cage evidence; and
- one Flutter renderer: R7 deleted the Rust layout/paint island, WebRender/gleam, both EGL owners, native visual headless, RGBA frame transport, Linux texture presentation, raw coordinate input, and their obsolete tests/gates.
Architecture rules for every stage
- BrowserCore owns browser truth. Profile → browser → context → document is authoritative for navigation, DOM, V8, Stylo computed styles, network/security, persistence, history, resource acceptance, events, and accessibility meaning.
- Flutter owns rendered truth. The renderer owns CSS box/anonymous trees, formatting/fragmentation, Paragraph/image measurement, paint order, clips, transforms, mechanical scroll geometry, hit testing, semantic bounds, scenes, and capture. Public Flutter scene APIs sit over required Impeller; a Skia fallback does not satisfy a Vixen rendered-platform gate.
- Mutations are not a second DOM. Dart receives bounded immutable
RenderMutationBatchdata with stable ids and exact compound revisions. It cannot mutate navigation, DOM, policy, or durable state. - Commits are atomic. One
RenderCommitpairs scene-ready layout, geometry, an opaque Flutter-side hit-test handle, text-query state, scroll state, and semantic bounds. Visible input and native accessibility name the displayed commit. - Basic geometry comes back to BrowserCore. Flutter computes it; BrowserCore validates and queries the immutable index for synchronous DOM/CSSOM/CDP operations. Paragraph-specific queries remain bounded renderer services.
- Synchronous layout is explicit. Same-task mutation followed by geometry
uses deadlock-safe, cancellable, deadline-bounded
EnsureLayout; stale approximations cannot become the permanent behavior. - One renderer after cutover. Experimental Flutter rendering is test-only until parity. Production cuts over once, then deletes WebRender/EGL/RGBA and obsolete Rust renderer ownership. No fallback renderer survives.
- Policy precedes renderer exposure. URL/CSP/CORS/mixed-content/integrity, response type, body/decode limits, and cache policy run before Flutter receives image/font/resource data.
- Every content-controlled boundary is bounded. Mutations, snapshots, strings, nodes/depth, resources, image/font bytes, fragments, queries, commits, queues, V8 work, protocol handles, and diagnostics have explicit limits.
- Generations reject late work. Navigation, runtime, resource, renderer, query, scroll, input, and semantic results cannot affect a replacement document or commit.
- Flutter supplies primitives, not CSS semantics. Flutter Flex/widgets and
packages are not accepted as CSS implementations. Vixen formatting code is
WPT-driven and uses
dart:uiParagraph/Canvas/scene primitives. - Linux proves the contract first. Framework support is not Vixen support; each platform and ABI earns native renderer, input, accessibility, lifecycle, host-service, package, size, and performance evidence under ADR-019.
Renderer transition — execute before feature breadth
Keep one active renderer slice. Only an independently critical BrowserCore security/lifecycle fix may run beside it. Do not widen native interaction, WebRender, Rust layout, text shaping, paint effects, packaging registries, or new Web API shape while it would create porting work for the renderer transition.
R0. Freeze and name ownership — landed with ADR-022
- Consolidate current decisions and remove superseded renderer/shell/layout ADRs.
- Mark WebRender, EGL, RGBA frame transport, native visual headless, and Rust layout/paint breadth as transitional.
- Make the mutation/commit/query model and aggressive deletion policy the sole current direction.
Proof: no current-direction document names WebRender or Rust layout as the
target; git diff --check, docs build, and architecture references are clean.
R1. Renderer protocol types — landed
Dependency-free, versioned, bounded DTOs in vixen-api now provide:
- compound
RenderRevisionwith context, document, source/style, viewport, and resource generations; - incremental
RenderMutationBatch, exactbase_revision, bounded full snapshot, and resync request; - stable render node/resource/fragment/commit ids;
- atomic
RenderCommitand separatePresentedacknowledgement; - immutable geometry indices, opaque Flutter-side hit-test handles, text/caret/range query DTOs, scroll snapshot/commands, semantic bounds, and truncation/limit diagnostics;
- input targets carrying displayed commit, revision, node/fragment, and finite coordinates; and
- semantic-action targets carrying document, displayed commit, semantic node, and advertised action generation.
Define limits before payload details. Prefer plain arrays/records and explicit release over a generic scene framework.
Proof: just test-api covers malformed/round-tripped ids, exact monotonic
source and viewport generations, non-finite geometry, oversized/deep snapshots,
unknown resources, atomic invalid-batch rejection, missed bases, deterministic
full-resync recovery, equal-revision idempotence, stale/late commits, separate
presentation, query correlation, bounded UTF-16 ranges, truncation policy,
scroll-command replay, forged/stale/replayed semantic actions, and explicit
opaque-handle retirement. Strict API Clippy and the all-target workspace check
pass. This is model-only evidence: no C ABI, Dart bridge, broker, or production
renderer changed.
R2. Native/Dart bridge and broker — landed
- Carry R1 DTOs through the safe Rust controller, C ABI, handwritten Dart models, and fake controller.
- Add a dedicated renderer request/response channel that the Flutter UI/renderer isolate can service while the BrowserCore command worker or V8 evaluation is waiting.
- Keep ordinary mutation/commit flow asynchronous; reserve the broker for
EnsureLayoutand bounded renderer queries. - Prohibit renderer-to-BrowserCore re-entry during layout and release every retained payload/resource explicitly.
Proof at R2: ABI/header/layout checks, Dart/Rust golden round trips, malformed and stale wire tests, cancellation/timeout tests, queue bounds, worker-blocked broker service, shutdown, and full resync. Production still displayed the old frame at that checkpoint.
Implemented evidence: the bounded RenderBroker is independent of the
serialized BrowserCore controller lock. Ordinary snapshots, every mutation
variant, and handle releases use a bounded asynchronous update queue; commits,
presentation, and resync use a separately bounded submission queue. Only
EnsureLayout, hit tests, and text queries use correlated request/response.
C renderer_poll/renderer_respond/renderer_submit/renderer_shutdown
entrypoints and handwritten Dart records are strict, versioned, and retain C
output only through the existing tokenized release contract. Total in-flight
requests remain capped after polling, update source is capped at 512 KiB before
JSON encoding, incoming messages remain capped at 64 KiB, and encoded output at
1 MiB. Timeout, late response, exact identity/kind correlation, cancellation,
queue saturation, shutdown wakeup, malformed wire, double release,
worker-blocked progress, native header, and Rust/Dart golden tests are checked
in. A small Dart service drives the formatter from the same transport; the
scripted fake enforces the same queue/payload bounds. Normal browsing still used
the old frame at R2.
R3. First Flutter-rendered document — landed test-only
Use one controlled fixture containing:
- block and inline boxes with margin/padding/background;
- mixed styled text requiring Paragraph measurement and wrapping;
- one BrowserCore-policy-accepted PNG image; and
- semantic heading/link/text descriptors.
Build the smallest Vixen Dart formatter over dart:ui, not a widget-per-DOM
adapter. Construct a Flutter scene and return one atomic commit with geometry,
an opaque Flutter-side hit-test handle, text ranges, scroll limits, and semantic
bounds.
Proof: exact-generation Impeller-backed Canvas pixels/visual hash, Paragraph line/range checks, image pixels, geometry index, renderer hit tests, Semantics bounds, scene capture, mutation update, stale rejection, and full resync. This path remains test-only.
Implemented evidence: just test-flutter-formatter-impeller drives one
immutable snapshot through a small flow formatter over dart:ui Paragraph,
Canvas/Picture, encoded PNG decode, Scene capture, geometry, reverse-paint-order
hit testing, UTF-16 range/point queries, scroll limits, semantic bounds, mutation,
presentation, explicit idempotent handle release, stale/equal-snapshot rejection,
deterministic resync, and reset. Candidate source/scene state publishes only
after successful formatting and bounded commit submission; failed submissions or
superseded asynchronous builds retain the previous revision and dispose their
Paragraph/image/Picture resources. Mixed
text runs have run/line fragments, padded boxes retain distinct content bounds,
and wrapped semantic text retains all Paragraph rectangles.
Software and Impeller-requested captures have separate exact raw-RGBA hashes.
The formatter remained test-only through R3; the bounded production vertical
below now reuses it without claiming the rest of R4.
R4. One interactive commit vertical — landed
Route one controlled Linux document through the new renderer for:
- displayed-commit pointer targeting and DOM click;
- wheel/key/script scroll intent,
preventDefault(), renderer clamp, returned scroll commit, and DOMscrolleffect; - find match and caret/range geometry from Paragraph;
- page zoom/viewport change as a new revision;
- BrowserCore semantic meaning combined with renderer bounds; and
- lifecycle hide/resume with stale scene/commit suppression.
Proof: widget/core/ABI tests plus a Cage interaction smoke. Every assertion names one commit id. The old texture path remains production-only comparison and is not widened.
Implemented evidence: the native Linux shell now requests
one bounded BrowserCore projection for the selected document, carries it over the
dedicated renderer update queue, formats it with the R3 service, validates the
returned commit in Rust, and paints the accepted RenderCommitPainter view.
At R4 completion the source was deliberately a basic title plus at most 64
non-hidden semantic elements (or bounded body-text fallback), not a claim of
computed-style or general CSS rendering. The R5 source checkpoint below has now
replaced that temporary projection.
Presentation is acknowledged only from a Flutter post-frame callback. Pointer input uses the formatter's displayed commit, opaque hit-test handle, exact revision, fragment, viewport point, and local point; Rust validates all of them, resolves text hits to the nearest BrowserCore semantic element, and only then dispatches the DOM event. Snapshot replacement, submissions, releases, and queues stay bounded; consuming a submission and publishing all resulting handle releases is atomic. At R4 the WebRender/RGBA texture was still the explicit fallback; R7 deleted it.
All six R4 behavior slices now cross the production seam:
- renderer-targeted down/up input synthesizes a real DOM
clickon the exact displayed commit in the native ABI smoke; - find results, highlight boxes, and endpoint carets come from commit-bound Paragraph UTF-16 geometry rather than the transitional layout;
- page zoom and physical viewport changes produce, accept, and present newer revisions/commit ids while retiring old handles;
- BrowserCore semantic descriptors use Flutter-computed bounds, and advertised tap/focus/value/range actions are suppressed unless the same commit and accessibility generation are still displayed; and
- lifecycle generations clear hidden presentation, reject late hidden work, and require a newer commit before resume while bounding acknowledgement retries.
- BrowserCore snapshots carry the accepted root offset and extent only after its
cancelable wheel/key/script policy runs. The formatter independently clamps
that intent, translates pixels, geometry, Paragraph queries, hit testing, and
semantic bounds together, and returns the offset in a newer atomic commit.
Canceled wheel input leaves the offset unchanged; the native ABI smoke covers
script, wheel cancellation/default, and key commits while the release-process
Cage interaction smoke correlates DOM effects with exact presented commit ids.
mousedownno longer publishes a replacement source before its matchingmouseup, and input is suppressed during source/commit transition windows, so strict stale validation remains enabled without breaking click synthesis.
Computed styled nodes/resources, nested Flutter scroll nodes, and DOM/script mutation batches remain broader renderer-transition work; deletion of the fallback remains R7.
R5. Chrome-less Flutter automation host
- Add a minimal Flutter entrypoint that opens an exact viewport without browser chrome, drives the same BrowserCore/renderer bridge, and captures an exact presented commit.
- Run it under Cage/wlroots headless Wayland on Linux.
- Move visual hashes, layout-box evidence, screenshots, and CDP screenshot/input workflows to it in coherent groups. Keep text-only native tests only where they require no pixels or geometry.
- Retire
display-list-containsand migrate its three assertions in two fixtures to commit-bound layout/pixel evidence before claiming the full manifest; do not recreate a Flutter display-list dump compatibility API. - Preserve independent contexts/targets and bounded startup/shutdown behavior.
Proof: fixture manifest through the Flutter host, external Playwright smoke, multiple target viewports, input, before/after script capture, renderer loss, and no compositor/chrome pixels in page screenshots.
First implemented checkpoint: the release bundle now runtime-selects a
page-only Dart host with an undecorated Linux runner window. One strict
--vixen-automation invocation requires an absolute file/HTTP(S) URL, a viewport
within the existing 4,096-pixel/64-MiB bounds, and an absolute bounded .png
output path. It bypasses profile tab restore/save and browser/frame fallback
capture, paints the accepted formatter view without browser widgets, then only
after a Flutter frame acknowledges and captures the exact still-presented commit
through Scene.toImage. Startup/capture is bounded to 60 seconds; successful
work closes the sole BrowserCore, while shutdown gets a five-second grace before
the process fails closed. just linux-automation-smoke launches that same
release/AOT bundle under Cage twice at 320×240 and 480×300 with fresh profiles;
it checks Impeller and exact commit diagnostics, strict PNG structure/dimensions,
RGBA scene pixels, and pinned full-scene hashes. Because capture serializes the
formatter scene rather than the Flutter or compositor surface, browser,
runner, and compositor chrome cannot enter the PNG. Dart tests cover
configuration rejection, legacy-capture suppression, exact presentation identity,
PNG encoding, and output bounds. At that checkpoint this did not yet satisfy
full R5: fixture
manifest, layout evidence, CDP/Playwright screenshot and input routing,
independent simultaneous targets, before/after mutation capture, and renderer
loss remain to migrate.
Renderer-source checkpoint: BrowserCore now publishes the bounded renderable
DOM tree rather than synthetic title/semantic wrappers. Element ids are the
stable BrowserCore node ids; renderer-only text ids occupy a disjoint range;
parent/sibling/depth topology, viewport-resolved Stylo properties, accepted PNG
resources, semantic descriptors, and root scroll intent travel in one validated
FullRenderSnapshot. Metadata/script/style subtrees are counted for stable DOM
ids but excluded from renderer payload and paint. The Dart formatter consumes
authored dimensions, per-side margin/padding, background colors, visibility,
image sizing, and page zoom while preserving exact commit input validation. The
release Cage hashes now cover actual fixtures/dom/basic.html DOM text rather
than the former synthetic document card. This establishes the source needed by
the remaining manifest/CDP migration; it does not by itself satisfy the proof
paragraph above.
Shared-core CDP checkpoint: CDP protocol ownership now lives in the reusable
vixen-cdp adapter. BrowserCore can create independent bounded event
subscriptions without cloning lifecycle ownership, so the long-lived release
Flutter host runs the listener against its sole BrowserCore. Rendered CDP
screenshots publish a target-specific full snapshot, wait for its exact Flutter
commit and Presented acknowledgement, then return bounded raw PNG bytes from
the displayed scene. DOM.getContentQuads/DOM.getBoxModel and mouse input use
Flutter commit geometry/hit testing in this mode. At that checkpoint native CDP
retained a comparison backend; R7 later deleted it. just flutter-cdp-playwright-smoke proves
320×240 and 480×300 targets alive together, target isolation, Flutter-routed
input, before/after mutation pixels, target switching, no chrome pixels, and a
forced renderer reset followed by byte-identical full-resync capture. The old
display-list-contains manifest check is removed; its three assertions now use
computed-style together with existing layout/pixel evidence. At that checkpoint,
full fixture-manifest routing was the last R5 migration item.
R5 complete: the Dart formatter now implements the bounded fixture slice of
content-box/border-box block and inline flow, relative/absolute positioning,
row/column/reverse flex sizing, fixed/fractional/minmax grid tracks, gaps,
deterministic text line geometry, backgrounds, borders, and images. just flutter-fixture-manifest starts one release/AOT Flutter host under Cage and runs
all 270 fixtures / 2,027 checks in manifest order. Every fixture uses a fresh
target in the host's sole BrowserCore, so script/style mutations and rendered
assertions share one document/runtime lifecycle. The 1,868 native-safe
document/runtime checks use typed BrowserCore inspection; 19 flutter-js-eval,
104 layout-box, 25 visual-hash, and 11 ref-equivalent checks use exact
presented Flutter commits. Reference
checks compare direct RGBA scene pixels, visual baselines now name Flutter
scenes, and the native runner is text/runtime-only. just gate-r5 composes this
manifest with the one-shot and external Playwright gates. R6 synchronous layout
and R7 cutover/deletion are now also complete; R8 stabilization is next.
R6. Synchronous layout and recovery gate — landed
Implement:
- DOM mutation → Stylo flush → mutation batch →
EnsureLayout→ matching commit → synchronous geometry answer; - repeated/batched geometry reads without repeated layout;
- cancellation by navigate/stop/close/shutdown;
- renderer timeout, crash/loss, malformed commit, resource eviction, missed revision, and bounded full-resync recovery; and
- no BrowserCore mutex held during wait, no Dart re-entry, no late commit, and no poisoned next request.
Proof: same-task style/DOM mutation plus getBoundingClientRect(), Range and
caret queries, forced races/timeouts, isolate reuse, and GUI/CDP agreement on the
same commit.
Implemented evidence: BrowserCore page realms now share the one authoritative
Page with a synchronous geometry host. A geometry read drains the task's
bounded DOM mutation sink, refreshes the Page cascade, diffs the previous exact
renderer source into a RenderMutationBatch (or publishes a full snapshot for
first load/resync), and waits on the dedicated broker without holding the C
controller or renderer-state mutex. The response is accepted only after its
matching asynchronous commit submission validates against the same replica.
Repeated element reads reuse that commit; Range boxes and collapsed caret
rectangles use commit-bound batched Paragraph text queries.
Navigation, stop, close, shutdown, and the V8 deadline carry explicit renderer
cancellation while the normal GUI keeps a separate bounded UI-isolate broker
pump alive even when its browser command worker is blocked. Late replies are
unknown/inert. One bounded retry sends a full snapshot after renderer resync,
timeout, malformed commit, or missed state; a non-finite malformed submission is
consumed and retired without poisoning the next request. Focused tests prove
same-task style mutation plus two reused element reads, Range and caret geometry,
exact source batches, renderer-reset full resync, navigation/stop races, late
reply rejection, malformed-commit recovery, and same-isolate reuse. just test-r6 runs the focused Rust/Dart gate; just gate-r6 composes it with all R5
rendered fixture/CDP/Cage evidence.
R7. Production cutover and aggressive deletion
R7 cut over after R3–R6 were green and removed in one reviewed migration series:
webrender,gleam,GlContext, native renderer integration, and WebRender image upload;- native-headless and FFI frame EGL implementations;
- RGBA frame ABI/tokens/pools, Dart frame worker, Linux pixel-buffer texture plugin/presenter, and texture recovery tests;
- Rust display-list/paint modules and formatting/layout code not explicitly reused by the Dart formatter;
- obsolete visual/layout tests, gates, docs, dependencies, fixtures, and CLI flags rather than preserving compatibility shims; and
- duplicated scale, hit-test, scroll, text-metric, and semantic-bound projections.
Use source search and dependency gates to prove absence. Do not retain dead APIs for hypothetical embedders.
Proof: one Flutter renderer in dependency/source scans; no WebRender/EGL/frame transport; GUI and chrome-less host share mutation/commit code; all supported layout/pixel/input/semantics/CDP evidence uses it.
Landed: production GUI and automation always paint Flutter commits. The
WebRender/gleam dependency graph, GlContext, both EGL implementations, native
visual headless, screenshot/incremental CLI flags, RGBA C/Dart transport, Linux
texture path, Rust layout/display-list/paint and paint-helper modules,
PaintSnapshot, Page hit testing/geometry/semantic bounds, raw coordinate-input
ABI, native rendered WPT/CDP checks, and obsolete Phase 4/5 gates are deleted.
flutter-js-eval makes renderer-dependent manifest checks explicit. just test-r7 proves source/dependency absence and both native/Flutter surfaces;
just gate-r7 composes all R5/R6 rendered evidence.
R8. Linux stabilization and rebaseline
- Reproduce the compatibility manifest and imported profiles through appropriate
native or Flutter-hosted paths; update
COMPAT.mdonly from output. - Re-run Linux interaction, IME, AT-SPI, release archive, startup, memory, frame, screenshot latency, and profile-growth evidence.
- Rebaseline hello-Flutter versus Flutter+Vixen and attribute removed WebRender/EGL/frame code, new Dart formatter, and chrome-less-host costs.
- Fix renderer-transition regressions before broadening APIs or resuming FlatPark publication work.
Compatibility reproduction checkpoint: on clean revision e224bf6, just compat-report reproduced all 270 fixtures and all 1,868 native-safe BrowserCore
checks at 100%. The post-R7/Yaru release/AOT Flutter host subsequently reproduced
the full 270 fixtures / 2,027 checks at 100%, including 19
flutter-js-eval checks plus 104 exact layout boxes, 25 visual hashes, and 11
exact-pixel references. Renderer evidence is kept separate from, not inferred
from, the native run.
The matching external Playwright/CDP rerun is also green: two target viewports, Flutter-routed geometry/input, before/after mutation captures, target switching, and forced renderer reset/full-resync all retained exact scene identity.
Renderer/frame/GPU measurement checkpoint:
just baseline-flutter-linux now measures the release/AOT CDP host from process
spawn through exact capture, then joins eight direct mutations and one mouse
release to exact presented Flutter commits and engine frame timings. Clean
five-run/one-warmup version-2 references contain 45 interaction frames each.
Mesa software records 15.402 ms median mutation → commit-frame, 26.364 ms mouse
release → commit-frame, and 2,587 µs exact-frame total span; the corresponding
AMD Ryzen 7 7700X integrated-GPU/radeonsi/Mesa 26.0.4 run records 14.527 ms,
25.269 ms, and 2,590 µs. Renderer-specific exact PNGs repeated in every sample
and all processes exited cleanly. Cage reported no refresh rate, and Flutter
raster finish is not compositor scanout. These are checked-in measurement-only
single-host observations, not budgets, animation stability, physical-input
latency, isolated Flutter/GPU attribution, or a supported GPU matrix.
First size/release checkpoint: clean, equally stripped Flutter 3.47 hello and post-R7/Yaru Vixen release bundles now have a checked-in component report. The 85,377,960-byte Vixen bundle is 131,560 bytes smaller than the historical pre-R7 bundle despite adding Yaru assets/plugins; its aggregate native library is 2,076,976 bytes smaller. The hello control also shrank, so the current 63,979,292-byte Vixen-minus-hello delta is larger and is not misreported as a product regression. The same Vixen bundle produces a deterministic 31,913,890-byte archive; clean extraction and a bounded Cage launch reported Impeller and presented an exact Flutter commit. These are unreproduced measurements and one controlled launch, not budgets, sustained release evidence, or FlatPark install evidence.
Profile-growth checkpoint: a clean five-repeated/five-unique-visit run kept the opaque profile's logical size constant, added 8,192 allocated bytes across repeated visits and zero across unique visits, then added 139,264 bytes for a 65,536-byte localStorage payload that a fresh process reopened successfully. This is a checked-in single-host measurement, not a growth budget or broad history/cache workload.
Native interaction/accessibility checkpoint: R8's final gate passed on
2026-07-17. An unchanged Fedora ibus-mozc/mozc
2.29.5111.102-16.fc43 pair ran from a workspace-local extraction under a private
IBus daemon; a user-namespace bind supplied its compiled /usr/libexec path
without changing host packages. The release/AOT Cage run observed real GTK
preedit start/update/end and commits in both the native input and
contenteditable controls. A narrowly scoped Linux-runner guard terminates
Flutter 3.47's recursive Component.get_extents walk at its non-component
FlViewAccessible root; descendant bounds remain Flutter-authored. The same run
then observed the editor as text/editable/visible/showing with positive bounds
(8, 187, 40, 20), invoked Flutter's unchanged native Focus action, reached
DOM focus=editor, and advanced the same document from commit 18 to 20. The
complete interaction corridor continued through IME, wheel ownership and
cancellation, script/root scroll, navigation stop/recovery, keyboard input, and
clean app exit (commits=3>31>34>40>45). just linux-at-spi-smoke separately
passed the process-filtered name gate. This closes R8; it is one controlled
Linux/IBus/Mozc/AT-SPI proof, not an IME, assistive-technology, compositor, or
device matrix.
GTK4 toolchain migration checkpoint: on 2026-07-18 the release runner moved
to the immutable flutter-dev 328b829d35 SDK, Dart 3.14.0-28.0.dev, and
libflutter_linux_gtk4.so. The GTK3 ATK guard and GTK3-only Yaru/window plugins
were removed. Fresh GTK4 evidence observes BrowserCore names, text role,
editable/visible/showing states, and positive local (0, 0, 40, 20) bounds,
while /proc proves GTK4 is loaded and GTK3 is not. The deterministic
headless-window interaction run advances atspi=21>24 and
commits=3>37>40>46>51 through native IME and pointer input. The pinned GTK4
engine does not expose AT-SPI Action or transformed screen-coordinate origins;
those old GTK3 properties remain historical evidence, not current GTK4 claims.
Exit: the controlled Linux corridor uses no transitional renderer component, all renderer failure modes are bounded, and the next compatibility failure can be reduced directly against the final architecture.
Alpha — converge live browser state on render commits
R8 and A1 are complete. Continue shared-core convergence in this order without reintroducing native renderer ownership or weakening the landed host gates.
A1. Live document/runtime convergence
Status: complete (2026-07-17). The mutable surface Vixen currently claims is live and Page-backed. Bounded op snapshots remain transport read models; they no longer stand in for mutable host-object ownership. APIs outside the claimed subset fail explicitly rather than presenting plausible inert behavior.
- Replace remaining Page/runtime compatibility snapshots with live Node/Element/Document, CSSOM, events, focus, selection, forms, history, and storage resources.
- Make every relevant mutation produce one render-source revision and invalidate accepted geometry explicitly.
- Execute parser classic/module scripts with document event-loop and microtask ordering; preserve realm teardown and same-origin frame boundaries.
- Delete plausible inert compatibility shims as real owners land.
First A1 checkpoint: HTMLElement.dataset is now one stable live
DOMStringMap per element instead of a frozen property projection. External
attribute changes reflect into the retained object; property assignment/deletion
uses the shared Rust name conversion and the normal DOM mutation path. Focused
runtime proof requires exactly one render-source generation per write and Stylo
attribute-selector recascade. The release/AOT Playwright smoke then performs one
dataset write, observes 140×32 geometry synchronously in that task, reads the
same attribute/node/geometry through CDP, and pins different before/after exact
Flutter PNGs. This is one live host-family vertical, not completion of A1.
Second A1 checkpoint: Element.classList now retains one live
DOMTokenList identity across external and list-driven class mutations rather
than discarding the wrapper after every attribute write. Focused runtime proof
retains the object through setAttribute, reflects current tokens, advances
exactly one renderer-source generation per write, and recascades .wide and
.tall selectors to 140×30. The release/AOT Playwright corridor retains the
same object through Flutter-routed input, observes clicked and 140px geometry
in the page task and CDP, and pins the resulting exact Flutter PNG to
5633ca7a032c8c6a1582f5389b6b4a594b91d99e89784683fbf3679f18639f95 before
byte-identical target switching and renderer recovery. This converges one more
attribute-backed host object; other token lists, inline style, collections, and
attribute nodes remain separate work.
Third A1 checkpoint: HTMLAnchorElement.relList now retains one live
DOMTokenList across external and list-driven rel mutations. Focused runtime
proof retains identity through setAttribute and add, reflects ordered tokens,
advances exactly one renderer-source generation per write, and recascades
[rel~="wide"]/[rel~="tall"] selectors to 140×30. A hidden real anchor keeps
the prior release/AOT baseline, dataset, and classList hashes unchanged; its rel
mutation becomes visible at 120×32, agrees with CDP attributes/geometry, and
pins exact Flutter pixels to
7ae6e6d8f650d733922b1af018dfdcac310bdcbb4f14537cdb20500c44da3c04 before
byte-identical target switching and renderer recovery. Sandbox tokens, inline
style, collections, and attribute nodes remain separate work.
Fourth A1 checkpoint: HTMLIFrameElement.sandbox now retains one live
DOMTokenList across external and list-driven sandbox mutations, completing
the three attribute-backed token-list identities currently hosted by the
runtime. Focused proof retains identity through setAttribute and add,
reflects valid ordered sandbox tokens, advances exactly one renderer-source
generation per write, and recascades token selectors to 140×30. A hidden real
iframe preserves all earlier exact hashes; allow-same-origin allow-forms
reveals a 120×32 box in the release/AOT corridor, agrees with CDP, and pins
Flutter pixels to
57b9814c22902e40fc38180d79a1a78068f1b15154f4149bef8fbea5b6cf05cb
before byte-identical target switching and renderer recovery. Inline style,
collections, and attribute nodes remain separate work.
Fifth A1 checkpoint: HTMLElement.style now retains one live inline
CSSStyleDeclaration across external style replacement and declaration API
writes instead of replacing its wrapper after each mutation. Focused proof
retains identity through setAttribute and setProperty, reflects current
declarations in both directions, advances exactly one renderer-source generation
per write, and recascades to 140×30. A hidden target preserves all prior exact
hashes; the release/AOT corridor reveals it at 120×32, matches its serialized
style and geometry through CDP, and pins exact Flutter pixels to
b4fe0e2cdba9f98193e8dfc7aadb7fa892e508e269a4a94beb9c2970d8ce5096
before byte-identical target switching and renderer recovery. Collections and
attribute nodes remain separate work.
Sixth A1 checkpoint: Element.attributes now retains one live
NamedNodeMap, with dynamic length/index/name lookup and stable attached Attr
identity across external writes. Attached Attr.value reads current state and
writes through the authoritative DOM mutation path. Focused proof retains both
identities through setAttribute and Attr.value, advances exactly one
renderer-source generation per write, and recascades to 140×30. A hidden target
preserves all prior exact hashes; the release/AOT corridor reveals it at 120×32,
agrees with CDP attribute/geometry state, and pins Flutter pixels to
17cb0de692001fcb97dcab23c870b800e7e7c3b09010e312a0bbc64e496ec1ea
before byte-identical target switching and renderer recovery. Detached Attr
lifecycle plus setNamedItem/removeNamedItem, and live structural collections,
remain separate work.
Seventh A1 checkpoint: live structural collection attributes now retain
resolver-backed identity while reflecting Page mutations: Node/Element
childNodes/children, document forms/images/links/scripts, form controls,
select/datalist options, labels, and table collections. Element/document
getElementsByTagName and getElementsByClassName return cached live
HTMLCollections; querySelectorAll remains a static NodeList as required.
Focused proof performs two structural writes, observes exactly one
renderer-source generation each, preserves collection identity/index/name
lookup, and proves a pre-mutation query list stays static. The release/AOT click
corridor retains empty collections before Flutter-routed input, observes the
rendered #dynamic.badge afterward through the same objects, matches the
authoritative CDP node, and keeps the pinned classList scene hash byte-identical.
Detached Attr operations and live CSSOM/script scheduling remain separate work.
Eighth A1 checkpoint: document.styleSheets now retains one live
StyleSheetList, each author <style> resolves to the same stable
CSSStyleSheet, and retained CSSRuleList, CSSStyleRule, and rule
CSSStyleDeclaration objects resolve refreshed BrowserCore CSS after an
external style-element mutation. The CSSOM resource refreshes even when a
same-task synchronous geometry query consumed the pending mutation before the
ordinary runtime drain. Focused proof retains every identity, advances exactly
one renderer-source generation, and observes Stylo's 140×30 result. The
release/AOT corridor retains the objects across all seven earlier stages,
changes one dedicated author rule, observes 120×32 synchronously and through
the retained CSSOM plus CDP, and pins exact Flutter pixels to
b09bce0ee8acf5ac3b40a2190241a6592880a3e47615c030469b2a887d118f1d
before target switching and byte-identical renderer recovery. CSS rule mutation
APIs and parser-module/task scheduling remain separate work.
Ninth A1 checkpoint: Document.createAttribute, detached Attr.value, and
NamedNodeMap.setNamedItem/removeNamedItem now complete the hosted attribute
lifecycle. Attaching preserves the supplied Attr identity, replacement and
removal return the prior object detached with its value intact, direct
removeAttribute detaches cached nodes, and attaching an Attr still owned by a
different element fails closed. Focused proof covers replace/remove/reattach/
external-remove transitions, Stylo recascade, and exactly one renderer-source
generation per actual mutation. The release/AOT corridor repeats replacement,
removal, reattachment, and in-use rejection in one retained map, observes
120×32 synchronously and through CDP, and pins exact Flutter pixels to
92181acffcd1e39ac9720c8edeeba2c148034a89f61297652dc948306f3af052
before target switching and byte-identical renderer recovery. Parser-module/task
scheduling and remaining plausible runtime shims are the next A1 boundary.
Tenth A1 checkpoint: parser-discovered inline and external ES modules now use
V8's native module parser/evaluator in the document realm. Modules defer until
parser classics finish, top-level await and exports execute, and each classic,
module, and document task receives its own microtask checkpoint. The document
task owner replaces Promise-backed timer shims with bounded timeout, interval,
animation-frame, cancellation, and post-load/automation pumps. CSP, mixed
content, response policy, cancellation, and stale document/runtime rejection
remain on the existing BrowserCore external-script boundary. Unresolved module
imports were left fail closed for A2's unified dependency loader. Focused
runtime and production-navigation proofs pin classic → microtask → deferred module → module
microtask/await → load → task ordering, task cancellation, one interval turn,
animation-frame delivery, post-load tasks, realm reuse after failure, exactly
one renderer-source generation for the module mutation, and external module
loading. The release/AOT fixture preserves every earlier exact hash, proves the
same parser order, reveals a module-owned 120×32 target synchronously and through
CDP, and pins exact Flutter pixels to
faa3c863350c742bdeb38338bca09307a4db49e6f7bb7a3f4e6d73eef60ae2fa
before target switching and byte-identical renderer recovery. The obsolete
non-page inert history object and fallback inert stylesheet object were deleted.
A1 exit: live Node/Element/Document mutations, author CSSOM objects, events,
focus, selection, forms, history, and profile/context-partitioned storage all
share the BrowserCore page realm and render-source path. Every mutation vertical
above proves authoritative Page state, explicit geometry invalidation, CDP
agreement, and Flutter pixels. Parser classics, modules, microtasks, and bounded
document tasks have production lifecycle ordering; cross-document navigation
retires the old realm, and two contexts retain isolated runtimes/session state.
Vixen still does not fabricate child-frame realms: contentWindow and
contentDocument remain null until A3 establishes same-origin access and
cross-origin wrappers, preserving the frame boundary without an inert fake.
Static module dependency graphs moved to A2's first loader checkpoint; broader
CSSOM/DOM/Web API surface remains compatibility breadth rather than an A1
ownership blocker.
Proof: script-driven mutation visibly changes the Flutter scene; synchronous and asynchronous geometry observe the right commit; CDP and page script inspect the same nodes.
A2. Unified loader and profile policy
Status: in progress (started 2026-07-18). Converge one resource family at a time without moving network or profile ownership into V8 or Flutter.
- Finish one resource loader for documents, scripts, styles, images, fonts, fetch/XHR, frames, and downloads with shared request ids, redirect/policy, cookies/cache, priorities, cancellation, and diagnostics.
- Complete streaming/abort/progress behavior and policy-before-renderer exposure.
- Integrate profile state, partition keys, cert/proxy/path/portal host services, and a real bounded download lifecycle.
First A2 checkpoint: parser-discovered inline and external ES modules now load nested static dependencies through the same bounded external-resource loader as parser scripts, stylesheets, and images. V8 discovers and evaluates the graph, while BrowserCore supplies shared numeric request ids, redirect and final-URL resolution, CSP/mixed-content checks, strict JavaScript response MIME, profile cookies and cache writes, bounded network diagnostics, and graph/event limits before source reaches V8. File and same-origin HTTP graphs execute in the persistent page realm; redirected roots resolve relative imports from the accepted final URL. Stop aborts the in-flight transport and rejects late module, DOM, cookie, cache, and lifecycle effects. Focused tests prove nested execution, cross-context profile cookies, cache records, distinct request ids, cross-origin fail-closed diagnostics, final-URL resolution, and transport disconnect on cancellation. The existing release/AOT Playwright fixture now imports a real dependency before producing the unchanged module-owned Flutter scene.
Second A2 checkpoint: static HTTP(S) graphs now enforce CORS for external
module roots and every dependency/redirect response before source reaches V8.
Cross-origin requests carry the serialized document Origin; default and
anonymous module graphs suppress cross-origin credentials and ignore response
cookies, while crossorigin="use-credentials" requires an exact allowed origin,
Access-Control-Allow-Credentials: true, and inherits credentialed behavior
through dependencies. Wildcard default graphs remain credentialless. Focused
BrowserCore tests prove allowed redirect/final/nested responses, missing-header
rejection without following the redirect or executing source, ignored default
cookies, credentialed root-cookie propagation, and stable lifecycle settlement.
Third A2 checkpoint: eligible exact-URL HTTP(S) module cache entries now
conditionally revalidate both external roots and graph dependencies through the
shared resource loader. Cached validators are added to live requests; only a
matching 304 restores bounded raw source bytes, while current URL/CSP,
mixed-content, CORS, status, and strict JavaScript MIME policy still run before
V8 exposure. Cache-disabled contexts perform neither module cache reads nor
writes. Entries with no-store, unsupported Vary, no validator, non-2xx
status, or bodies beyond the current resource limit are not reused. Focused
two-context tests prove root/dependency validator requests, raw 304 diagnostics,
source execution, persisted 200 representations, cache-disable bypass, current
CORS rejection, and strict MIME for external roots. Freshness-based reuse,
redirect aliases, full Vary, import maps, dynamic import(), and import
attributes remain explicit next work.
Fourth A2 checkpoint: one bounded parser-discovered inline import map may now
register before module discovery. The Deno-maintained import_map resolver
handles exact, prefix, URL-like, null-blocking, and most-specific scoped
imports/scopes mappings; each mapped URL still crosses the existing graph's
scheme, CSP, mixed-content, CORS, credentials, strict-MIME, cache, request-id,
cancellation, and diagnostics boundaries. Maps are capped at 256 KiB, 2,048
mappings, 128 scopes, and 16 KiB strings/URLs; recoverable parser diagnostics are
bounded before becoming runtime warnings. Import maps do not remap a module
script's src, and import.meta.resolve() uses the same frozen map. External,
multiple, late, integrity-bearing, malformed, or oversized maps fail closed with
stable script.import-map diagnostics and no partial registration. Focused file
graphs prove bare/prefix/base/scoped resolution; BrowserCore HTTP tests prove
numeric request ids, visible module mutation, and CORS rejection for a mapped
cross-origin target. Modern multiple-map merging/resolved-module-set behavior,
integrity maps, dynamic import(), and import attributes remain explicit next
work.
Fifth A2 checkpoint: dynamic import() originating in parser-discovered
page module graphs now extends the same retained graph instead of consulting a
mutable “last root” policy. Every specified and accepted-final module URL keeps
its original root's CSP, CORS credentials mode, import map, profile/cache path,
and shared request-id allocator. Static plus dynamic loads share the existing
64-load graph cap; per-graph and per-realm provenance maps are separately
bounded. Dynamic redirects register their accepted URL before child resolution,
and later module-owned functions/document tasks are driven to bounded event-loop
quiescence. Stop aborts tracked transport tasks, generation-checks profile
effects, suppresses stale DOM/cookie/cache/lifecycle effects, and rebuilds the
cancelled page realm before reuse. Focused tests prove delayed mapped file
imports, module-map single evaluation, cumulative graph limits, rejected import
attributes, credential policy retained after a different root runs, redirected
child resolution, cache records, transport disconnect, and clean subsequent
evaluation. The release/AOT Playwright corridor now evaluates one real dynamic
dependency without changing the pinned Flutter scene. Dynamic imports directly
authored by classic scripts or automation source remain fail-closed until those
scripts carry an exact URL and graph policy; import attributes, workers, modern
multiple-map merging, and integrity maps remain explicit breadth.
Sixth A2 checkpoint: page fetch()/XHR and parser-module HTTP(S) loads now
share one bounded private-cache decision module. The transport records the exact
effective final-hop request headers, including automatic compression, user-agent,
host, cookie, and body-length fields. Cache records retain at most 32 normalized
Vary names and their exact present/absent values; wildcard, malformed,
oversized, no-store, non-success, mismatched, and legacy Vary records are not
reused. Default requests reuse max-age freshness after accounting for Age,
stale/no-cache entries conditionally revalidate only when a validator exists,
forced cache modes retain their explicit behavior, and cache-disabled contexts
bypass reads and writes. Cached responses still cross current CORS, integrity,
status/MIME, graph provenance, and body-size policy before exposure. Focused
runtime tests prove a fresh exact-language variant performs one transport
request while a changed value refetches; a two-context module graph proves fresh
root and dependency reuse through the same profile cache. At this checkpoint
the URL-keyed store retained only the latest representation for a URL;
simultaneous variants,
Expires/heuristic freshness, request cache directives, and redirect aliases
remain explicit breadth.
Seventh A2 checkpoint: the shared HTTP transport now drains response bodies
chunk by chunk and checks the destination limit before extending its bounded
buffer, rather than allocating an unchecked complete body first. Stable
response/progress/completed events carry chunk, cumulative, optional total, and
final body bytes through BrowserCore, the C ABI, module diagnostics, and CDP.
Sub-quantum transport chunks coalesce into at most 256 progress records per
response before crossing those boundaries; CDP maps them to
Network.dataReceived/loadingFinished. Page Response.body is a real bounded
ReadableStream over the retained transfer chunks with one-shot bodyUsed
semantics; Blob streams use the same implementation. XHR emits typed upload and
download ProgressEvents with exact loaded/total values and preserves
headers-received → progress → loading/done → load/loadend ordering. A pre-aborted
fetch rejects with the signal's first reason and performs no transport. Focused
transport, runtime, XHR, CDP, module, and cancellation tests cover the new event
order and byte counts. The current text/cache/integrity pipeline still buffers
the bounded response before resolving fetch(): active page AbortSignal
cancellation and policy-safe response-before-completion streaming remain the next
loader boundary, while BrowserCore stop/navigation cancellation continues to
drop the live reqwest future.
Eighth A2 checkpoint: page fetch() now starts one host-owned asynchronous
request instead of holding V8 inside a blocking op. Each realm admits at most 32
active requests with opaque ids, one completion waiter, explicit cancellation,
and teardown cancellation. An active AbortSignal drops the pending reqwest
transport, rejects with the signal's exact first reason, and records a bounded
request/failure diagnostic; XHR owns a controller and send generation so
abort() drops the same transport and cannot publish late ready-state/load/error
events into a reopened request. Runtime stop and deadlines use a persistent
interrupt generation, so cancellation remains visible after V8 termination is
cleared and no partial cookie, preflight-cache, or response-cache effect can
commit. Focused stalled-peer tests prove fetch and XHR disconnect, exact reason,
terminal event order, and the existing stop/preflight-stop recovery corridor.
The Deno realm now retains one current-thread Tokio executor for async host ops
across evaluations rather than stranding op tasks on a per-evaluation runtime;
non-blocking shutdown keeps async CDP owner teardown safe. Responses still
resolved only after the bounded body, integrity, cache, and visibility decisions
completed; policy-safe response-before-completion streaming remained the next
transfer boundary at that checkpoint.
Ninth A2 checkpoint: the profile cache now retains simultaneous response
variants as independently bounded rows under a versioned URL-plus-selector key.
Canonical sorted Vary selectors preserve absent versus empty values, cap total
selected request-header data at 64 KiB, replace only the matching variant, and
continue counting every representation toward the existing 512-record global
eviction limit. Legacy URL-only rows remain readable and are transactionally
replaced on the next write. Page fetch/XHR and BrowserCore module/resource loads
select the newest matching usable variant through one shared decision before
rerunning current policy. Store tests prove two variants survive, selector order
does not create a duplicate, legacy migration works, and bounds count rows. The
runtime en → fr → en proof performs exactly two transport requests and
returns the first English representation on the third fetch. At this
checkpoint, Expires/request directives and redirect aliases remained the next
cache breadth.
Tenth A2 checkpoint: the shared cache decision now computes current age from
strict HTTP Date, Age, stored time, and resident time, then uses response
max-age or Expires for explicit freshness. Malformed max-age/Age is
stale, while invalid Date/Expires is ignored rather than guessed. Effective
request no-store bypasses
reuse and insertion; no-cache/legacy Pragma: no-cache, max-age, and
min-fresh force revalidation when required. Bounded or valueless max-stale
may reuse an expired response but cannot override response no-cache or
must-revalidate. Numeric overflow and conflicting duplicate directives do not
become permissive. Unit boundaries pin age/freshness equality, contradictory
max-age/Expires, malformed values, request constraints, and stale allowance.
An end-to-end page test performs one Expires-fresh cache hit, then sends author
Cache-Control: no-cache and proves validator/304 revalidation with exactly two
transport requests; a separate profile test proves request no-store persists
no representation. At this checkpoint heuristic freshness and redirect aliases
remained cache breadth.
Eleventh A2 checkpoint: permanent same-origin redirect aliases now retain
accepted final-URL identity without duplicating response bodies. A separate
profile table stores at most 512 aliases, each capped at 20 hops and 64 KiB of
targets; clear-data removes aliases with representations. Only complete 301/308
chains without Cache-Control: no-store whose hops remain in the original
origin are persisted. Temporary,
cross-origin, malformed, looping, over-limit, and policy-blocked aliases fall
back to live transport; direct or unsafe later responses invalidate the original
alias. Lookup revalidates URL/CSP/mixed-content policy on every target, computes
final-hop cookies and Vary headers, and reuses aliases only while the final
representation is fresh. Cached diagnostics replay request/redirect/response/
progress/completion with the accepted final URL and redirect count. A page
fetch() proof performs the initial redirect plus target requests, then repeats
the original URL with no transport while preserving Response.url and
redirected. A two-context module proof reuses the redirected root and relative
dependency from the profile, preserving final-URL import resolution and network
redirect events. Store and decision tests pin eviction, validation, deletion,
clear-data, legacy safety, and rejection of temporary/cross-origin aliases.
General redirect-response caching still requires retained redirect response
headers; this checkpoint deliberately does not treat 302/307 as permanent.
Twelfth A2 checkpoint: ordinary page fetch() responses now resolve at a
separate policy-accepted final-response head while one host worker retains the
body and terminal profile effects. Every live redirect target reruns URL, CSP,
and mixed-content policy; final same-origin/CORS visibility and filtered headers
are fixed before status, URL, or headers reach V8. Accepted raw chunks cross an
eight-message backpressured channel under the existing destination body cap and
32-request realm cap. ReadableStream reads and XHR loading/progress therefore
advance before transport completion, while terminal success waits for bounded
cookie/cache commit and preserves one request id through response, progress, and
completion. A serialized fetch-generation gate permits those commits while V8 is
idle between reads but rejects them after abort, stop, or deadline invalidation.
Abort after head exposure drops transport, removes ownership, emits one failure, and rejects pending or later body reads with the exact first JS
reason. CORS failure at the head cancels without reading the stalled body.
Integrity-bearing requests, conditional 304 revalidation, and opaque no-cors
responses remain buffer-before-resolution; active network-body clone/tee also
fails closed rather than creating an unbounded second consumer. Gated-peer tests
prove response-before-first-chunk, first-read-before-completion, stable split
diagnostics, post-response abort/disconnect, redirect-policy rejection before a
second request, final-head rejection, and integrity buffering.
Thirteenth A2 checkpoint: dynamic import() authored by parser classic
scripts and BrowserCore automation evaluations now enters the same retained
module loader with an explicit source URL and document policy. Inline classics
receive distinct document-base fragment identities and the import map available
at their parser position; redirected external classics execute under the
accepted final URL, so relative imports cannot fall back to the original
request. Automation uses the current document base, CSP/bypass decision, origin,
and retained import map. Source-only harness documents whose identifiers are not
absolute URLs continue ordinary evaluation but cannot create module provenance;
their dynamic imports fail before transport. Every admitted path receives same-origin module credentials,
shared profile/cache state and numeric request ids, the existing 64-load graph
cap, generation cancellation, response policy, and terminal diagnostics.
Focused file tests prove mapped classic and automation imports, while a
BrowserCore redirect test proves a classic dependency resolves from the final
URL and settles navigation under one numeric module request id. Import
attributes remain fail closed pending destination-specific response policy.
Fourteenth A2 checkpoint: static and dynamic JSON modules now admit exactly
with { type: "json" } through the existing graph loader. A V8 import-attribute
validation callback sees the complete static/dynamic attribute map and records a
bounded dynamic denial before resolution, so unknown keys and non-JSON types
cannot trigger transport even when deno_core later reduces attributes to a
requested module type. File loads require a .json URL; HTTP(S) loads require
application/json or a +json subtype. Accepted source remains under the same
URL/CSP/mixed-content/CORS, credentials, redirect, profile/cache, body, graph,
request-id, diagnostics, and cancellation policy as JavaScript modules before
V8 creates a JSON module namespace. Focused tests prove static and dynamic file
imports, reject an extra dynamic key without a third request, preserve isolate
reuse, and reject JavaScript MIME without a profile cache write. A BrowserCore
HTTP proof persists an accepted JSON representation and exposes one numeric
request id. Text/bytes/custom attributes and integrity metadata remain fail
closed.
Fifteenth A2 checkpoint: parser-discovered external classic and module roots
now retain their authored integrity metadata through the shared resource
request. BrowserCore verifies the strongest recognized SHA-2 candidate against
accepted raw response bytes after URL/CSP/CORS/status/MIME policy but before
UTF-8 conversion, V8 execution, profile cookies, or cache insertion. Mismatch
adds one stable integrity failure under the existing numeric request id,
leaves the document runnable for later scripts/lifecycle, and commits no response
profile effects. Unknown or malformed-only algorithms retain the existing SRI
no-metadata behavior rather than inventing a failure. Focused parser and
BrowserCore tests prove a SHA-384 module executes and caches, while a mismatched
classic neither executes nor exposes its response cookie/cache row. Import-map
integrity metadata for graph dependencies remains fail closed. Cross-origin
classic SRI uses the module-style CORS boundary: requests carry document origin,
anonymous mode omits cross-origin credentials, and source is rejected before SRI
unless the response grants access.
Sixteenth A2 checkpoint: the retained inline import map now owns at most
2,048 exact normalized-URL integrity entries beside the Deno-maintained resolver.
Absolute and URL-like relative keys resolve against the map base; non-object,
non-string, bare-relative, oversized, and normalized-duplicate forms reject the
whole map without partial registration. Static and dynamic JavaScript/JSON
dependencies select metadata only after import-map resolution, while an external
module root uses it as fallback only when no authored integrity attribute is
present. Accepted raw bytes pass URL/CSP/mixed-content/CORS/status/MIME policy,
then strongest-recognized SHA-2 verification before redirect graph publication,
V8, response-cookie commit, or cache insertion. Mismatch preserves the allocated
numeric request id, adds one terminal integrity failure, and leaves no module or
profile effect. Focused file proof accepts two mapped dependencies and rejects a
tampered one without execution. BrowserCore HTTP proof accepts and caches a
mapped root/dependency under separate numeric ids, while mismatch neither
executes nor exposes its cookie/cache row. Multiple-map merging remained a
separate follow-up trust boundary at this checkpoint.
Seventeenth A2 checkpoint: a document may now register up to 64 bounded
inline import maps before or after module discovery. Each source retains the
256 KiB parse cap; the merged normalized resolver plus integrity state is capped
at 512 KiB, 2,048 mappings, 128 scopes, and 2,048 integrity rows. Imports,
scope rules, and integrity entries merge first-wins; conflicts produce at most
32 bounded warnings plus one omission marker, and malformed or
cumulative-overflow maps leave the prior state unchanged. One shared successful-resolution set retains at most 2,048
(referrer, normalized specifier) records and 1 MiB of URL data. A repeated pair
returns its first successful URL even across immutable map versions, while merge
filtering prevents new global/scoped rules from affecting prior resolutions.
Parser-discovered static roots keep their parser-position snapshot. Dynamic
imports and import.meta.resolve() switch that graph to the latest document map
without changing its CSP, credentials, profile, request-id, cancellation, or
limit provenance; BrowserCore automation also receives the latest snapshot.
Focused tests pin first-wins imports/scopes/integrity, late-map static isolation,
late-map dynamic success, result stability, and every count/byte cap. A
BrowserCore HTTP graph requests only the first map's conflicted module plus the
second map's additive module, verifies both integrity entries, exposes numeric
request ids and bounded conflict warnings, and settles once. External maps remain
unsupported because HTML's import-map delivery format is inline.
Proof: multi-context profile tests, waterfalls, CORS/CSP/SRI/mixed-content/ cache profiles, cancellation races, safe download tests, and Linux host smokes.
A3. Renderer and frame model breadth
- Establish child-frame render mutations, same-origin access, cross-origin boundaries, sandboxing, nested viewport/scroll commits, and lifecycle teardown.
- Widen Flutter formatting only from reduced corridor/WPT failures; do not build isolated CSS helpers without a rendered commit consumer.
- Make animation/timers request bounded commits without starving BrowserCore or creating unbounded scene work.
Alpha exit gate
Alpha requires:
- one BrowserCore profile/context/document/runtime lifecycle;
- one Flutter mutation/commit renderer for GUI and rendered automation;
- two contexts that independently load, script, render, inspect, and share only intended profile state;
- active navigation/runtime/render work cancellable without stale commits;
- same-task DOM/style mutation driving correct synchronous geometry and visible pixels;
- input, scroll, find/selection, CDP, and accessibility naming exact commits; and
- reproducible architecture, compatibility, limitations, and measurements.
Beta — build a useful measured browser
B1. Rendering and content fidelity
Drive the Dart formatter from reductions and pinned profiles:
- common block/inline formatting, floats, positioned/fixed/sticky, overflow, flex, grid, tables, intrinsic sizing, replaced elements, fragmentation/print;
- responsive raster images, SVG basics, accepted web fonts, gradients, borders, shadows, transforms, opacity/compositing, filters, animation;
- typography, bidi/writing modes, fallback, line breaking, caret/selection; and
- browser-correct form-control rendering and interaction.
Prioritize typography, intrinsic sizing, tables, controls, and scrolling because they dominate real-page failures.
B2. Runtime and application basics
Widen live DOM, HTML, CSSOM, events, forms, navigation, URL/encoding/streams, timers, observers, messaging, WebSocket/EventSource, modules, workers, frames, sandboxing, and resource timing from corridor failures. Unsupported APIs remain explicit; inert shape does not count.
B3. Network, security, privacy, and downloads
Complete transfer streaming, upload/download progress, authentication/proxy, HTTP/2 interoperability, cache freshness, safe filenames/resume/history, Permissions Policy, COOP/COEP/CORP, HSTS, Trusted Types, partitioned state, private-network access, prompts, and failure classification.
B4. Daily-smoke Flutter product
Deliver robust tabs, address/search, reload/stop, history, find, zoom, downloads, permissions, error/recovery pages, session restore, settings/privacy controls, keyboard navigation, safe external opens, and host integration. Chrome remains a controller over BrowserCore; renderer state remains ephemeral and commit-bound.
B5. Automation and inspection products
Support independent targets/contexts, reliable waits, DOM/runtime handles, commit-aware input, downloads, dialogs, network/console/lifecycle events, Flutter-scene screenshots, permissions, and bounded traces. Drive additions from external Playwright workflows rather than method-name counts.
B6. Compatibility, performance, and reliability loop
- Expand pinned WPT profiles across parser, DOM/events/forms, CSS/layout/paint, network/security, storage/history, runtime APIs, and accessibility.
- Publish a controlled corridor spanning static content, docs, forms, downloads, app-like pages, and automation-heavy pages.
- Track startup, navigation, cascade/layout/paint/commit time, frame stability, memory, capture latency, throughput, install size, and profile growth.
- Bound malformed/content-controlled work and make renderer/runtime/profile recovery diagnosable.
Beta exit gate
The corridor loads in Linux GUI and chrome-less automation, supports meaningful interaction/persistence, survives restart/cancellation/renderer loss, and has published screenshots, reductions, profile counts, automation results, measurements, and known gaps. Other platforms remain committed targets until they pass their own gates.
v1.0 — honest daily-driver minimum
Vixen may call itself v1.0 when:
- common document, documentation, form, download, and app-like corridor pages are readable and usable with stable typography, images, layout, scrolling, interaction, navigation, and profile state;
- GUI and Playwright/CDP share BrowserCore and the Flutter renderer and recover predictably from network, document, runtime, renderer, and profile failures;
- supported security/privacy behavior is fail-closed and tested; single-process isolation limits are prominent;
- Linux install/update, certs, fonts, portals, downloads, GPU, settings, session restore, accessibility, and clear-data flows pass, and each additional platform claimed as supported by that release passes its native gate;
- compatibility, performance, memory, binary/install size, and unsupported capabilities are published from reproducible commands; and
- every claim maps to an acceptance gate, fixture/profile/smoke, and owner.
v1.0 is a useful supported subset, not the end of the replacement goal.
Platform expansion
After Linux R8 and beta-quality renderer stability:
- macOS and Windows: same render mutation/commit broker, native Flutter runner, fonts, input/IME, accessibility, host services, signing/packaging, capture, size, and performance evidence.
- Android: pinned V8 source/toolchain, lifecycle/process recreation, touch/IME, accessibility, host services, split-ABI packaging, capture, and resource budgets through the same renderer contract. A prewarmed builder is allowed only as a reviewed digest-pinned cache that exactly matches the Flutter/engine/JDK/API/NDK/Gradle pins and still supports reproducible Rust/V8 source builds.
- Apple Silicon iOS Simulator: same Flutter renderer, BrowserCore, V8 JavaScript/WebAssembly, simulated lifecycle/input/accessibility/host services, and reproducible Xcode runner. Physical iOS requires a new decision.
- WebAssembly: widen API and resource/conformance proof on every declared target without adding an alternate runtime.
Replacement horizon
After v1, prioritize by measured site/user impact:
- Accessible browser: complete semantics, screen-reader interaction, keyboard, caret/selection, forced colors, reduced motion, and native controls.
- Media: Flutter-compatible platform media integration, codecs, controls, tracks, fullscreen/PiP, autoplay/permissions, Media Source, and WebAudio.
- Offline applications: IndexedDB, Cache Storage, service workers, workers, file/blob streaming, notifications, installability, and offline lifecycle.
- Communications: production WebSocket/EventSource, WebRTC/device permissions, richer streaming/compression, and justified WebTransport.
- Graphics/documents: Canvas 2D, SVG breadth, WebGL/WebGPU, print/PDF, color management, advanced typography/writing modes, and CSS long tail.
- User ecosystem: scoped extensions, content blocking, password/autofill, import/export, developer tools, and policy controls.
- Defense in depth: renderer/content sandboxing, site isolation/OOPIF, brokered host access, crash containment, update/signing hardening.
- Broader compatibility: continuously widen WPT and the real-site corridor until exceptions are uncommon across supported targets.
Immediate execution queue
Work top-to-bottom and finish/document/commit each slice:
- Continue A2 request metadata: retain authored external-script/module referrer policy and fetch priority through roots, accepted redirects, static and dynamic descendants, cache requests, and bounded diagnostics. Apply the policy at the shared loader rather than synthesizing headers in V8.
- Preserve the R8/A1 corridors: keep real Mozc preedit/commit, native AT-SPI role/state/positive-local-bounds plus native-pointer focus → DOM → newer-commit evidence green while widening shared-core behavior; do not replace it with injected text or BrowserCore geometry. Restore an AT-SPI Action claim only after a newer immutable GTK4 engine provides it.
Do not reintroduce native layout/paint/frame ownership while stabilizing. A security, data-loss, or release-blocking regression may preempt the queue.
Velocity and deletion policy
- One renderer slice at a time. A critical BrowserCore fix may run beside it; adjacent feature breadth may not.
- Delete before adapting. If transitional code has no independent BrowserCore value and replacement evidence exists, remove it instead of adding compatibility layers.
- No speculative renderer framework. Start from the R3 fixture and generalize only when a second reduced case proves the need.
- One trust boundary per commit. Split protocol, ABI/broker, formatter, automation host, synchronous flush, and deletion at independently reviewable points.
- Use the test ladder once. Focused checks while editing, relevant gate before
commit,
just gate-pushonce for a coherent push batch. - Executable evidence beats prose. A commit advances only with DTO adversarial tests, fixture pixels/geometry, a race, a native smoke, or measured output.
- Update or delete gates with ownership. Tests that prove removed WebRender/ EGL/texture behavior disappear at cutover; tests of browser semantics move to the Flutter renderer rather than pinning old implementation details.
- Keep handoffs cheap. Update limitations and leave the next smallest queue item; completed queue prose is replaced, not accumulated.
Working rule
Every milestone lands with:
- one named authoritative owner and no parallel browser/renderer truth;
- exact revisions/commit ids across every renderer boundary;
- focused unit/adversarial tests plus one browser-visible fixture or smoke;
- stable bounded diagnostics at trust and lifecycle boundaries;
- compatibility/limitation updates when behavior changes; and
- the cheapest focused checks followed by the relevant hk/
justgate.
Prefer small, boring verticals. A large surface of plausible APIs is less valuable than one exact BrowserCore mutation becoming one Flutter commit observed by pixels, script, input, CDP, and accessibility together.