Vixen build plan
Phased execution runbook. Each phase ends in a green test suite, a working binary, and a measured size. Do not start the next phase until the previous one's gate passes.
For current focus, start with PROJECT_DIRECTION.md and
ROADMAP.md. This file is the historical phase runbook; avoid
adding broad status prose here when a concise ADR, roadmap entry, or code test is
enough.
Tick-tock discipline applies throughout: each phase is a tick
(capability lands); the post-phase cleanup is the tock (dead-code
removal, module ≤ 1 kLOC, references cited). See docs/ACCEPTANCE.md
for the per-phase gates.
For current delivery order use docs/ROADMAP.md; use
docs/MILESTONES.md only to map executable evidence to
just gate-* commands. New browser features should extend the shared browser
core/document path, not land only as isolated prep modules.
For larger alpha/dev batches, follow docs/DEVELOPMENT.md:
partial capability is acceptable only when it is visible, tested, fail-closed,
and bounded by a named maintainability follow-up.
Phase 0 — Scaffolding (≈ 3 days)
Create the workspace from docs/ARCHITECTURE.md. Empty crates with
stub lib.rs so the workspace compiles.
Steps:
- Workspace
Cargo.tomlwith all 7 crates as members. Rootsrc/main.rscallsvixen_shell::run()(which is a stub for now). vixen-apipopulated:Enginetrait,EngineDelegate(Send),EngineInspector,EngineProfile, DTOs,EngineDiagnosticshape — perdocs/ARCHITECTURE.md.vixen-shellskeleton:Appcomponent with emptyFactoryVecDeque<TabModel>and a placeholder window. Establish the Relm4 worker/factory patterns early per ADR-010 — the shell's idioms should be set in Phase 0, not retrofitted later.vixen-net,vixen-store,vixen-wpt,vixen-headless,vixen-engineall empty withpub mod placeholder;stubs.justfileadapted:check-all-hostbuilds the workspace;test-apiruns the API crate;gate-phase0bundles the phase's executable proof..gitignore,LICENSE(Apache 2.0),data/,build-aux/skeleton,fixtures/(empty),benches/(empty)..mise.tomlpins the dev toolchain (rust,just,cargo-binstall) somise bootstrap --yesconverges a fresh machine by delegating project work tojust setup. The library MSRV (1.88) is in each crate'srust-version; the developer toolchain is pinned in.mise.toml. The GNOME 50 SDK is not installed on the host — it is managed inside a flatpak-builder container (just flatpak-update-sdk/just flatpak-build); seedocs/guidance/gnome-sdk-flatpak-builder.mdandmise bootstrap.
Gate: just gate-phase0 passes (the workspace builds and vixen-api DTO /
trait tests pass). The shell's
empty App launches and renders an empty window.
Phase 1 — Networking and storage crown jewels (≈ 1 week)
Build the well-tested, fail-closed subsystems first. These are pure Rust with no upstream-crate dependencies.
Steps:
vixen-net/src/network.rs: reqwest + rustls HTTP client, HTTP/2, gzip, brotli, redirect handling, max body size, cookie header generation. Test surface: every error variant ofNetworkError.vixen-net/src/cookie.rs: RFC 6265 jar, every rule indocs/SPEC.md"Cookie contract". Test surface: every rejection rule, every outgoing-header rule, the 512-entry cap, FIFO eviction.vixen-net/src/url_policy.rs: blocklist perdocs/SPEC.md"URL policy", including the precise CGNAT check (100.64.0.0/10, not all of100/8).vixen-net/src/csp.rs: directive parser + enforcer perdocs/SPEC.md"CSP contract". Test surface: every directive, every source-list grammar element.vixen-net/src/permissions.rs,origin.rs,fetch_types.rs,http_helpers.rs: small supporting modules.vixen-store/src/lib.rs: redb-backed persistence, per-origin partitioning, schema perdocs/ARCHITECTURE.md"App ID and profile paths".
Shell session restore slice landed. vixen-shell::profile now uses the
app-ID scoped profile database to load/save vixen-store::SessionRecord for the
GTK shell. Startup falls back to the configured start page for an empty profile,
restores persisted tab URLs and active tab when present, and clamps/truncates
shell-written records to the store's bounded tab limits before persistence.
Gate: just gate-phase1 passes (vixen-net / vixen-store, just audit,
and the just fuzz-security 1 M iteration targets).
Phase 2 — JavaScript runtime (≈ 1 week)
Stand up the JS engine.
Steps:
deno_coreimplementation landed:deno_core/V8 powersvixen-engine::script::JsRuntimeand the Phase 2 eval gate.- Stable Vixen seam: keep the public
JsRuntime/JsValueseam so headless, CDP, and Page tests do not depend on runtime internals. - Host hook registration, minimum viable:
console.log,fetch(delegating tovixen-net::Network),document.titlegetter. Defer the full DOM/Event/Storage surface to Phase 6. - Host internals follow ADR-014: package Vixen-owned Web API bindings as
deno_coreextensions — small feature modules, explicit registration, local JS bootstrap, resource/permission boundaries, and focused tests per host family.
Gate: just gate-phase2 passes (basic engine tests and
vixen-headless --url file:///.../hello.html --eval '1+2' returns 3). Binary
size recorded.
Phase 3 — HTML parse + Stylo cascade (≈ 2 weeks)
Wire up HTML parsing and CSS cascade.
Steps:
html5everparse into RcDom. Already a dependency. Done — seevixen-engine::doc.- Selector matching via Stylo (done) —
vixen-engine::style_domimplementsselectors::Elementover the RcDom (a precomputedElementArenakeeps the moduleforbid(unsafe_code)). This powers--extract-selector, the WPT selector checks, and the:valid/:invalid/:checkedpseudos. Phase 3's gate (WPT CSS fixtures) now passes against the selector surface. The sharedvixen-engine::page::Pagefacade now owns URL + parsed document state for headless and WPT; cascade/layout/paint slices extend that facade in order. - Milestone 1 computed-style cascade projection (done) —
Page::computed_stylemaps the stable selectornode_idback to the element and returns a compact author/inline cascade.vixen-engine::style_cascadeloads<style>blocks, matches selectors through Stylo's selector engine, applies specificity, source order, cascade layers, media/supports conditions, custom-propertyvar()resolution, inherited custom properties, CSS-wide keywords, and author/inline!important, and keeps the WPTcomputed-stylecheck vertical behindPage.Page::evaluate_dom_expressionalso projects small CSSOM smoke seams forgetComputedStyle(document.querySelector(...)),CSS.supports(),document.styleSheets, and read-only CSSStyleRule/CSSStyleDeclaration state while full computed values and stylesheet host objects land. vixen-engine/src/style.rs(next slice): replace the compact projection with full Stylo style data: load<style>/<link rel=stylesheet>intoStylesheetlist →Stylist::update_stylist→ cascade via Stylo'sSharedStyleContext/ traversal. Exposecomputed_values_for(NodeId) -> Arc<ComputedValues>. Requires implementing the fullTNode/TElement/TDocumenttraits; budget 3–4 days for trait conformance. Consult.tmp/ref/firefox/dom/base/for DOM API behavior and.tmp/ref/firefox/servo/components/style/dom.rsfor the Stylo trait definitions being implemented.- CSS-wide keywords,
@layer,@supports,@media, and custom properties +var()are now covered in the compactPage::computed_styleprojection for the v1 layout/paint seam. Full Stylo still owns the long tail (@property,@import,@keyframes, shorthand expansion, full computed-value serialisation). Verify via WPT fixtures.
Pure-logic foundation landed (testing-strategy item).
vixen-engine::length implements CSS Values 4 <length> parsing + the
absolute/relative unit conversions the cascade and layout resolves against
(px/em/rem/%/vh/vw/vi/vb/vmin/vmax/sv*/lv*/dv*/
ex/ch/pt/pc/in/cm/mm/Q).
Rust-unit-tested per "Rust tests cover only pure logic (CSS length
arithmetic, …)".
The rest of the CSS Values 4 dimension family landed. <length> was
the first; the family is now complete for v1.0:
vixen-engine::color— CSS Color 4 sRGB family: 3/4/6/8-digit hex,rgb()/rgba()(legacy comma + modern space forms),hsl()/hsla()with hue normalisation, the 148 named colours,transparent/currentcolorkeywords, premultiplied-alpha arithmetic, and linear-sRGB interpolation (the primitive gradients and transitions reduce to).oklch/lab/lch/color()fail closed withUnsupportedColorSpace(deferred slice).vixen-engine::angle—<angle>(deg/rad/grad/turn) with degree/radian normalisation,cos_sin()for transforms and conic gradients.vixen-engine::time—<time>(s/ms) with millisecond normalisation for transitions/animations.vixen-engine::resolution—<resolution>(dpi/dpcm/dppx/x) with dots-per-pixel normalisation for media queries.xis the historical alias fordppx(CSS Images 4 § 7.3).vixen-engine::ratio— CSS Values 4 § 4.4<ratio>(number | number / number): the numerator/denominator pair with the quotient theaspect-ratioproperty and theaspect-ratio/device-aspect-ratiomedia features reduce to. A zero denominator is the § 4.4 "infinite ratio" encoding; the single-number shorthand meansN/1; the legacy Media-Queries-4 integerwidth/heightform folds in unchanged.
Each is #![forbid(unsafe_code)], mirrors the length parse/resolve shape,
and stays Rust-unit-tested (cascade/paint integration lands when Stylo /
WebRender plug in).
Note on Stylo sourcing. Stylo is now published on crates.io as
stylo (lib name style); the
"needs a Servo git dependency" caveat from earlier revisions of this
plan no longer applies. See ADR-011.
Gate: just gate-phase3 passes; fixtures/css/computed-advanced.html
proves the Milestone 1 cascade seam (@media, @supports, @layer, inherited
custom properties, var() fallback, and CSS-wide keywords) through the WPT
computed-style check. Full Stylo computed values remain the implementation
replacement behind the same Page facade (step 4), not a new public seam.
Phase 4 — Vixen-owned Rust layout (≈ 4–8 weeks for v1 subset)
Turn cascade output into a positioned box tree.
Steps:
- Build Vixen's Rust layout engine per ADR-013. The architecture reference is
Ladybird LibWeb at
0de15a5dd2a9, especially.tmp/ref/ladybird/Libraries/LibWeb/Layout/TreeBuilder.cppand.tmp/ref/ladybird/Libraries/LibWeb/Layout/*FormattingContext*. vixen-engine/src/layout_tree.rs: convert the Stylo-computed DOM into an arena-backed layout tree with stableLayoutNodeIds, explicit dirty bits, and no cross-crate pointers.vixen-engine/src/layout.rs/formatting_context.rs: run block, inline, flex, and grid formatting-context passes over that tree and produce positioned fragments.- Feed positioned fragments into the existing display-list builder; layout never owns a paint backend.
- Tables, floats, full vertical writing, page fragmentation, and advanced intrinsic sizing are post-v1 unless a WPT/real-site gate promotes them.
Implementation crate note. Keep the layout engine Vixen-owned, but use
small helper crates where they reduce risk without taking over web layout
semantics: smallvec for common-case child/fragment lists, bitflags for
dirty/invalidation state, slotmap/thunderdome if raw arena ids become
error-prone, and euclid when replacing ad-hoc geometry with typed units.
Defer text-specific crates (rustybuzz, fontdb, unicode-linebreak,
unicode-bidi, unicode-segmentation) until the inline formatting slice.
Do not use generic UI layout engines (taffy, stretch, etc.) for CSS layout
without a new ADR.
Vertical layout-tree + fragment slice landed. vixen-engine::layout_tree now builds
the first arena-backed Vixen layout tree behind Page::layout_tree, and
vixen-headless --dump-layout-tree exposes a deterministic dump. The first
block formatting-context slice consumes cascade-projected width/height,
margin, border-width/border, padding, and box-sizing through the
existing box_model resolver, so authored block dimensions now affect node
boxes. The existing Page::dump_lines projection derives visible text from the
tree instead of raw body text, keeping the line-layout and paint surfaces on the
same spine. Page::layout_fragments(viewport) now projects block backgrounds
and wrapped text lines from that tree into paint-consumable fragments; the
display-list builder consumes that seam instead of re-walking layout nodes.
Next slices replace the deterministic average-width text metric with styled
glyph fragments and enrich grid/inline placement without changing the CLI seam.
Gate: Visual-hash WPT check on 20+ fixtures matches reference
baseline within tolerance. Specifically, nested-flex/grid + padding +
margins + gaps must produce correct absolute coordinates without any
post-pass coordinate fixup. docs/COMPAT.md records the achieved WPT profile
for the shipped subset.
Pure-logic foundation landed (Phase 4 prep).
vixen-engine::box_model implements the CSS2 § 10.3.3 block-level
horizontal-constraint solve (auto-width leftover absorption, one/two
auto-margin distribution + centering, box-sizing: border-box content
subtraction) and the four-box nesting (margin ⊃ border ⊃ padding ⊃ content) the layout tree feeds off. Pure given cascade-resolved edges;
Rust-unit-tested per "Rust tests cover only pure logic".
Flexbox main-axis resolution landed (Phase 4 prep).
vixen-engine::flex_resolve implements CSS Flexbox 1 § 9.7 "Resolving
Flexible Lengths" end-to-end: the used-flex-factor selection (grow if items
under-fill, shrink otherwise), the inflexible-item freeze step, the
proportional free-space distribution (scaled by shrink × flex_basis for
the shrink case), and the iterative min/max-violation clamping that
terminates when every item is frozen. Pure given cascade-resolved
flex-basis + grow/shrink + min/max per item. Cross-axis alignment
and line packing stay in Vixen's formatting-context pass where they compose
against real text metrics.
CSS Grid track sizing landed (Phase 4 prep).
vixen-engine::grid_resolve implements CSS Grid 1 § 12.5 "Distribute
Extra Space" + § 11.7 "Maximize Tracks" — the natural complement to
flex_resolve for grid columns / rows. GridTrack carries the
§ 11.2 min track size (caller-resolved definite base) + the § 11.3 growth
limit + the Nfr flex factor; resolve_tracks distributes the
container's leftover to flex tracks proportionally to their flex factor,
freezes any track that hits its growth limit, redistributes the excess to
the remaining flex tracks (iterative, the same freeze-on-violation pattern
flex_resolve uses), then grows non-flex tracks up to their growth limits
equally when leftover remains (§ 11.7). The constructors (GridTrack::fr
for 1fr, GridTrack::minmax for minmax(min, max, fr),
GridTrack::length for fixed) cover the common authoring forms. Pure
given definite base sizes; content-based sizing (min-content/max-content/
auto) and multi-track spanning items stay in Vixen's formatting-context pass
where they compose against real text-shaping (the caller folds each spanning
item's contribution into the track base before calling).
Pure-logic foundation landed for CSS Writing Modes + logical properties (Phase 4 prep).
The writing-mode / direction → block + inline axis + the logical →
physical side mapping the box model, the logical insets, the logical-size →
width/height swap, and the flex/grid main-axis selection resolve against.
#![forbid(unsafe_code)], Rust-unit-tested.
vixen-engine::writing_modes— CSS Writing Modes 3 § 3 + CSS Logical Properties 1.WritingModeis the five § 3.1 values (horizontal-tb/vertical-rl/vertical-lr+ the CSS WM 4sideways-rl/sideways-lr);Directionis the § 2.1ltr/rtlinline-base direction.Flowbundles the pair and projects the derived geometry:Flow::block_axis/Flow::inline_axis(which physical axis each logical axis runs along) +Flow::block_start/Flow::block_end/Flow::inline_start/Flow::inline_end→PhysicalSide(the § 7 side mapping table, with thesideways-*reusing thevertical-*axis mapping per § 3.1).LogicalSize::to_physicalswapsinline/block→width/heightfor vertical modes;LogicalInsets::to_physicalresolves the four logical edges to(top, right, bottom, left);LogicalRect::to_physicalresolves a layout-produced logical rect to a physical(x, y, w, h)rect given the containing block (the rtl / vertical-rl inline-start flip from the right/bottom edge folded in). Theunicode-bidialgorithm + thetext-orientationglyph rotation stay in the text-shaping / paint path; this module is the pure axis + side mapping.
Pure-logic foundation landed for CSS Multi-column resolution (Phase 4 prep).
The column-width / column-count / column-gap § 3.4 resolution the
layout layer's column-row distribution reduces to. #![forbid(unsafe_code)],
Rust-unit-tested.
vixen-engine::multicol— CSS Multi-column Layout 1 § 3.ColumnWidth(autoor px) +ColumnCount(autoor ≥ 1) + theColumnSpec(column-width, column-count, gap)triple.ColumnSpec::resolveruns the § 3.4 pseudo-algorithm end-to-end: the four branches (both auto ⇒ single column; count set + width auto ⇒ even distribution; width set + count auto ⇒⌊(avail+gap)/(width+gap)⌋count; both set ⇒min(count, fit)+ the § 3.4 (11)–(12) single-column-authored-wider- than-available clamp), with a finalmax(0, width)guard so a too-large count never produces a negative column.ResolvedColumns::column_xis thei * (column_width + gap)stride the box model feeds off;ResolvedColumns::total_width+ResolvedColumns::overflowsreport the row geometry (the gaps-alone-overflow case). Thecolumn-gap: normal→1emlength resolution, the § 8column-fill: balanceheight balancing, thecolumn-rulepaint, andcolumn-span: allstay in Vixen's formatting-context / paint path (they compose against real text metrics).
Pure-logic foundation landed for CSS Scroll Snap (Phase 4 prep).
The § 5 snap-position computation + the scroll-snap-type axis/strictness
model the scroll container's snap targeting reduces to.
#![forbid(unsafe_code)], Rust-unit-tested.
vixen-engine::scroll_snap— CSS Scroll Snap 1 § 5.ScrollSnapType(noneor(axis, strictness); axisx/y/block/inline/both, strictnessproximity/mandatory, parsed in either order per the § 5.1 grammar) +SnapAlign(none/start/end/center, the 1–2 value(block, inline)form) +SnapStop(normal/always).compute_axisis the § 5 snap position for one axis: thestart ⇒ O,end ⇒ O + A − S,center ⇒ O + A/2 − S/2formula clamped to[0, max(0, overflow − S)].compute_snapproduces the(x, y)pair (the block/inline → x/y mapping via the writing-mode flow flag);should_snapis the strictness policy (mandatory always; proximity iff within a threshold). The scrollable-overflow computation, the scroll animation, thescroll-padding/scroll-margininsets, and the content-change resnap (§ 5.4) stay in the layout/input layers.
Phase 5 — Paint: WebRender + EGL surfaceless (≈ 2 weeks)
Make the engine produce pixels via a single WebRender paint path bound
to two GlContext implementations.
Steps:
-
vixen-engine/src/paint.rs: singleDisplayListtype + a WebRenderRendererthat consumes a&dyn GlContext(trait defined invixen-api, see ADR-006). One paint path; the twoGlContextimplementations are the only thing that varies between GUI and headless. -
GlAreaSurface(invixen-shell): implementsGlContextaroundgtk4::GLArea. Per the GTK4 idiom, GL work happens inside therendersignal callback, where GTK has already made thegdk::GLContextcurrent;proc_addressdispatches through Gdk's GL loader. This is the GUI path. -
SurfacelessSurface(invixen-headless): implementsGlContextviaEGL_MESA_platform_surfaceless(orEGL_KHR_surfaceless+ pbuffer fallback). Renders into an FBO;glReadPixelsextracts RGBA. This is the headless/CI path. -
Display-list builder enforces the invariants from
SPEC.md: z-index sorting, clip stacking (content clipped, borders not), opacity group multiplication, visibility skip-paint, background clip/origin/attachment.Invariant enforcement landed (pure slice).
vixen-engine::display_listimplements all eightSPEC.md"Display-list invariants" as auditable, individually-tested pure functions (z_tier,effective_opacity,background_paint_rect, …) plus aDisplayListBuilder::buildthat emits the pruned, z-sortedPaintCommandstream. The WebRenderRenderer(this step, next slice) consumes that stream; the invariant logic is done and Rust-unit-tested.Vertical display-list slice landed.
Page::display_listnow turns the Phase 4 layout fragments into the singleDisplayListBuildercommand stream: viewport background first, then fragment-backed backgrounds/text commands, exposed throughvixen-headless --dump-display-list.--paint-statsnow aggregates command counts and painted area from that same stream. This is not a renderer or CPU paint fallback; WebRender consumes the samePaintCommandstream once the GL surfaces land. -
vixen-shell/src/engine_factory.rs: creates thegtk4::GLArea, wraps it asGlAreaSurface(the shell'sGlContextimpl), and returns it as the content widget alongside the tab'sEngineWorker. The worker's engine renders to the screen via thatGlContext.Surface scaffolding landed.
vixen-shell::surface::GlAreaSurface(behindgtk-shell) andvixen_headless::surface::SurfacelessSurfacenow implementvixen_api::GlContext. Headless construction still fails closed withunsupported.screenshotuntil EGL context creation, WebRender command submission,glReadPixels, and PNG encoding land; no CPU fallback or second paint path was introduced. -
CI: verify
LIBGL_ALWAYS_SOFTWARE=1produces working screenshots viallvmpipeso headless runs anywhere.
Pure-logic foundation landed for radial + conic gradients (Phase 5 prep).
The CSS Images 4 § 4.2.3 + § 4.3.3 colour-sampling siblings of gradient,
completing the three gradient families the paint path samples against. All
three #![forbid(unsafe_code)], Rust-unit-tested, reusing the
crate::gradient::resolve_stop_positions + linear-sRGB interpolation the
linear-gradient surface already owns.
vixen-engine::radial_gradient— CSS Images 4 § 4.2.3radial-gradient.RadialShape(Circle/Ellipse) +RadialSize(the four § 4.2.4 keywordsclosest-side/farthest-side/closest-corner/farthest-corner- the explicit
Length/LengthPairforms, withfarthest-cornerthe spec default).compute_radiusis the § 4.2.4 radius-resolution step for one of the four keyword forms against a known(width, height)reference box centred at(cx, cy), returning(rx, ry)so circle + ellipse share the call site (theclosest-corner/farthest-cornerellipse cases keep the closest-side/farthest-siderx/ryratio and scale to the corner per the § 4.2.4 corner-scaling rule).project_to_tis the per-pixel(dx, dy)→tdistance projection (Euclidean for circle, ellipse-norm for ellipse).RadialGradient::sampleis the colour at a projectedt(with therepeating-radial-gradient()wrap via the sharedsample_resolvedhelper). The<position>centre and the<geometry-box>reference-box resolution stay in the layout/paint layer; this module receives(cx, cy, width, height)already resolved.
- the explicit
vixen-engine::conic_gradient— CSS Images 4 § 4.3.3conic-gradient.ConicGradientcarries the stop list + thefrom <angle>start angle (radians) + therepeatingflag.project_angle_to_tis the per-pixel(dx, dy)→t ∈ [0, 1)projection (CSS-clockwise from 12 o'clock, in turns — one full revolution =1.0);add_from_anglefolds in thefromangle and reduces modulo 1.0.ConicGradient::sampleis the colour at a projectedt(with therepeating-conic-gradient()wrap). The<angle>grammar + the<position>centre stay in the cascade / layout layer.
Pure-logic foundation landed for CSS Geometry Interfaces (Phase 5/6 prep).
The DOMPoint / DOMRect / DOMQuad / DOMMatrix value family the
geometry-bearing host hooks reduce to. #![forbid(unsafe_code)],
Rust-unit-tested, complementing crate::transform (which owns the 2D
subset of the matrix surface).
vixen-engine::geometry— CSS Geometry Interfaces L1.DOMPointis the 2D/3D/homogeneous(x, y, z, w)point (§ 2; the perspective divide normaliseswto1when projecting).DOMRectis the(x, y, width, height)rectangle (§ 3) with the derivedtop/right/bottom/leftaccessors + the negative-dimensionDOMRect::normalizedflip + thecontains_point/intersects/unionpredicatesgetBoundingClientRect()+IntersectionObserverconsult.DOMQuadis the four-corner quadrilateral (§ 4) with thefrom_rectconstructor + theDOMQuad::boundsaxis-aligned bounding rectangle (§ 4.4).DOMMatrixis the § 6 4×4 homogeneous matrix (the 2Dmatrix(a,b,c,d,e,f)subset folds into the upper-left 2×3 + the[0 0 1 0]/[0 0 0 1]bottom rows) with every § 6.3 transform (translate/scale/scale_non_uniform/rotate/rotate_axis_angle/skew_x/skew_y/multiply/flip_x/flip_y/inverse) + the § 6.4DOMMatrix::transform_pointhomogeneous-coordinate projection + theis_2dpredicate + theto_4x4_column_majorround-trip. Matrix decomposition / interpolation (the CSS Transforms 2 § 16 pipeline the animation interpolation layer reduces to) and the fulltransformproperty parser land with the 3D WebRender plumbing; this module is the arithmetic those slices reduce to.- The runtime DOM host now exposes the first geometry host seam:
Element.getBoundingClientRect()returns the Page layout box as a DOMRect projection (x/y/width/height/left/top/right/bottom),getClientRects().lengthreports whether layout produced a box, client/offset/scroll metrics read the same rect,getBoxQuads()projects aDOMQuadfrom that box, and Range rectangles reuse the same page-backed geometry seam. It also projects the read-only Geometry Interfaces value constructors (DOMPoint,DOMRect.fromRect(),DOMQuad.fromRect()/getBounds(), andDOMMatrixtransform/transformPoint()smoke) until real JS host wrappers replace the string projection.
Gate: just run shows a real web page in the window.
vixen-headless --screenshot out.png --url fixtures/css/border-rendering.html
produces a PNG matching the GUI's render within 1 % pixel diff on 5
fixtures (both renders going through the same WebRender paint path).
Paint-geometry pure-logic foundations landed (Phase 5 prep).
vixen-engine::transform— CSS Transforms 1 § 13 2D affine algebra:translate/scale/rotate/skew/matrix,multiplycomposition (post-multiply ⇒ rightmost-applied-first, matching Firefox/Servo),apply_point/apply_rect(AABB),determinant/inverse, plus aparse_transformlist parser for the--computed-styleprojection. Consumesvixen-engine::angleso the full angle unit grammar is shared.vixen-engine::border_radius— CSS Backgrounds 3 § 5.5 corner shaping: the eight authored radii → four shaped corners with the proportional scale-down when adjacent radii overflow a side. Pure given px radii + px sizes; the cascade resolves percentages first.vixen-engine::gradient— CSS Images 4 § 4.5 linear-gradient colour sampling: stop-position normalisation (first/last defaults, even auto-distribution between positioned anchors, monotonicity fix-up, unit- interval clamp), linear-sRGB interpolation between stops (viacrate::color::interpolate), and therepeating-linear-gradient()wrap that tiles the colour function. Angle / direction → gradient-line geometry stays in the paint path.vixen-engine::box_shadow— CSS Backgrounds 3 § 7.2box-shadowgeometry: the<shadow>#grammar parser (offset / blur / spread / colour /inset, the paren-respecting colour-function tokeniser, negative-blur clamping) + the per-shadow paint-rect arithmetic (outer_paint_rectfor display-list culling;inset_clip_rectfor the inset "hole" with the spec's spread-sign-flip + blur-shrinks-hole rule). Pure given px values; the cascade resolves percentages /emfirst.vixen-engine::background_position— CSS Backgrounds 3 § 3.6 + § 4.2<position>resolution: the four-value grammar (1/2/3/4 forms, keyword / length / percentage mix), the keyword-axis swap rule (top right≡right top), and the § 4.2 formula(container − image) * fraction + offset. Pure given px sizes; the cascade resolves thebackground-origin-selected container size first.vixen-engine::stacking_context— CSS 2.1 § 9.9.1 + CSS Positioned Layout 3 § 6 + CSS Compositing 1 § 3 stacking-context formation predicate + the seven-layer § App. E.2.1 paint-order classification (classify_descendantslots each descendant into one ofContextBackgroundAndBorders/NegativeZChildren/InFlowBlockLevel/NonPositionedFloats/InFlowInlineLevel/PositionedZeroZ/PositiveZChildren, in bottom-to-top paint order). Composes withdisplay_list::z_tierfor the coarse z-bucketing and gives the paint pass the fine-grained in-flow layering the CSS 2.1 appendix specifies.
All six #![forbid(unsafe_code)], Rust-unit-tested, ready for WebRender to
consume once the display-list builder feeds them in.
Paint compositing pure-logic foundations landed (Phase 5 prep).
The pixel-mixing family the paint path's mix-blend-mode / filter /
border-image surfaces reduce to. All three #![forbid(unsafe_code)],
Rust-unit-tested, consuming vixen-engine::color's linear-sRGB arithmetic.
vixen-engine::blend— CSS Compositing 1 § 5 + § 10: the 13 Porter-Duff compositing operators (blend::CompositingOperatorwith the § 5.1 general formula + per-operator Fa/Fb factors) and the 16 § 10 blend modes (blend::BlendMode—normal+ 11 separable § 10.1 + 4 non-separable § 10.2, with theSetLum/SetSat/ClipColorhelpers).blend::compositeevaluates one operator;blend::blendapplies one mode to a pixel;blend::composite_blendruns the § 5.2 combined pipeline (isolation blend against the backdrop, then the Porter-Duff operator) thatmix-blend-modeactually performs. All arithmetic is in linear sRGB viablend::LinColor(reusingcolor::Color::to_linear_f32).vixen-engine::filter— CSS Filter Effects 1 § 5: the<filter-function- list>grammar + the per-pixel colour-matrix family.filter::FilterListparses a chain (tolerant of parenthesised-argument whitespace); the 10 § 5 functions (blur/brightness/contrast/drop-shadow/grayscale/hue-rotate/invert/opacity/saturate/sepia) carry their § 5 default-argument rules. The per-pixel family folds into onefilter::ColorMatrix(SVGfeColorMatrix-shaped 4×5) viafilter::compose_color_matrixso the paint path runs a single matrix multiply per pixel;blur/drop-shadowkeep their geometry for the paint path's spatial pass (drop-shadowreusesbox_shadow::BoxShadow).vixen-engine::border_image— CSS Backgrounds 3 § 6: the four longhands (border-image-slice/-width/-outset/-repeat) with full 1–4 TRBL expansion + parse, the 3×3 nine-region carving (border_image::source_regions/border_image::destination_regions), and theborder-image-repeattiling primitive (border_image::tile_edge—stretch/repeat/round/space, with theroundinteger-count rescale and thespaceeven-gap distribution).
Pure-logic foundation landed for clip-path + mask (Phase 5 prep).
The masking family the paint path's per-pixel clip + the masked-element
alpha/luminance sampling reduce to. Both #![forbid(unsafe_code)],
Rust-unit-tested, consuming crate::border_radius + crate::blend.
vixen-engine::clip_path— CSS Masking 1 § 5clip-pathbasic shapes.ClipPathis the typed family (ClipPath::Inset/ClipPath::Circle/ClipPath::Ellipse/ClipPath::Polygon/ClipPath::None);Coordis theat <position>coordinate (px / percent / keyword) withCoord::resolveagainst a reference box;GeometryBoxis the<geometry-box>reference selector.parse_clip_pathparses the four basic shapes (case-insensitive function name, parenthesised args, theinset(… round <radius>)form reusesBorderRadius, thepolygon(<fill-rule>, …)form carriesFillRule::NonZero/FillRule::EvenOdd).ClipPath::containsis the point-in-shape test the paint path calls per pixel — the inset corner rounding via quarter-ellipse containment, the polygon winding rules (non-zero + even-odd, the SVG § 8.4 ray-crossing algorithm). Thepath()SVG-path form is deferred (the four geometric shapes cover the common HTML surface).vixen-engine::mask— CSS Masking 1 § 6maskshorthand per-layer model.MaskMode(alpha/luminance/match-source),MaskRepeat(the 6 repeat styles,repeat-x/repeat-ycollapsed),MaskBox(the sharedmask-clip+mask-originkeyword set,no-clipclip-only), andMaskLayer(one layer's resolved longhands).parse_masksplits comma-separated layers (paren-aware, so a gradient's commas don't split a layer), fills the per-longhand slots in any order, recognises the<position> / <size>slash form, and applies the "first unrecognised token is the image source" rule. The mask-image fetch + the per-pixel sampling is the paint path.
Pure-logic foundation landed for the Web Animations timing model (Phase 5 prep).
The § 5 timing-model pipeline the CSS transition / animation drivers +
the Animation / KeyframeEffect host hooks reduce to.
#![forbid(unsafe_code)], Rust-unit-tested, consuming crate::easing.
vixen-engine::animation— Web Animations § 5.EffectTimingcarries the § 5.4 timing properties (delay/end_delay/fill/iteration_start/iterations/duration/direction);Fillis thenone/forwards/backwards/bothfill mode;PlaybackDirectionis thenormal/reverse/alternate/alternate-reversedirection.active_duration+end_timeare the § 5.3 derived times;phaseis the § 5.5before/active/afterclassification;simple_iteration_progress+current_iterationare the § 5.5 iteration progress + index (the after-phaseiterations = 0and integer-boundaryprogress = 1rules folded in);directed_progressis the § 5.6 direction-aware progress;apply_easingis the § 5.7 transformed progress (consumescrate::easing::Easing);compute_timingties the pipeline together into aComputedTimingwith the fill-mode before/after resolution (backwards/both ⇒ the iteration-0 start in before; forwards/both ⇒ the end state in after; elseNone). The keyframe value interpolation + the animation-frame scheduling + theautoduration resolution stay in the paint / event-loop layer (this module produces theprogressthey sample at).
Phase 6 — Host bindings (≈ 2 weeks)
Register the DOM/Event/Storage/Network host hooks the modern web needs. Priority order:
- DOM Core:
document,Node,Element,HTMLElement, attribute accessors,querySelector*,getElementsByTagName,classList,dataset. - Events:
Event,EventTarget,addEventListener,removeEventListener, dispatch, capture/target/bubble, focus/click/ input/submit/change,composedPath(), composed event dispatch order perdocs/SPEC.md. - Forms:
HTMLInputElement,HTMLFormElement,HTMLSelectElement,HTMLTextAreaElement,HTMLButtonElement,ValidityState(11 flags perdocs/SPEC.md),checkValidity,reportValidity,setCustomValidity, form submission algorithm. - Storage:
localStorage,sessionStorageagainstvixen-store, per-origin partitioning. - Network:
fetch,XMLHttpRequest,Request/Response,Headers,URL,URLSearchParams,TextEncoder/TextDecoder.
Each family lands with its WPT fixtures passing before moving on.
Pure-logic foundation landed for Events + Forms + Storage (Phase 6 prep).
vixen-engine::event_path—composedPath()(shadow-boundary aware via thecomposedflag) and the focus-transition orderingfocusout → focusin → blur → focus(bubbling flags per SPEC). The host-hook layer invokes these; the ordering is done and unit-tested.vixen-engine::date_units— the date/time canonical-unit parser (forms.rs"lives indate_unitsuntil a proper parser lands" → landed):date/time/week/month/datetime-local→DateTimeUnit, sostepMismatchis now testable end-to-end over real input strings.vixen-engine::storage_key— Web Storage key/value validation (non-empty key, no NUL bytes, ≤MAX_KEY_LEN/MAX_VALUE_LEN) + the(origin, kind)StoragePartitionkey thevixen-storeredb tables partition under, plus the per-partitionStorageQuota(5 MiB / 8 192 entries) the host hooks reportQuotaExceededErroragainst.Page::evaluate_dom_expressionnow projects the read-only document/navigator state shape (readyState,compatMode, visibility,documentURI/baseURI, focus/active element, viewport/window/screen state, language/userAgent). The runtimeStoragehost object now crosses explicitdeno_coreops for in-memorylocalStorage/sessionStoragemutation through the same storage-key validation and quota boundary the persistent host object will use.navigator.permissions.query(), Notification permission reads, and StorageManager persisted-state checks now cross an explicit permission op that reads persistedvixen-store::PermissionRecorddecisions by origin, returningprompt/defaultfor unknown decisions and rejecting unsupported names;navigator.storage.estimate()reports bounded local-storage usage.JsRuntimenow owns a persistent realm, so sequential evals retain globals, storage, and promise/event-loop state until the caller switches between non-page/page realms or navigates to a new page snapshot. The runtimefetch()MVP now crosses an explicit op intovixen-net, returns real HTTP(S)Responsestatus/headers/text body, and rejects private/reserved hosts through the shared URL policy before I/O. The DOM query seam now also covers core node/ancestry properties (nodeName/nodeType,isConnected,ownerDocument, andElement.closest()) before full Node / Element JS host wrappers replace the string projection.vixen-engine::script::JsRuntime::evaluate_with_pagenow installs the firstdeno_coredocumentsnapshot host-object seam for focused evals:document.title,document.body.textContent, simplequerySelector/getElementByIdelement properties and attributes, andquerySelectorAll().length, plus read-onlyDOMTokenList(classList/relList/sandbox) andDOMStringMap(dataset) property reads. The remaining broad DOM smoke surface still fails closed throughPage::evaluate_dom_expressionuntil each family moves behind real wrappers.Page::evaluate_dom_expressionnow projects constructor smoke forEvent/CustomEventand elementdispatchEvent(new Event(...))so event-object and EventTarget wiring has fixture coverage before listener queues and full capture / bubble dispatch land.vixen-engine::form_submission— the three WHATWG HTML § 4.10.21 form- submission encoders (application/x-www-form-urlencoded,multipart/form-data,text/plain) plus theFormEntry/FormEntryValuedata model +FormEnctypeselector. The URL-encoder uses the URL Standard's space→++ uppercase-hex percent-encoding; the multipart encoder handles RFC 7578 § 4.2Content-Dispositionquoting +filename+Content-Typeper part, with CRLF discipline; the boundary generator is RFC 2046-capped.Page::evaluate_dom_expressionnow reuses the same form entry-list builder for a read-onlyFormData(form)smoke seam (get/getAll/has, iterator first-entry shape, plus filename/type/size). Runtime/CDP submit actions now call the same Page-backed entry-list by stable node id, so idless forms, successful submitter entries, and submitterformaction/formmethod/formenctypeoverrides share the navigation path. Runtime form reset restores default value/checked/selected state and honors cancelableresetevents; mutableFormDataremains a narrow in-realm helper until full file/body plumbing lands.vixen-engine::dataset— WHATWG HTML § 3.2.6.9data-*attribute ↔ dataset property-name bidirectional mapping (deserialise, serialise, collect), with the anti-collision rule (-followed by uppercase ⇒ not exposed).Page::evaluate_dom_expressionand the transitionaldocumentsnapshot now use that same dataset mapper for the read-only smoke surface (element.dataset.fooBar/ bracket access), proven byfixtures/dom/dataset.htmljs-evalchecks while the WPT adapter continues to use the Page projection.Page::evaluate_dom_expressionalso projectsValidityStateflags,willValidate, andcheckValidity()/reportValidity()from the pure forms module for fixture smoke coverage until the Phase 6 form host objects land.Page::evaluate_dom_expressionprojectsElement.innerHTML/outerHTMLgetters throughvixen-engine::html_serialize, proving the HTML serialisation host-object seam with WPTjs-evalchecks before mutation setters and Trusted Types enforcement land.Page::evaluate_dom_expressionalso reflects simple security-relevant element properties (HTMLMetaElement.content/.charset) so CSP/referrer meta fixtures cover the DOM host seam before Phase 7 enforcement consumes those declarations.fixtures/forms/validation.html— exercises every form pseudo-classstyle_domresolves today (:checked/:disabled/:enabled/:required/:optional/:read-only/:read-write) plus the Page-backed validity eval seam; wired intofixtures/manifest.json.fixtures/dom/dataset.html— exercises the canonicaldata-foo-bar→fooBarsurface the host-hook layer will reflect; wired intofixtures/manifest.json.fixtures/forms/submission.html— fixes the form-DOM input shape the three encoders andFormDataprojection walk; wired intofixtures/manifest.json.
Pure-logic foundation landed for the DOMTokenList surface (Phase 6 prep).
vixen-engine::class_list— WHATWG HTML § 4.6.4DOMTokenList+ the § 2.7.3 "ordered set of unique space-separated tokens" parser + validator (empty ⇒SyntaxError; ASCII-whitespace-bearing ⇒InvalidCharacterError). The full mutating surface (add/remove/togglewith theforceparameter /replacewith the drop-old-if-new-already-present edge case /contains/item/iter/serialize) with the spec's atomic validate-then-mutate rule (any invalid token in a multi-tokenadd/removeaborts the whole call without partial mutation). The optionalSupportedTokensset is the surface<link>.relList.supports(token)consults (the onlyDOMTokenListwith a supported-tokens set per WHATWG § 4.6.5).fixtures/dom/class-list.html— exercises the canonical classList patterns the host-hook layer reflects (duplicate-token collapse, whitespace-run collapse, the case-sensitiveFoo/foo/FOOdistinction, the multi-value<link rel>form) and now asserts the read-onlyclassList/relListeval seam; headless/CDP focused evals run through the current JS runtime seam while the WPT adapter continues to use the Page projection; wired intofixtures/manifest.json.fixtures/security/sandbox.htmlnow also asserts the Page-backediframe.sandboxDOMTokenList projection (length/contains/item), with focused headless/CDP evals now backed by runtimeDOMTokenListsnapshots, before real framed-document sandbox enforcement consumes the same tokens.
Pure-logic foundation landed for Network host hooks (Phase 6 prep).
vixen-engine::url_search_params— WHATWG URL StandardURLSearchParams(§ 5.2 parse + § 5.3 serialize) plus the full mutating surface (get/getAll/has/has_pair/append/set/delete/delete_pair/sort/entries/keys/values) thenew URLSearchParams()JS host hook reflects. The parser handles leading-?stripping,+→SPACE, percent-decode with U+FFFD on ill-formed UTF-8, and empty-tuple dropping; the serializer shares theapplication/x-www-form-urlencodedbyte set withform_submission::encode_urlencoded(kept separate because the specs are).Page::evaluate_dom_expressionnow exposes a read-onlyURL.canParse()/new URL()/URLSearchParamssmoke seam (href/origin/components /toString()/searchParams, record-list constructor, two-argumenthas, and first iterator entry/key/value shape) fromwhatwg_url+url_search_params, proven byfixtures/network/url-parsing.html.vixen-engine::mime— WHATWG MIME Sniffing § 2.1MimeType::parse+ § 2.2serialize+ theessence()accessor. Tolerant whitespace + case handling, quoted-string parameter values (RFC 9110 § 3.2.6 backslash-pair escaping), first-occurrence-wins on duplicate parameter names. Every network layer (Content-Type),fetch()/XHR(.type/overrideMimeType), and<object>/<embed>plugin negotiation consults this one parser.vixen-engine::text_codec— WHATWG Encoding API (TextEncoder+TextDecoder).encode_intoreports UTF-16-code-unitread+ bytewrittenwithout splitting a scalar value; the Page seam and the current runtime host-constructor pilot both parse theTextDecoderconstructor label/options dictionary (fatal,ignoreBOM);decodedoes the BOM sniff (ignoreBOMopt-out), thefatal-flag UTF-8 validation, the WHATWG § 4.6 one-U+FFFD-per-maximal-subpart replacement (viafrom_utf8_lossy, which agrees with the WHATWG count), and the § 7.1CRLF/lone-CR→LFline-break normalisation. v1 ships UTF-8 only; unknown labels fail closed.- The same fixture set now asserts the Page-backed
TextEncoder/TextDecoder,atob/btoa, andDOMParser.parseFromString(..., "text/html")smoke seams (encoding, encoded byte length,encodeIntoread/written,TextDecoderlabel/options, decode, base64 round-trip, parsed document query) whilevixen-headless --eval/ CDPRuntime.evaluatenow exercise op-backed runtimeTextEncoder/TextDecoderconstructors plus focuseddocument/Element/ read-onlyDOMTokenList/DOMStringMapsnapshot extension objects backed by the same Page data.script::webidlnow renders the first generated browser interface/prototype substrate for the runtime-visible DOM/CSSOM/geometry subset; DOM and CSSOM bootstraps adopt those generated prototypes instead of hand-rolling constructor shape. The DOM snapshot data now crosses thedeno_coreop boundary viaop_vixen_dom_snapshot; selector lookup andElement.matches()now use finer-grained DOM ops, and element record data is loaded throughop_vixen_dom_element_snapshot. Element text/attribute reads plus read-only DOMTokenList/dataset data now use focused DOM ops, and elementgetBoundingClientRect()/getClientRects()/getBoxQuads()geometry reads plus client/offset/scroll metrics now cross a focused DOM rect op. FocusedCSS.supports,getComputedStyle, and CSSStyleSheet/CSSRule smoke evals now use the op-backedscript::cssomextension, leaving imported full WebIDL manifests, Geometry Interface value constructors/forms/events/history/storage/fetch as the next host-object replacement targets.
Pure-logic foundation landed for the fetch host-hook data model (Phase 6 prep).
The Headers object, Blob/File metadata, read-only Request/Response
state, static Response.error() / Response.redirect(), and
AbortController/AbortSignal primitives are the sync data surfaces the
fetch() / XMLHttpRequest / streaming host hooks reduce to. The pure modules
remain #![forbid(unsafe_code)], Rust-unit-tested.
vixen-engine::headers— Fetch § 3.2.2Headersobject data model:validate_header_name(RFC 9110 § 5.5token+ lowercasing) +validate_header_value(OWS trim, NUL/CRLF rejection, code-point-≤ U+00FFgating); the § 3.2.2 forbidden predicatesis_forbidden_request_header(the exact 21-name list + theproxy-/sec-prefix rules the Request constructor strips) +is_forbidden_response_header_name(set-cookie/set-cookie2); the § 3.2.1.2 CORS-safelist predicateis_cors_safelisted_request_header(theAccept/Accept-Language/Content-Language/Content-Type(+Range) family with the value-byte cap, the CORS-unsafe-byte gate, and the MIME-essence +Rangegrammar checks); and the normalisedHeadersstore (append/set/get/getAll/delete/has + comma-combine on read + byte-order + insertion-order iteration).vixen-engine::abort— DOM § 8.1AbortController/AbortSignal: theaborted+reasonvalue model (default reason ="AbortError"DOMException),AbortController::abort(idempotent, first-reason-wins),abort_any(§ 8.1.3.2AbortSignal.any()snapshot — aborted iff any input is, taking the first-aborted input's reason; reactive propagation is the host-hook event-loop layer's job), andTimeoutSignal(§ 8.1.3.2AbortSignal.timeout(ms)request record with the zero-delay-aborts- synchronously rule).Page::evaluate_dom_expressionnow projects read-onlyHeaders,Blob/File,Request,Response,AbortController/AbortSignal, andURLPatternsmoke seams from these pure modules (get/has, iterator shape, forbidden-header filtering in Request/Response init, byte-size/type/name/ method/status/header state,Response.json(), timeout/any snapshots, pathname test/exec groups) while full streaming/upload/XHR routing host objects land.vixen-engine::script::webapinow exposes a minimalfetch(input, init)host function. It normalizes throughRequest, validates viavixen-net::validate_http_url, routes HTTP(S) throughvixen-net::Network, and resolves toResponsewith status, headers, URL, redirect flag, and text body; policy/network failures reject the promise asTypeErrorand emit stable fetch failure events for CDPNetwork.loadingFaileddiagnostics.
Pure-logic foundation landed for the Performance API + viewport adaptation (Phase 6 prep).
The performance.now() monotonic-clock + <meta name=viewport> primitives
the timing host hooks and the mobile layout layer reduce to. Both
#![forbid(unsafe_code)], Rust-unit-tested.
vixen-engine::high_res_time— High Resolution Time § 4:DOMHighResTimeStamp(f64ms), the per-globalTimeOrigin(ms since Unix epoch thatperformance.now()is relative to), the § 4.4MonotonicClock(non-decreasing across calls + clamped to≥ 0), the § 4.4coarseneffective-time-value coarsening (floor to100µsunless cross-origin isolated), and theperformance.now()→ Unix-epoch conversion (timeOrigin + now) the legacyPerformanceTimingsurface reduces to.Page::evaluate_dom_expressionnow projects Performance API smoke checks fortypeof performance.now(), non-negativeperformance.now(), and monotonictimeOrigin + nowshape through this pure clock model while the real per-global timer host object lands.vixen-engine::viewport_meta— WHATWG HTML § 9.3<meta name="viewport">contentparser: the comma-separated<name>=<value>declaration set (width/heightdevice-keyword or CSS-px number,initial-scale/minimum-scale/maximum-scaleclamped to[0.1, 10],user-scalableyes/no,viewport-fitauto/contain/cover). Names ASCII-case-insensitive, values use the lenient leading-numeric-prefix extraction, unknown properties ignored. The CSS Device Adaptation 1 § 10 defaulting (width=980, &c.) stays in the layout layer; this module captures the authored declaration set.
Pure-logic foundation landed for URLPattern (Phase 6 prep).
vixen-engine::url_pattern— URLPattern API § 2 pathname pattern compile + match: the routing primitive client-side routers, service-workerFetchEventrouting, and thenew URLPattern()host hook reduce to.URLPattern::compileparses the pathname-grammar subset (literal segments,:namenamed captures with the[A-Za-z_][A-Za-z0-9_]*name rule,*rest-of-path wildcard) with duplicate-name detection + the wildcard-must-be-trailing rule;URLPattern::match_pathnameis a full-match (segment-based, empty-segment-collapsing so/posts≡/posts/,:namecaptures one non-empty segment,*captures the rest joined by/). Theprotocol/hostname/port/search/hashcomponents- full-regex custom params (
:name(\\d+)) land with the host hook; the named/*subset covers real routing.
- full-regex custom params (
- The Page eval seam exposes that pathname subset through
new URLPattern({ pathname })test()/exec().pathname.groupssmoke checks.
Pure-logic foundation landed for HTML attribute microsyntaxes + data:/srcset URLs (Phase 6 prep).
vixen-engine::microsyntax— the WHATWG HTML § 2.4 "common parser idioms" every attribute-value reflection reduces to:parse_signed_integer(§ 2.4.4) andparse_non_negative_integer(§ 2.4.3) with saturating overflow socolspan/rowspan/tabindex/cols/maxlengthnever panic;parse_float(§ 2.4.5) — the lenient leading-numeric-prefix extractor ("100px"→100.0,"3e999"→+∞) that<input type=number>and thevalue sanitization algorithmbuild on;parse_dimension_value(§ 2.4.6) — the legacy<td width>/<img width>surface producing either a pixel length or a percentage; andparse_list_of_integersfor<area coords>. Every HTML attribute-value parser here is deliberately lenient (leading whitespace skipped, trailing content ignored for the float surface) per the spec's documented browser contract; the stricter value-sanitisation layers a trailing-garbage check on top of these primitives.vixen-engine::srcset— WHATWG HTML § 4.8.4.6 "Parsing a srcset attribute": the comma-separated image-candidate-string splitter + the § 4.8.4.7Nw/Nxdescriptor validator (Descriptor::Width/Density). Candidates carrying ≥ 3 whitespace-separated tokens (a URL can't hold two descriptors) and candidates with an unparseable descriptor are dropped per spec; survivors keep document order (the § 4.8.4.8 selection algorithm prefers the first match on ties). The responsive-image selection step itself (composing candidates with the viewport DPR +<img sizes>) lands with the resource-fetch layer in Phase 1/6.vixen-engine::data_url— RFC 2397data:URL parsing: the case-insensitive scheme check, the;base64flag (final-parameter form), the mediatype defaulting rules (omitted ⇒text/plain;charset=US-ASCII; parameters-only ⇒text/plain+ authored parameters), and the payload decode (standard-alphabet base64 with ASCII-whitespace skipping + missing- padding tolerance, or RFC 3986 § 2.1 percent-decode). The Fetch standard does not MIME-sniffdata:URLs, so the declared mediatype is exposed verbatim. Base64 decoding uses the vettedbase64crate (pure-Rust,unsafe-free), shared byvixen-engine(data URLs) andvixen-net(CSP hash sources); the percent decoder is hand-rolled.fixtures/dom/srcset.html— exercises every<img srcset>/<source srcset>authoring form the parser handles (width descriptors, density descriptors, the bare-URL form, the<picture>/<source>art-direction combination); wired intofixtures/manifest.json.
Pure-logic foundation landed for responsive-image selection (Phase 6 prep).
The srcset parser left the § 4.8.4.8 selection step itself as a TODO; the
family is now complete end-to-end.
vixen-engine::media_query— CSS Media Queries 4 condition evaluation: a recursive-descent parser for the<media-condition>tree (§ 3) over parenthesised<media-feature>s (§ 4) withand/or/notcombinators, the<media-type>prefix (screen/print/allwithnot/only), and themin-/max-prefix decode into aRangeconstraint (min-width≡width >=).MediaQuery::matchesevaluates against aViewport(CSS-px width/height, DPR, derived orientation, output context (screen/print), colour depth, primary hover/pointer, aggregateany-hover/any-pointer,prefers-color-scheme,prefers-reduced-motion). The § 4 features implemented:width/height/aspect-ratio/orientation/resolution/color/hover/pointer/any-hover/any-pointer/prefers-color-scheme/prefers-reduced-motion, with the § 4.3 boolean form ((hover),(color)) and the<general-enclosed>fail-closed rule (unknown ⇒false).vixen-engine::source_size— WHATWG HTML § 4.8.4.7 "Parsing a sizes attribute": the<source-size-list>splitter + per-entry validator. The final comma-separated entry is the unconditional default (§ 4.8.4.8: the last entry always provides a fallback when reached); a non-last entry without a media-condition is a parse error and the whole list falls back to the spec's100vwdefault.resolve_px(&Viewport)walks the entries in document order and returns the first match's length in CSS px.vixen-engine::responsive_select— WHATWG HTML § 4.8.4.8 "Selecting an image source": composes a parsedsrcsetwith a resolved source size + the viewport DPR. Computes per-candidate pixel density (width ÷ source-size forNw, thexvalue for density, implicit1xfor bare), rejects mixed width/density lists (§ 4.8.4.6 parse error), keeps candidates withdensity ≥ DPR(falling back to all if that empties the list), and picks the smallest surviving density (ties → document order). Theselect_sourcehelper walks the<picture>/<source media>art-direction list: the first<source>whose media query matches the viewport wins, else the<img>srcset selects.fixtures/dom/sizes.html— exercises every<img sizes>/<source media>authoring form (mobile-first + three-tier conditional lists, the bare-length default, em-based sizes, the<picture>art-direction surface with min/max-width, orientation, output-context, and aggregate input-device media queries); wired intofixtures/manifest.json.Page::evaluate_dom_expressionnow projects the read-only<img>.currentSrcsmoke surface for plainsrcset/sizesimages fromresponsive_select, plus Page-backedHTMLImageElementalt/dimension/loading/decoding/complete/decode reflection and amatchMedia().matches/.mediaseam frommedia_query, proving selected-image URL reflection and MediaQueryList shape until the full image resource fetch path lands.fixtures/dom/media.htmlcovers the adjacent inert media-control seam:HTMLMediaElement/ audio / video identity/constants, booleans, timing/volume state,canPlayType(), and promise-shapedplay()without claiming codec/decode support.fixtures/dom/resources.htmlcovers resource-element reflection forlink/style/script/sourceattributes, stylesheet ownership, andHTMLScriptElement.supports()while parser-discovered script execution remains a separate host-runtime slice.fixtures/dom/dialog-details.htmlcovers details/dialog open-state reflection, includingshow()/showModal()/close()/requestClose()state updates and the close-event automation hook.fixtures/dom/reflected-misc.htmlwidens the reflected-attribute host-object seam across list, quote/time/edit, image-map, embedded-content, and table-cell attributes that automation suites commonly probe before deeper layout/resource semantics exist.fixtures/dom/progress-meter.htmlcovers progress/meter numeric host state (value/min/max/low/high/optimum/position) plus label associations for status-control automation probes.fixtures/dom/canvas.htmladds an inert Canvas 2D context seam: default canvas dimensions, cachedgetContext('2d'), no-op drawing calls,measureText(),createImageData(), and deterministictoDataURL()smoke until the real raster surface lands.fixtures/forms/reflected-attrs.htmlwidens form-associated host reflection: form metadata, submitter override attributes, inputvalueAsNumberplusstepUp()/stepDown(), textareasetRangeText()/textLength, and custom validity message smoke.fixtures/dom/table-collections.htmladds read-only table structure smoke forcaption,tHead,tFoot,tBodies,rows,cells, and row/cell indexes, giving automation a table traversal seam before mutation APIs land.fixtures/dom/html-element-attrs.htmlcovers HTMLElement interaction/global reflection (tabIndex, access keys, drag/spellcheck/translate, virtual-keyboard hints, and popover state) used by higher-level locator/keyboard automation.fixtures/dom/text-tracks.htmlcovers inert media text-track state:HTMLTrackElementreflected attributes, stabletrackobjects,TextTrack, andTextTrackListlookup from media elements.fixtures/dom/offscreen-canvas.htmlcovers adjacent inert canvas adjunct APIs:ImageData,OffscreenCanvas2D context identity, blob/bitmap promises, and no-opPath2Dconstruction until real raster/bitmap transfer lands.fixtures/dom/shadow-root.htmlcovers the first Shadow DOM host-object shape:attachShadow(),ShadowRoot/DocumentFragmentidentity, host/mode fields, empty fragment queries, anddocument.createDocumentFragment()before composed-tree distribution/layout lands.fixtures/dom/template-slot.htmlcovers the adjacent web-component host shape:HTMLTemplateElement.contentas aDocumentFragmentplus slotname,assignedNodes(), andassignedElements()methods before slot distribution is wired into the composed tree.fixtures/dom/construction-serialization.htmlcovers DOM construction and serialization helpers (createElementNS(), text nodes, fragments, andXMLSerializer.serializeToString()) for framework feature-detection probes.fixtures/dom/platform-apis.htmlcovers browser-platform host probes that now run in thedeno_corepage realm: securecrypto.getRandomValues()/randomUUID(), async Clipboard text /ClipboardItem, first-callbackIntersectionObserver/ResizeObservergeometry, and fail-closedWebSocketclose diagnostics until real socket transport is wired.
Pure-logic foundation landed for CSS value-resolution + easing (Phase 3/6 prep).
The calculation + timing-function primitives the cascade (calc() reduction,
var() substitution, custom-property resolution) and the transition/animation
drivers (animation-timing-function) reduce to.
vixen-engine::calc— CSS Values 4 § 10calc()/min()/max()/clamp()arithmetic tree + evaluator. A recursive-descent parser produces aCalcNodeAST (Number/Length/Percent/Add/Sub/Mul/Div/ the § 10.1Min/Max/Clampmath functions);evaluateruns the § 10.7 "argument resolution" pass with full dimension type-checking (+/-require homogeneous operands;*requires a number operand;/requires a number divisor; violations are hard errors). Lengths and percentages mix in the classiccalc(50% + 10px)form, resolving to(px, percent)against aLengthContext. Operator precedence (*//over+/-) and nested parenthesised grouping enforced; bare expressions (nocalc()wrapper) parse too, so the--computed-styleprojection re-resolves the unwrapped form.vixen-engine::easing— CSS Easing 1 § 2-4: the timing-function family that maps an input progress (0..1) to an output progress.Easing::parsecovers the keyword aliases (linear/ease/ease-in/ease-out/ease-in-out/step-start/step-end) and the function forms (cubic-bezier(),steps(),linear());Easing::evaluateprojects cubic-bezier control points via Newton-Raphson (8 iterations) with a bisection fallback so it converges on every valid curve (incl. overshoot spring curves where the y-coordinates exceed[0, 1]), implements the § 4.1 step jump-position rules (jump-start/jump-end/jump-none/jump-bothwith thejump-none-requires-n ≥ 2validation), and piecewise-linearly interpolates thelinear()multi-stop function (explicit percentage positions + the § 3.1 implicit even-distribution rule).
Pure-logic foundation landed for CSS generated content (Phase 5/6 prep).
The counter-scope + marker-text primitives the content property
(counter()/counters()), list-style-type, and the ::marker box reduce
to. Both #![forbid(unsafe_code)], Rust-unit-tested.
vixen-engine::list_marker— CSS Lists 3 § 6.1<list-style-type>marker text: the predefined counter-style family (disc/circle/squarebullet glyphs,decimal/decimal-leading-zeronumeric, thelower-alpha/upper-alpha(+lower-latin/upper-latinaliases) bijective base-26 alphabetic,lower-roman/upper-romanadditive,lower-greekover the 24-letter alphabet,none).ListStyleType::renderis thevalue → textprojection per the § 6.1 algorithm table; the § 6.1 fallback rule (out-of-range additive/alphabetic values fall back todecimal, the default fallback) is enforced so a counter value never fails to produce a marker. Aliases normalise to the canonical name at parse so the round-trip is canonical.vixen-engine::counter— CSS2 § 12.4 counter scoping (reset/increment/set, with the per-kind default value —0for reset/set,1for increment) + CSS Lists 3 § 5counter()/counters()resolution.parse_counter_opstokenises thecounter-*declaration value (ASCII-whitespace-separated<custom-ident>optionally followed by one<integer>, thenoneno-op, saturating integer overflow, the--fooCSS-variable reservation rejected);resolve_counterreads the innermost in-scope value (orNone→ empty marker per § 5);resolve_countersjoins every in-scope value outermost→innermost with the delimiter string ("1.1","1.3.2"). The DOM traversal that pushes/pops scopes + applies the ops in document order stays in the Phase 4 layout layer; this module is the pure resolution primitive given the already-walked scope stack, and composes withlist_markerviarender_counter.
Pure-logic foundation landed for structured clone + MessagePort (Phase 6 prep).
The serialisation + entangled-port model postMessage(),
new MessageChannel(), worker postMessage(), BroadcastChannel, and
IndexedDB / history.pushState() reduce to. Both
#![forbid(unsafe_code)], Rust-unit-tested, composing with the cross-origin-
isolation gate (coep::is_cross_origin_isolated) for SharedArrayBuffer
exposure.
vixen-engine::structured_clone— HTML § 2.7.5 structured clone algorithm.StructuredCloneValueis the type-tagged tree of serialisable values (primitives,Date,Array,Object,Map,Set,ArrayBuffer,MessagePort,Errorwith theErrorKindsubclass family, and thePlatformObjectslot reserved forFile/Blob/ImageData&c.).clonedeep-clones the tree honouring the transfer list: every transferred handle must be reachable (DataCloneError::UnreachableTransferable), the list may not carry duplicates (DataCloneError::DuplicateTransferable), a detached buffer is rejected (DataCloneError::DetachedTransferable), and aSharedArrayBufferclone (not transfer) requires a cross-origin- isolated context (DataCloneError::SharedBufferRequiresIsolation— the gateis_cross_origin_isolatedfeeds).detach_transferredflips the transferredArrayBuffers to detached in the source tree;SharedArrayBuffers stay shared.is_cloneableis the partial-check a host hook calls before walking (so aDataCloneErrorsurfaces before any transfer side-effect). Shared-reference identity preservation (the spec's "memory" map) lives at the host hook where real JS object identities exist; this is the faithful tree-clone for tree inputs.vixen-engine::message_port— HTML § 9.5MessagePort/MessageChannel.MessagePortis one end of an entangled pair (thePortIdhandle appears inStructuredCloneValue::MessagePortand the transfer list);MessageChannel::newconstructs the pair.MessagePort::post_messageruns the § 9.5.4 steps: structured-clone the value (honouring the transfer list), and return the clone + the partner id + the transferred ports in aPostOutcome(the host hook routes the enqueue to the partner — the two ports may live in different compartments / workers).MessagePort::enqueue/MessagePort::drainare the receiver-side inbox + the event-loop hand-off;start()/close()carry the § 9.5.3 / § 9.5.5 lifecycle (a detached port dropspostMessageand drains nothing).Page::evaluate_dom_expressionnow projects a smallstructuredClone()smoke seam for primitive strings, arrays, shallow objects, Date, Map, Set, and Error name/message shape through the same clone function thatpostMessage()/ history state will call. RuntimeMessageChannelandBroadcastChannelsmoke now dispatchMessageEventthrough the same EventTarget/WebIDL host layer while the pure Rustmessage_portmodel remains the cross-compartment transfer primitive.
Pure-logic foundation landed for Range + Selection (Phase 6 prep).
The boundary-point model the Range / Selection host hooks + the
editing-command surface (document.execCommand, beforeinput dispatch)
reduce to. #![forbid(unsafe_code)], Rust-unit-tested.
vixen-engine::range— DOM § 5.2Range+ § 5.4Selection.NodeRefis an opaque DOM-node handle carrying aDocumentOrderindex (the pre-order DFS position the caller assigns) so two boundaries compare in document order by pure arithmetic.Boundaryis the(node, offset)pair (child index for elements, UTF-16 index for text nodes);Boundary::compareis the § 5.2 relative position (Ordering::Before/Ordering::Equal/Ordering::After).Rangecarries the(start, end)pair withRange::newre-ordering to the § 5.2start ≤ endinvariant,Range::is_collapsed+Range::collapse+Range::contains_boundary+Range::intersect.Selectioncarries theRangelist + the anchor/focus (direction-aware) +add_range/collapse_to/extend_to/remove_all_ranges+ theSelectionDirection(Forward/Backward/None— the focus-before-anchor "backward" selection state). The live tree mutation (surroundContents/insertNode/extractContents— the § 5.3 algorithms) is the host hook; this module is the pure boundary model.Page::evaluate_dom_expressionnow projects the read-only initial Range / Selection smoke seam (document.createRange().collapsed/ offsets and emptygetSelection()accessors) through this pure model while the live DOM mutation and selection host objects remain the Phase 6 swap-in.
Pure-logic foundation landed for session history + pushState (Phase 6 prep).
The HTML § 7.1 session-history entry-stack + the history.pushState /
replaceState / back / forward / go surface the History host hook
- the navigation layer reduce to.
#![forbid(unsafe_code)], Rust-unit-tested.
vixen-engine::history— HTML § 7.1.ScrollRestorationis theauto/manualhistory.scrollRestorationmode;HistoryEntryis one session-history entry (URL string + opaque structured-clonestateblob- the
scrollRestorationmode + the optional title).SessionHistoryis the entry stack + the current-entry cursor with the § 7.1 surface:push(truncates the forward branch per the § 7.1 "remove all entries after the current one" rule, appends, advances the cursor),replace(swaps the current entry, length unchanged),back/forward/go(delta)(cursor movement with out-of-range ⇒ no-op),length/index/url/state/scroll_restoration, and thewith_entriesescape hatch for the host hook that restores a persisted session history. The document load/unload for a traversal (the § 7.5 "traverse the history" algorithm), the same-origin URL check forpushState/replaceState, and the structured-clone serialisation of thestatevalue stay in the navigation layer / host hook (the host hook serialises viacrate::structured_clonebefore callingpushState).
- the
Page::evaluate_dom_expressionnow projects the read-only initial History smoke seam (history.length,history.state,history.scrollRestoration) fromSessionHistory::new(HistoryEntry::navigation(_))before mutablepushState/traversal host objects land.
Pure-logic foundation landed for MutationObserver (Phase 6 prep).
The DOM § 4.3 mutation-queue + the § 4.3.1 match predicate the
MutationObserver host hook + the microtask-delivery step reduce to.
#![forbid(unsafe_code)], Rust-unit-tested.
vixen-engine::mutation_observer— DOM § 4.3.MutationTypeis the threechildList/attributes/characterDatarecord types;MutationRecordis one record (target + added/removed nodes + siblings forchildList+ attribute name/namespace +oldValue);MutationObserverInitis the § 4.3.1observe()options (childList/attributes/attributeFilter/attributeOldValue/characterData/characterDataOldValue/subtree).Relation(Target/Descendant) +should_observeis the § 4.3.1 match predicate (the options vs the mutation type + the target/subtree relation + the attribute filter).MutationObservercarries the record queue + the registrations +observe(re-observing replaces per § 4.3.1, invalid options rejected) /disconnect(clears registrations, keeps pending records) /takeRecords/drain_for_delivery(the microtask-checkpoint batch). The live-DOM-tree relation classification + the microtask checkpoint scheduling + the callback invocation stay in the host hook / event-loop layer.Page::evaluate_dom_expressionnow projects the initial MutationObserver lifecycle smoke seam (takeRecords().length,disconnect()) through this pure queue model while live DOM mutation delivery remains in the host hook.
Pure-logic foundation landed for TreeWalker + NodeIterator (Phase 6 prep).
The DOM § 6 filtered traversal model the two NodeFilter-based iterators
reduce to. #![forbid(unsafe_code)], Rust-unit-tested, over a Tree
trait the host hook implements on the real DOM.
vixen-engine::traversal— DOM § 6.NodeType(the DOMnodeTypecodes) +WhatToShow(the § 6.1whatToShowbitmask,SHOW_*constants +SHOW_ALL) +FilterResult(FILTER_ACCEPT/FILTER_REJECT/FILTER_SKIP) + theNodeFiltertrait (the JS callback the host hook implements) + theTreetrait (the host hook's tree access).TreeWalkeris the § 6.2 rooted stateful walker with the seven methods (parent_node/first_child/last_child/next_sibling/previous_sibling/next_node/previous_node);FILTER_REJECTskips the rejected node's subtree,FILTER_SKIPtraverses into it.NodeIteratoris the § 6.3 flat preorder iterator (next_node/previous_node) whereREJECT==SKIP(the flat cursor has no subtree state), plus theadjust_for_removalstep the host hook consults when a node is removed from the tree (the reference moves to the removed subtree's previous sibling's last descendant, else the parent). The real-DOM tree walk + the JSNodeFiltercallback invocation stay in the host hook.Page::evaluate_dom_expressionnow projects the whatToShow-only element traversal smoke seam fordocument.createTreeWalker()anddocument.createNodeIterator()by adapting the parsed document to the traversal module'sTreetrait.
Pure-logic foundation landed for the WHATWG URL parser (Phase 6 prep).
The URL Standard § 4 parse + serialize + relative-resolution model the
fetch / navigation / new URL() host hooks consult. #![forbid(unsafe_code)],
Rust-unit-tested.
vixen-engine::whatwg_url— WHATWG URL Standard.Urlcarries the parsed components (scheme / username / password / host / port / path / query / fragment);is_special_scheme+default_portencode the § 3.1 special-scheme family (http/https/ws/wss/file).parseparses an absolute URL;parse_with_baseis the § 4.6 relative-resolution parser (absolute-path / relative-segment merge / query-only / fragment-only / scheme-relative against a baseUrl).Url::serializeis the § 4.1 canonical serialiser (the default port omitted, IPv6 re-wrapped in[...], the opaque-path no-slash form for non-special schemes);Url::originis the § 4.5(scheme, host, port)tuple the fetch / storage layers partition on.percent_encode+ theEncodeSets (C0 control / fragment / query / path / userinfo) cover the § 4.2 percent-encoding family. IDNA, full IPv6, and the opaque-path long tail are the deferred slices (non-ASCII hosts fail closed; the IPv6 literal is captured verbatim). The module is namedwhatwg_url(noturl) so it doesn't shadow the externurlcrate the rest of the engine consumes.
Pure-logic foundation landed for HTML fragment serialisation (Phase 6 prep).
The DOM → HTML string pipeline the Element.innerHTML getter, outerHTML,
document.write, DOMParser round-trip, and XMLHttpRequest.responseText
(HTML documents) host hooks read from. #![forbid(unsafe_code)],
Rust-unit-tested, operating over the markup5ever_rcdom::Handle the parse
side (crate::doc) already owns.
vixen-engine::html_serialize— WHATWG HTML § 13.2.9 "Serializing HTML fragments".serialize_childrenis theElement.innerHTMLgetter (the § 13.2.9 fragment serializer over a node's children);serialize_nodeis theElement.outerHTMLgetter (one node + descendants).escape_text(§ 13.2.9 step 8:&→&,<→<,>→>, NBSP → ) andescape_attribute(§ 13.2.9 step 5:&,", NBSP) are the escape rules exposed standalone for the editing-command surface.Scriptingis the scripting-flag toggle (thenoscriptelement is raw-text when scripting is enabled, the production case; normal-text otherwise, theDOMParser/ print case). The void-element table (area/base/br/col/embed/hr/img/input/link/meta/param/source/track/wbr) + the raw-text table (script/style/xmp/iframe/noembed/noframes/plaintext+ conditionalnoscript) are the § 13.2.9 step 3 classification. The pre-serialisation tree mutation for theinnerHTMLsetter (the parse side) and the foreign-content (SVG/MathML) CDATA escapes stay in the parse layer.
Gate: fixtures/dom/, fixtures/events/, fixtures/forms/,
fixtures/storage/, fixtures/network/ all pass.
Phase 7 — Security hardening (≈ 1 week)
Wire every trust boundary from docs/ARCHITECTURE.md.
Steps:
- CSP enforcement at
script.rs::evaluate(script-src / unsafe-inline / nonce / hash) and at fetch (per-directive URL matching). - Cookie validation already done in Phase 1; confirm document-side
boundary (
document.cookiecannot set HttpOnly). - URL policy re-applied at every fetch, including JS-initiated fetch / XHR.
- Origin isolation confirmed across storage, scripts, cookies.
- Permissions API behaves per spec.
just auditpasses.- Fuzz targets:
url_policy,csp::parse,html5everparse, the cookie parser. Each runs 1 M iterations without panic.
Pure-logic foundation landed (Phase 7 prep).
vixen-net::referrer_policy— Fetch § 3.4Referrer-Policyparser (last-known directive wins) + § 4.3.7resolve_referrercovering every policy branch (downgrade suppression, same-origin gating, origin-only, strict-origin-when-cross-origin default) + theis_potentially_trustworthytest the downgrade rules reduce to. The network layer attaches the resolvedRefereronce wired.vixen-net::strict_transport_security— RFC 6795 § 6.1 HSTS header parser (case-insensitive directives, tolerant whitespace, header ignored without validmax-age,max-age=0cache-deletion signal) + § 8.2HstsEntry::matches(exact host or, withincludeSubDomains, a dot-prefixed subdomain — the superdomain rule is one-way).vixen-net::cors— Fetch § 3.2.1Access-Control-*response-header parser (case-insensitive names, lowercased + de-duplicated lists, repeated origin header first-wins), § 4.1.5cors_check(wildcard + credentials forbidden, specific-origin string equality,null-origin echo), and § 4.1.6cors_filtered_headers(safelist of 7 response headers + named exposes, withSet-Cookie/Set-Cookie2always stripped). The script→fetch host hook consults this at every cross-origin response.vixen-net::mixed_content— W3C Mixed Content L1 § 3 verdict (NotMixed/Block/Upgrade) the fetch layer applies at every subresource fetch out of a secure context. [ResourceType] collapses the fetch destination to the three modal categories (active=block, passive=upgrade, navigation=allow);block-all-mixed-contentCSP overrides upgrades. Reusesreferrer_policy::is_potentially_trustworthyfor the request-URL secure-transport test.fixtures/security/cors-headers.html— exercises the HTML surface (crossorigin,integrity,nonce) the host-hook layer dispatches on when constructing the cross-origin fetch; wired intofixtures/manifest.json.fixtures/network/mixed-content.html— exercises every mixed-content surface (http:// scripts/stylesheets/iframe/object vs. images/audio/video vs. top-level navigation, plus https:// counterparts); wired intofixtures/manifest.json.
Pure-logic foundation landed for <iframe sandbox> (Phase 7 prep).
vixen-net::sandboxing— WHATWG HTML § 4.8.5 sandbox-flag parser (the fullallow-*keyword set: forms / modals / orientation-lock / pointer-lock / popups / popups-to-escape-sandbox / presentation / same-origin / scripts / top-navigation + the user-activation + custom-protocols variants / downloads / storage-access / unsafe-downloads). Tokenised on ASCII whitespace, case-insensitive, unknown flags ignored, empty value ⇒ most-restrictive. The derived security predicates the script/navigation/storage layers consult:implies_unique_origin(the § 4.8.5 opaque-origin rule), andis_dangerous_scripts_plus_same_origin(the famous "if bothallow-scriptsandallow-same-originare present, the sandbox is escapable" warning the spec mandates).fixtures/security/sandbox.html— exercises everysandboxvariant the parser handles (empty / scripts-only / scripts+same-origin dangerous combination / top-nav family / popups family / mixed legacy flags / unknown-token tolerance / case-insensitivity); wired intofixtures/manifest.json.
Pure-logic foundation landed for Sec-Fetch-* + Permissions Policy (Phase 7 prep).
vixen-net::sec_fetch— Fetch § 3.1Sec-Fetch-*request-metadata parsing:SecFetchSite/SecFetchMode/SecFetchDest/SecFetchUsertyped enums (case-sensitive token parse, fail-closed to [Default] on unknown values) + a bundledSecFetchHeaders::parseover a(name, value)iterator (case-insensitive names, last-wins combine). The § 3.2.4classify_siteclassifier resolves the embedder↔target relationship (same-origin/same-site/cross-site/none) the fetch layer attaches and that servers consult for the § 3.2 Cross-Origin gates; thesame-siteregistrable-domain comparison uses the last-two-labels heuristic (documented limitation; the PSL lands when the cookiedomainmatcher needs it too).SecFetchDest::is_navigation/is_embedpredicate the § 4.4 navigation and § 3.2 COEP checks.vixen-net::permissions_policy— Permissions Policy 1 § 3.3Permissions-Policyresponse-header parser + the § 5.2<iframe allow>attribute parser. TheAllowlistenum covers every § 3.3 source-list form (Everyone */Self_ self/Src src/Origins(list)/None ()-deny-all);PermissionsPolicy::allowsis the § 4 evaluation the host hooks consult before exposingnavigator.geolocation/camera/ &c. (features not in the policy default to embedder-only per § 3.3). The structured-field parser is paren/quote-aware (handlesgeolocation=(self "https://partner.test")and the iframe shorthandcamera 'self'), tolerant of whitespace, and drops malformed items per the spec's "parse error ⇒ item dropped" rule.fixtures/security/permissions-policy.html— exercises every<iframe allow>authoring form (bare feature names, theself/srckeywords, explicit origin lists, the empty()deny-all, the camera/geolocation/ microphone/fullscreen/autoplay family); wired intofixtures/manifest.json.
Pure-logic foundation landed for the WebSocket protocol boundary (Phase 6/7 prep).
vixen-net::websocket— RFC 6455 pure-logic boundary:compute_accept(§ 4.2.2Sec-WebSocket-Accept=base64(SHA1(key + GUID)), via thesha1crate — already transitively present),validate_client_handshake(§ 4.1 the server-sideUpgrade/Connection/Sec-WebSocket-Version: 13/16-byte-key enforcement) +validate_server_response(§ 4.2.2 the client-side101+ Accept-matches-sent-key check),parse_frame_header(§ 5.2 the 2–14-byte frame decoder — FIN/RSV/opcode/mask/length, with the § 5.2 reserved-RSV/opcode rejection + the non-canonical-length rule + the § 5.5 control-frame≤ 125bytes + FIN-set invariants),apply_mask(§ 5.3 the XOR demask) +validate_close_code(§ 7.4 the status-code range + reserved- band rule). The framed TCP+TLS transport + theWebSocketJS host hook sit on top;permessage-deflateis deferred.
Pure-logic foundation landed for the cross-origin isolation gate (Phase 7 prep).
The COOP + COEP response-header pair that, together, make a browsing context
"cross-origin isolated" — the gate the high-resolution timers
(vixen_engine::high_res_time::coarsen), SharedArrayBuffer exposure,
and the other Spectre-hardened APIs consult. Both #![forbid(unsafe_code)],
Rust-unit-tested.
vixen-net::coop— HTML § 7.8Cross-Origin-Opener-Policyparser. The three § 7.8.4 policy values (coop::Coop—unsafe-nonedefault /same-origin-allow-popups/same-origin) via the § 7.8.1 structured-header item parse (case-insensitive token, unknown ⇒UnsafeNonefail-closed,report-toparameter captured).coop::Coop::isolates_openeris the § 7.8.4 opener-isolation predicate the navigation layer consults before reusing a browsing-context group.vixen-net::coep— Fetch § 3.2Cross-Origin-Embedder-Policyparser. The three § 3.2 policy values (coep::Coep—unsafe-nonedefault /require-corp/credentialless) via the structured-header item parse.coep::is_cross_origin_isolatedis the HTML § 7.2 combined gate:trueiff the COOP issame-originand the COEP isrequire-corporcredentialless. This is the booleanMonotonicClock::now'scross_origin_isolatedparameter receives, removing the100µscoarsening floor when the context is fully hardened.
Pure-logic foundation landed for Subresource Integrity + X-Content-Type-Options (Phase 7 prep).
The two response-header boundaries the fetch layer consults before
executing a subresource — the tampering-resistance surface (SRI) + the
MIME-confusion surface (nosniff). Both #![forbid(unsafe_code)],
Rust-unit-tested.
vixen-net::integrity— W3C SRI § 3.2.2<script integrity>/<link integrity>metadata parse + § 3.3.4 verify.HashAlgorithmis the three SRI-mandated algorithms (sha256/sha384/sha512); SHA-1/MD5 are collision-broken and dropped at parse time per spec.parse_integritysplits ASCII-whitespace-separated<algo>-<base64>entries (+ the optional?<options>tail, parsed but not enforced in v1).verifycomputes each hash over the raw response body via the vettedsha2crate + a constant-time compare (a timing oracle can't recover the digest); any match passes (the spec's "best candidate" rule). TheIntegrityOutcome(NoMetadata/Verified/Mismatch/NoKnownAlgorithms) drives the fetch layer's block.vixen-net::nosniff— Fetch § 2X-Content-Type-Options: nosniffenforcement.is_nosniffis the case-insensitive token parse (the parameterised historical form is rejected);is_javascript_mimeis the Fetch § 3.7 16-entry JavaScript-MIME-type predicate;Destinationcollapses the § 3.1.7 request destination to the two nosniff-relevant categories (Script/Style/Other);enforceblocks aScriptdestination whose MIME is not a JavaScript MIME type and aStyledestination whose MIME is nottext/css, returning theNosniffOutcome(Allow/BlockScript/BlockStyle) the fetch layer surfaces as a network error. Other destinations are unaffected (the spec intentionally limitsnosniff's scope).
Pure-logic foundation landed for Cross-Origin-Resource-Policy (Phase 7 prep).
The Fetch § 4.5.3 CORP header + the combined COEP + CORP gate the fetch
layer consults before applying a no-cors subresource response into a
COEP-hardened document. #![forbid(unsafe_code)], Rust-unit-tested,
reusing crate::coep::Coep + crate::origin::Origin.
vixen-net::corp— Fetch § 4.5.3.Corpis thesame-origin/same-site/cross-originvalue (parse_corpcase-insensitive,Nonefor an absent / unparseable header).is_same_siteis the § 4.5.3 same-site predicate (same scheme + matching registrable domain; the last-two-labels eTLD+1 heuristic the PSL refines later);check_corpis the § 4.5.3 check (Allow/Block, opaque origins fail closed).coep_corp_gateis the combined gate:unsafe-none⇒ allow; CORS ⇒ allow (the alternative opt-in);require-corp⇒ same-origin allow, cross-origin no-CORP block, cross-origin-with-CORPcheck_corp;credentialless⇒ same-origin allow, cross-originAllowWithoutCredentials. The CORS check itself + the COEP parse stay incrate::cors/crate::coep.
Pure-logic foundation landed for Trusted Types (Phase 7 prep).
The W3C Trusted Types trusted-types + require-trusted-types-for CSP
directive boundary the DOM injection-sink host hooks (.innerHTML,
eval(), document.write(), script.src = …, &c.) consult before
accepting a string. #![forbid(unsafe_code)], Rust-unit-tested.
vixen-net::trusted_types— W3C TT.TrustedTypeKindis the threeTrustedHTML/TrustedScript/TrustedScriptURLvalue kinds;AllowedNamesis thetrusted-typesdirective's policy-name set (None/Explicit(list)/Wildcard);TrustedTypesPolicyNamescarries the set + theallow-duplicatesflag;RequireForis therequire-trusted-types-for 'script'flag (the only sink-group in v1, covering every TT sink).parse_trusted_types+parse_require_trusted_types_forparse the two directives;policy_creation_allowedis the § 3.2.3createPolicy(name)gate (the allowed-name match + the duplicate-name block);evaluate_sinkis the § 3.3.5 injection-sink decision (a Trusted* value ⇒Allow; a string at a TT-requiring sink ⇒ApplyDefaultPolicyif adefaultpolicy exists elseBlock; a string at a non-TT sink ⇒Allow). The JSTrustedTypePolicyfactory + thecreateHTML/createScript/createScriptURLsanitisers + the violation-reporting surface stay in the host hook.
Gate: Every security test in vixen-net and vixen-engine green.
just audit passes. Fuzz targets stable.
Phase 8 — Headless CDP + tooling polish (≈ 1 week)
Implement the full headless tool surface.
Steps:
- Implement CDP server (tokio + tokio-tungstenite) in
vixen-headless. Command handlers call intovixen-enginevia theEngineInspectortrait. - Implement every CLI flag from
docs/SPEC.md"Headless CLI surface". Stable error codes preserved exactly. - Implement
--memory-stats,--paint-stats,--incremental,--list-fonts,--cdp. (Note:--gpuis omitted per ADR-003 — every render path is GPU-backed.) --cdpresponds to:Browser.getVersion,Target.createTarget,Target.attachToTarget,Page.navigate,Page.loadEventFired,Runtime.evaluate.
Gate: Every CLI flag works. CDP responds to required methods.
Phase 9 — Release hardening (≈ 1 week)
Final tock before v1.0.
Steps:
- Module size audit: no non-test module > 1,000 lines. Decompose where needed.
- Dead-code removal pass:
cargo machete, fix every clippy warning, audit#[allow(dead_code)]annotations. - Performance baselines: establish criterion baselines for
benches/{parse,style,layout,render}as each lands (Phase 3+). Future releases gate on no > 10 % regression vs the most recent release. - Binary size measurement:
just size-fp. Confirm targets perdocs/ACCEPTANCE.md. - WPT target profile from
docs/COMPAT.mdis green. Migrate remaining end-to-end CSS+DOM assertions out of Rust tests where an HTML fixture can cover the behavior. - Update
docs/COMPAT.mdwith measured pass counts, known gaps, and the next-release WPT expansion plan. - Write user-facing release notes.
Gate: every release gate in docs/ACCEPTANCE.md green. Tag v1.0.0.
Total: ~16–22 weeks of focused work.
Binary size strategy
Concrete levers, in priority order:
- Re-measure with the ADR-014
deno_coreruntime. V8 packaging changed the pre-migration size assumptions; current release promises must come from measured binaries. [profile.release]is already optimal (seedocs/ARCHITECTURE.md).- Feature-gate aggressively: CDP, devtools UI, keychain integration. Each behind a feature. Default build includes none of them.
- System Cairo/Pango/HarfBuzz/fontconfig from the GNOME SDK. WebRender uses the system GL stack; glyph rasterisation goes through fontconfig + freetype (system) into WebRender's own atlas.
- One paint path, not N. ADR-003/ADR-006 enforce this: no
tiny-skia, nofontdue, no parallel CPU rasterizer, noPaintBackendtrait. - Per-release measurement in
docs/ACCEPTANCE.md.
Targets:
| Binary | Target |
|---|---|
vixen (GUI) | TBD after deno_core/V8 measurement |
vixen-headless | TBD after deno_core/V8 measurement |
Testing strategy
WPT-first. Every CSS/DOM/Layout/Paint feature is tested via a WPT
fixture in fixtures/, not a Rust unit test. Rust tests cover only pure
logic (CSS length arithmetic, URL parsing, cookie validation, CSP
parsing, redb storage round-trip).
WPT-style check types in vixen-wpt (per docs/SPEC.md): DOM/style
assertions, layout-box coordinate assertions, display-list-contains paint
dump assertions, visual hashes, and ref-equivalent rendered-page comparisons
against reference HTML fixtures.
Snapshot tests against Firefox reference. A fixtures/reftest-baseline/
directory contains reference renderings. Each visual WPT fixture
compares against the baseline with a perceptual hash and 1 % pixel-diff
tolerance. Failures dump a side-by-side diff to target/reftest-diff/.
Performance regression. benches/{parse,style,layout,render}
criterion benches run on every release. Gate: no > 10 % regression vs
previous release.
Risk register
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Stylo integration harder than estimated | Medium | High | Time-box Phase 3 to 2 weeks; if traversal conformance blocks, narrow v1.0 CSS scope and document gaps in docs/COMPAT.md. |
deno_core / V8 packaging size and cache churn | Medium | High | Keep JsRuntime seam stable, measure release binaries with V8, and document cache/pinning in guidance. |
| EGL surfaceless unavailable on some CI runners | Low | Medium | LIBGL_ALWAYS_SOFTWARE=1 + Mesa llvmpipe covers every Linux runner. |
gtk4::GLArea context sharing with WebRender | Medium | High | Validate in Phase 5 first week. Fallback: render to FBO, blit to GLArea with a tex. |
| Vixen-owned layout takes longer than planned | High | High | Keep Phase 4 vertical through Page; ship only the WPT-profiled v1 subset and document gaps in docs/COMPAT.md (ADR-013). |
| JS host-extension churn | Medium | Medium | Keep the public JsRuntime/JsValue seam stable while converting bootstrap surfaces to deno_core ops/resources; cite .tmp/ref/deno/core/, .tmp/ref/deno/runtime/, and .tmp/ref/deno/ext/. |
| Real-world pages regress vs Servo/Firefox | Low | Medium | Upstream issues; report and work around. Document in docs/COMPAT.md. |
| WPT migration backlog grows during build | Medium | Medium | Per-phase gate: each phase deletes Rust tests at the rate it adds WPT fixtures. |
Relm4 breaking change in Factory/Worker API | Low | Medium | Pin Relm4 version per release; consult .tmp/ref/relm4/ on upgrades. |
Per-phase gate summary
| Phase | Gate |
|---|---|
| 0 — Scaffolding | just gate-phase0 passes |
| 1 — Net + store crown jewels | just gate-phase1 passes |
| 2 — JS runtime | just gate-phase2 passes; runtime is deno_core per ADR-014 |
| 3 — HTML + Stylo | WPT CSS fixtures pass; cascade output correct |
| 4 — Layout | 20+ visual-hash fixtures match reference |
| 5 — Paint | just run shows a page; headless PNG within 1 % of GUI on 5 fixtures |
| 6 — Host bindings | fixtures/{dom,events,forms,storage,network}/ all pass |
| 7 — Security | just audit clean; all security tests green; fuzz stable |
| 8 — Headless CDP | Every CLI flag works; CDP responds to required methods |
| 9 — Release | All docs/ACCEPTANCE.md gates green; tag v1.0.0 |