Skip to content

feat(consolidation): the n0 path becomes the SVG engine of record#61

Merged
softmarshmallow merged 69 commits into
mainfrom
chore/consolidation-web-first
Jul 27, 2026
Merged

feat(consolidation): the n0 path becomes the SVG engine of record#61
softmarshmallow merged 69 commits into
mainfrom
chore/consolidation-web-first

Conversation

@softmarshmallow

@softmarshmallow softmarshmallow commented Jul 21, 2026

Copy link
Copy Markdown
Member

feat(consolidation): the n0 path becomes the SVG engine of record

Supersedes this PR's original scope. The Web-first prototype is the first third
of the branch; what follows it is the decision that prototype fed and the
capability the decision unlocked.

This is a decision-evidence PR, not a feature PR. 57% of the added lines are
evidence — 10,355 in laws, 5,432 in fixtures and Chromium oracles, 2,297 in
docs/wg — against 13,047 of crate source. That ratio is the point: two registry
decisions are taken here, and each carries the bar it had to clear.

What is decided

The n0 path (websem → rframe → n0) is the SVG engine of record, static and
animated. htmlcss::svg is demoted to a frozen semantics donor, mined
per-feature. The product host routes every .svg/.html render through the
retained compiler session — Base by default, --time-ns for one exact
signed-nanosecond sample. Alongside it, the kernel's join point is settled for
glyphless visual facts; shaped text stays a later stage.

Then five evolution rungs, each landed as a triple (engine + laws / corpus +
bake / docs) and each Chromium-baked:

rung admits
viewport viewBox-only sizing (WxH is the initial viewport, SVG2 §8.2) + the full preserveAspectRatio grammar
basic shapes <circle>, <ellipse>, the auto-radius matrix, the zero-radius nothing
containers <g> and the whole transform grammar, flattened rather than represented
paths <path>, the d grammar (no arc — see below), fill-rule
strokes stroke, stroke-width, -linecap, -linejoin, -miterlimit, and <line>

What this does not claim

  • No conformance number, and none is computable here. The scoring rule is
    unratified (#49) and says no
    score may be produced or inspected until it is. Nothing in this branch computes
    one.
  • Phase 1 is not exited. Its vocabulary also contains opacity, clips,
    gradients and patterns; none land here.
  • The scoreboard suite is covered 10 of 14. n0 --strict renders ten rows of
    fixtures/scoreboard/svg-rect-path-v0 and refuses four, on exactly two
    constructs: opacity and rx on <rect>. Those two are the whole distance to
    full chassis coverage — and they are the next piece of work, not this one.

Evidence

  • 77 primitive cells, each byte-exact against an independent Chromium bake
    except six curved cells carrying a declared, geometrically confined tolerance.
    Provenance — browser version plus sha256 of the suite, sources, oracles and
    bake script — is committed and re-verified by law.
  • svg-scene-cub: an original composition covering the Ghostscript tiger's
    feature list in 17 nodes, committed as a pair (animated plus its static Base
    projection) so one scene gates the Base view and four exact-nanosecond
    samples, at two sizes.
  • The tiger renders --strict, exit 0, zero declared degradations, both
    admissions byte-identical, 96.27% of pixels byte-identical to Chromium. It is
    AGPL and stays untracked, so it is reported as an observation, not a gate —
    the committed corpus is what reproduces from a clean checkout.
  • The refusal corpus is enumerated and gated. unsupported/ is read from
    disk against a declared table: every fixture must depart by name in both
    admissions — seven document-level refusals, thirteen attributable declarations,
    no third outcome. The property is the invariant stated over a directory:
    nothing there renders silently.

Deliberate divergences, each measured

  • The arc refuses by name. Chromium rasterizes A as the ellipse's
    rational conics (measured byte-identical to <ellipse>); Blink's cubic
    normalizer, rendered by Chromium itself, differs from Chromium's own arc by 77
    pixels at delta 170. The arc rung must emit conics, so the contract carries no
    arc rather than a wrong lowering.
  • A malformed d refuses the whole path at its byte offset, where Chromium
    renders the valid prefix.
  • Length units without a basis (vw, ex, …) refuse rather than compute
    against the cascade's pinned device.
  • A non-butt stroke-linecap on a path mixing open and closed contours
    refuses
    , because one paint cannot carry a cap per contour and both
    alternatives are measurably wrong.

Three defects found and closed inside this PR

All three were silent wrong pixels — the one thing this programme does not
allow — and all three were in code this PR would merge.

A closed contour was stroked with a cap. Re-measuring moved the whole
diagnosis: not StrokeRec::apply_to_path (a centred stroke uses Skia's native
stroker, the one Blink drives), not the fill (stroke-only diverges identically),
and it tracks device width rather than authored. The first fix covered
<path> only and left <circle>/<ellipse> diverging 84–95 px of 2304; the
measurement now spans every painter arm, and shows draw_rect never took the
thin-stroke path while <line> must keep its cap because Chromium's line caps
are genuinely not invariant. Seven corpus cells cover the intersection.

A <style> sheet could smuggle a basis-less stroke-width, and so could a
capitalised one. stroke-width="2ex" refused; <style>rect{stroke-width:2ex}
rendered at 16 units; style="STROKE-WIDTH:1vw" rendered 12.8 units where
Chromium paints 0.64. All three spellings now depart by name.

Seven capability claims had gone stale — the compiler's own module doc still
declared the pre-rung slice, the oracle gate claimed unconditional byte-exactness
while six rows carry tolerances. Each is corrected against a measurement.

One CI gate amended, deliberately and in the open

The Cargo.lock additions-only gate compared the two lockfiles as text. That
is the right invariant for a crate cut but cannot express a decision whose
subject is dependency provenance — and this branch carries the Stylo pin.

Compliance was attempted before the rule was touched: all seven non-Stylo version
bumps were offered back to cargo with --precise, and cargo refused all seven,
naming the forcing edge each time. The gate now compares the whole coordinate —
version, source and checksum — and that immediately found what the old
comparison had been passing: servo_arc and to_shmem_derive moved from
crates.io to the git pin at an unchanged version
, which a name-and-version
check reports as no change at all. A dropped package and a same-coordinate
re-publish now fail unconditionally; a move that changes where a package comes
from fails unless the ledger spells both sources out. Twelve controls, ten of
them negative.

Gates

Run on this revision: cargo fmt --all --check, cargo clippy --no-deps (0
diagnostics), pnpm fmt:check, cargo doc -p websem (0 warnings), the full
cargo test, the legacy pixel sweep (65/65 byte-identical to main), the
scoreboard v0 shape check, the seam-architecture locks, and the Cargo.lock
gate plus its own controls.

Reading order

69 commits, ordered, each rung a self-contained triple. Start with
docs/wg/consolidation/svg-engine-of-record.md — its Current state section
says what is true at the tip without reconstructing it from the dated record
below. Then the rungs in commit order.

Record the owner's 2026-07-21 direction as a compact, code-agnostic
doctrine delta so later sessions stop forcing Web semantics into n0.

The topology becomes "one engine kernel, source-native semantic models,
one provisional resolved render contract, many hosts." The word that
changes is *model*: sources converge downstream at a shared render
contract, not upstream at one universal authored model. HTML/CSS + SVG
lead as one Web semantic family (one namespace-aware document, one
browser-grade cascade; standalone SVG is a different grammar entry into
the same machinery).

The amendment fixes the shared-boundary field discipline (derived frame
data only: identity/provenance, geometry+bounds, transforms/clips/masks/
effects/isolation/painter-order, ordered paint stacks, shaped-text,
resource refs + env revisions; never source syntax, ASTs, n0 ops, raw
XML, backend objects, or round-trip promises), draws the new-path
forbidden edges (no legacy import adapters, no legacy node model, no
backend escape hatch around the kernel, adopt-by-contract-not-copy), and
names the three questions deferred to a later evidence spike (n0's join
point, a generic frontend trait, the leaf-vocabulary seat).

What it supersedes: the "one model" reading of goal.md and the charter's
phase ordering where they conflict. What it does NOT supersede:
patrol-before-drop, oracle discipline, frozen surfaces, the two gate
classes, and the FLIP seal.

Wired into the cluster: index.md callout + superseding banners atop
goal.md and topology.md.

Gate evidence (docs step): git diff --check clean; all cross-links
resolve to existing sibling docs; no code paths touched; docs-wg
discipline (code-agnostic doctrine delta, defers to golden docs, states
only the delta, cluster hub updated).
…nt-end

The first internal, breakable architecture step of the Web-first track
(docs/wg/consolidation/web-first.md). Three real producers reach one
source-neutral downstream:

  HTML-with-inline-SVG ─┐
  standalone SVG ───────┼─▶ rframe::Frame ─▶ drawlist ─▶ painter ─▶ pixels
  n0 (canary) ──────────┘   (source-neutral   (private)   (one Skia
                             resolved contract)            module)

- crates/rframe: the provisional source-neutral resolved render contract
  (`Frame`, Skia-free), its private drawlist (Skia-free), and the one Skia
  painter (replays the drawlist; records no SkPicture). Adopts neither cg
  nor the n0 vocabulary — the leaf-vocabulary seat is a deferred owner
  decision, kept open. Geometry reuses math2.
- crates/websem: the Web semantic front-end. One namespace-aware document
  (csscascade::DemoDom) + one Stylo cascade + one SVG semantic compiler.
  Inline SVG is compiled in place from the shared document (no
  serialize-and-reparse); its descendant style comes from the surrounding
  cascade. Standalone SVG is a second grammar entry into the same machinery.
- n0 canary (rframe/tests/n0_canary.rs): a one-rect n0-model document is
  resolved by n0's own resolver and lowered into the same rframe::Frame →
  drawlist → painter. Proves source-neutrality; adds no n0 XML features.
- fixtures/web-first: the fixtures + a committed Chromium oracle (DSF=1)
  + provenance + a reproducible bake script.

KEY FINDING (the next crux). The workspace's Stylo is the *servo* engine,
which drops the gecko-only SVG paint longhands (`fill`, `stroke`, …) — they
are absent from ComputedValues, so `fill` cannot come from the shared
cascade. Verified empirically and from source. The cascade DOES carry
`color`, which crosses the HTML→inline-SVG boundary; the fixture uses
`color` + SVG `fill="currentColor"` to demonstrate the exact owner intent
honestly. Making the shared cascade model SVG paint properties is an owner
decision (gecko engine vs custom-property registration), filed, not shimmed.

Patrol ledger (captured essence — legacy know-how referenced, not copied):
- Scope: the legacy Web render path is crates/grida/src/htmlcss (+ /svg).
  It (a) serialize-reparses inline SVG, (b) uses a temporary SVG-only CSS
  matcher instead of Stylo, (c) outputs an SkPicture. None of that is
  reused; the new path is clean-room and avoids all three.
- Caveats re-homed: the SVG rect→paint semantics (Blink-modeled in the
  legacy renderer) are re-derived minimally against the contract; the
  "replay directly, no SkPicture" painter discipline (n0::paint) is adopted
  as a principle, painter written fresh.
- Deliberate drops (fail explicitly, never shimmed): gradients, strokes,
  masks, clips-by-clipPath, filters, text, `<use>`, markers — every
  unsupported construct returns an explicit websem::CompileError.
- Nothing legacy deleted or replaced; this only adds.
- Known caveat: csscascade's process-global document slot forces a compile
  mutex (fights "many hosts"); documented, parked.

Gate evidence (proportionate to the slice; no scoreboard score produced —
the sealed scoreboard is never invoked):
- Rust tests: `cargo test -p rframe -p websem` — 8 passed, 0 failed.
- Dependency-direction/architecture: rframe contract+drawlist locked
  Skia-/producer-free; websem locked off legacy import/node/fbs/backend/IO.
- Equivalence: inline-SVG-in-HTML and standalone SVG compile to a
  byte-identical SVG-local rframe::Frame.
- Reftest vs a committed Chromium oracle (DSF=1, integer-aligned 64×64):
  rframe's render probes exactly to Chromium's pixels; no similarity score.
- Determinism: two renders are byte-identical.
- n0 canary renders green through the same drawlist + painter.
- cargo fmt --check clean; cargo clippy --no-deps clean; git diff --check clean.
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nothing Ready Ready Preview, Comment Jul 27, 2026 4:39am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 203 files, which is 103 over the limit of 100.

To get a review, narrow the scope:
• coderabbit review --committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d9765cc9-3377-4640-90dc-daec7bfe80f9

📥 Commits

Reviewing files that changed from the base of the PR and between 58902b2 and bab3ad9.

⛔ Files ignored due to path filters (190)
  • Cargo.lock is excluded by !**/*.lock
  • fixtures/test-svg/probe/circle-fill-probe.svg is excluded by !**/*.svg
  • fixtures/test-svg/probe/path-fill-probe.svg is excluded by !**/*.svg
  • fixtures/test-svg/probe/polygon-fill-probe.svg is excluded by !**/*.svg
  • fixtures/web-first/animation/chromium/svg-rect-x-animation/base.png is excluded by !**/*.png
  • fixtures/web-first/animation/chromium/svg-rect-x-animation/sample-0ns.png is excluded by !**/*.png
  • fixtures/web-first/animation/chromium/svg-rect-x-animation/sample-1000000000ns.png is excluded by !**/*.png
  • fixtures/web-first/animation/chromium/svg-rect-x-animation/sample-2000000000ns.png is excluded by !**/*.png
  • fixtures/web-first/animation/chromium/svg-rect-x-animation/sample-3000000000ns.png is excluded by !**/*.png
  • fixtures/web-first/animation/chromium/svg-scene-cub/base.png is excluded by !**/*.png
  • fixtures/web-first/animation/chromium/svg-scene-cub/sample-0ns.png is excluded by !**/*.png
  • fixtures/web-first/animation/chromium/svg-scene-cub/sample-1000000000ns.png is excluded by !**/*.png
  • fixtures/web-first/animation/chromium/svg-scene-cub/sample-2000000000ns.png is excluded by !**/*.png
  • fixtures/web-first/animation/chromium/svg-scene-cub/sample-3000000000ns.png is excluded by !**/*.png
  • fixtures/web-first/animation/svg-rect-x-animation.svg is excluded by !**/*.svg
  • fixtures/web-first/animation/svg-rect-x-base.svg is excluded by !**/*.svg
  • fixtures/web-first/animation/svg-scene-cub-animation.svg is excluded by !**/*.svg
  • fixtures/web-first/animation/svg-scene-cub-base.svg is excluded by !**/*.svg
  • fixtures/web-first/chromium/html-inline-svg-currentcolor-rect.png is excluded by !**/*.png
  • fixtures/web-first/chromium/html-webpage-mockup.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-circle-defaults-clip.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-circle-fill.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-circle-viewbox-scaled.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-circle-zero-r.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-currentcolor-rect.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-ellipse-auto-rx.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-ellipse-fill.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-ellipse-negative-rx-auto.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-fill-inherited-rect.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-fill-invalid-initial-rect.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-fill-named-rect.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-fill-none-rect.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-group-inherited-fill.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-group-nested-transforms.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-group-paint-order.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-group-rotate-diagonal.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-group-rotate-quarter.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-group-transform-translate.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-non-rendering-elements.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-path-closed-move-only-contour.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-path-cubic-fill.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-path-draws-nothing.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-path-fill-rule-evenodd.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-path-fill-rule-inherited.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-path-fill-rule-nonzero.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-path-hv-shorthand.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-path-in-scaled-group.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-path-polygon-fill.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-path-quadratic.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-path-relative-commands.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-path-smooth-cubic.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-path-two-subpaths.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-path-unclosed-fill.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-preserve-aspect-ratio-align-max-meet.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-preserve-aspect-ratio-explicit.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-preserve-aspect-ratio-none-stretch.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-preserve-aspect-ratio-slice-clip.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-shape-transform-matrix.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-sizing-auto-rect.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-cap-butt.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-cap-circle-round.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-cap-circle-square.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-cap-closed-butt.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-cap-closed-round.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-cap-closed-square.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-cap-ellipse-round.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-cap-ellipse-square.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-cap-round.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-cap-square.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-circle.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-default-width.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-ellipse.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-inherited.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-invalid-width.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-join-bevel.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-join-miter.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-join-round.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-length-units.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-line-fill-never-paints.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-line.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-miter-limit.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-nonuniform-scale.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-over-fill.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-path-closed.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-path-open.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-rect-centred.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-scaled-group.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-zero-extent-rect.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-zero-length-dot.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-stroke-zero-width.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-style-attribute-fill-rect.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-style-element-fill-rect.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-viewbox-only-sizing-rect.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-viewbox-unequal-default.png is excluded by !**/*.png
  • fixtures/web-first/chromium/svg-viewbox-uniform-offset-rect.png is excluded by !**/*.png
  • fixtures/web-first/svg-circle-defaults-clip.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-circle-fill.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-circle-viewbox-scaled.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-circle-zero-r.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-currentcolor-rect.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-ellipse-auto-rx.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-ellipse-fill.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-ellipse-negative-rx-auto.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-fill-inherited-rect.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-fill-invalid-initial-rect.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-fill-named-rect.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-fill-none-rect.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-group-inherited-fill.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-group-nested-transforms.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-group-paint-order.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-group-rotate-diagonal.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-group-rotate-quarter.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-group-transform-translate.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-non-rendering-elements.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-path-closed-move-only-contour.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-path-cubic-fill.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-path-draws-nothing.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-path-fill-rule-evenodd.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-path-fill-rule-inherited.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-path-fill-rule-nonzero.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-path-hv-shorthand.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-path-in-scaled-group.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-path-polygon-fill.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-path-quadratic.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-path-relative-commands.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-path-smooth-cubic.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-path-two-subpaths.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-path-unclosed-fill.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-preserve-aspect-ratio-align-max-meet.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-preserve-aspect-ratio-explicit.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-preserve-aspect-ratio-none-stretch.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-preserve-aspect-ratio-slice-clip.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-shape-transform-matrix.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-sizing-auto-rect.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-cap-butt.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-cap-circle-round.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-cap-circle-square.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-cap-closed-butt.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-cap-closed-round.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-cap-closed-square.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-cap-ellipse-round.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-cap-ellipse-square.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-cap-round.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-cap-square.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-circle.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-default-width.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-ellipse.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-inherited.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-invalid-width.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-join-bevel.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-join-miter.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-join-round.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-length-units.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-line-fill-never-paints.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-line.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-miter-limit.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-nonuniform-scale.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-over-fill.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-path-closed.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-path-open.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-rect-centred.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-scaled-group.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-zero-extent-rect.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-zero-length-dot.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-stroke-zero-width.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-style-attribute-fill-rect.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-style-element-fill-rect.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-viewbox-only-sizing-rect.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-viewbox-unequal-default.svg is excluded by !**/*.svg
  • fixtures/web-first/svg-viewbox-uniform-offset-rect.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-path-arc.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-path-css-d-property.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-path-malformed-d.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-path-marker-end.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-path-no-leading-moveto.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-path-pathlength.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-path-trailing-dot-number.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-preserve-aspect-ratio-case-folded.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-preserve-aspect-ratio-defer.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-preserve-aspect-ratio-invalid-align.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-stroke-dasharray.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-stroke-opacity.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-stroke-paint-order.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-stroke-percentage-width.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-stroke-sheet-unit-width.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-stroke-vector-effect.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-viewbox-invalid-token.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-viewbox-repeated-comma.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-viewbox-trailing-comma.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-width-percentage.svg is excluded by !**/*.svg
📒 Files selected for processing (213)
  • .agents/skills/dev-render-htmlcss-feature/SKILL.md
  • .agents/skills/dev-render-htmlcss-svg-feature/SKILL.md
  • .agents/skills/render-reftest/SKILL.md
  • .github/workflows/consolidation-gates.yml
  • .gitignore
  • AGENTS.md
  • Cargo.toml
  • README.md
  • bin/check-cargo-lock-additions-only
  • bin/check-legacy-pixel-sweep
  • bin/test-check-cargo-lock-additions-only
  • crates/animation-sampling/Cargo.toml
  • crates/animation-sampling/README.md
  • crates/animation-sampling/src/lib.rs
  • crates/animation-sampling/tests/architecture.rs
  • crates/animation-sampling/tests/sampling.rs
  • crates/cg/src/types.rs
  • crates/csscascade/ARCHITECTURE.md
  • crates/csscascade/Cargo.toml
  • crates/csscascade/README.md
  • crates/csscascade/examples/exp_impl_telement.rs
  • crates/csscascade/examples/html2html.rs
  • crates/csscascade/examples/print_rcdom.rs
  • crates/csscascade/examples/print_tree.rs
  • crates/csscascade/examples/resolve_and_print.rs
  • crates/csscascade/src/adapter.rs
  • crates/csscascade/src/cascade.rs
  • crates/csscascade/src/dom.rs
  • crates/csscascade/src/lib.rs
  • crates/csscascade/src/main.rs
  • crates/csscascade/src/rcdom/mod.rs
  • crates/csscascade/src/tree/mod.rs
  • crates/csscascade/tests/document_sessions.rs
  • crates/csscascade/tests/servo_svg_properties.rs
  • crates/csscascade/tests/single_implementation.rs
  • crates/csscascade/tests/stylo_dependency_provenance.rs
  • crates/csscascade/tests/svg_presentation_hints.rs
  • crates/csscascade/tests/xml_document_entry.rs
  • crates/grida/Cargo.toml
  • crates/grida/assets/css/grida-markdown.css
  • crates/grida/src/formats/README.md
  • crates/grida/src/htmlcss.rs
  • crates/grida/src/htmlcss/frontend.rs
  • crates/grida/src/htmlcss/svg/dom/mod.rs
  • crates/grida/src/import/html/from_styled.rs
  • crates/grida/src/import/html/mod.rs
  • crates/grida/src/lib.rs
  • crates/grida/src/runtime/font_repository.rs
  • crates/grida/tests/goldens/html-import/inline-elements.snap.txt
  • crates/grida/tests/goldens/html-import/paint-border-image.snap.txt
  • crates/grida/tests/goldens/html-import/replaced-img.snap.txt
  • crates/grida/tests/html_import_snapshot.rs
  • crates/grida_dev/AGENTS.md
  • crates/htmlcss/Cargo.toml
  • crates/htmlcss/README.md
  • crates/htmlcss/assets/css/grida-markdown.css
  • crates/htmlcss/src/collect.rs
  • crates/htmlcss/src/faux_table.rs
  • crates/htmlcss/src/frontend.rs
  • crates/htmlcss/src/github_markdown.rs
  • crates/htmlcss/src/layout.rs
  • crates/htmlcss/src/lib.rs
  • crates/htmlcss/src/paint.rs
  • crates/htmlcss/src/style.rs
  • crates/htmlcss/src/styled_dom.rs
  • crates/htmlcss/src/svg/README.md
  • crates/htmlcss/src/svg/context.rs
  • crates/htmlcss/src/svg/dom/attrs.rs
  • crates/htmlcss/src/svg/dom/element.rs
  • crates/htmlcss/src/svg/dom/href.rs
  • crates/htmlcss/src/svg/dom/mod.rs
  • crates/htmlcss/src/svg/dom/parser.rs
  • crates/htmlcss/src/svg/dom/path_d.rs
  • crates/htmlcss/src/svg/error.rs
  • crates/htmlcss/src/svg/geometry/basic_shape.rs
  • crates/htmlcss/src/svg/geometry/mod.rs
  • crates/htmlcss/src/svg/layout/bbox.rs
  • crates/htmlcss/src/svg/layout/layout_svg_element.rs
  • crates/htmlcss/src/svg/layout/mod.rs
  • crates/htmlcss/src/svg/layout/transform.rs
  • crates/htmlcss/src/svg/layout/viewport.rs
  • crates/htmlcss/src/svg/mod.rs
  • crates/htmlcss/src/svg/paint/clip_path_clipper.rs
  • crates/htmlcss/src/svg/paint/effects.rs
  • crates/htmlcss/src/svg/paint/mod.rs
  • crates/htmlcss/src/svg/paint/scoped_svg_paint_state.rs
  • crates/htmlcss/src/svg/paint/svg_container_painter.rs
  • crates/htmlcss/src/svg/paint/svg_image_painter.rs
  • crates/htmlcss/src/svg/paint/svg_marker_painter.rs
  • crates/htmlcss/src/svg/paint/svg_object_painter.rs
  • crates/htmlcss/src/svg/paint/svg_painter.rs
  • crates/htmlcss/src/svg/paint/svg_root_painter.rs
  • crates/htmlcss/src/svg/paint/svg_shape_painter.rs
  • crates/htmlcss/src/svg/paint/svg_text_painter/mod.rs
  • crates/htmlcss/src/svg/paint/svg_text_painter/shaping.rs
  • crates/htmlcss/src/svg/paint/svg_use_painter.rs
  • crates/htmlcss/src/svg/paint/visibility.rs
  • crates/htmlcss/src/svg/resources/cache.rs
  • crates/htmlcss/src/svg/resources/clipper.rs
  • crates/htmlcss/src/svg/resources/filter.rs
  • crates/htmlcss/src/svg/resources/gradient.rs
  • crates/htmlcss/src/svg/resources/masker.rs
  • crates/htmlcss/src/svg/resources/mod.rs
  • crates/htmlcss/src/svg/resources/paint_server.rs
  • crates/htmlcss/src/svg/resources/pattern.rs
  • crates/htmlcss/src/svg/resources/svg_filter_builder.rs
  • crates/htmlcss/src/svg/resources/svg_resource_container.rs
  • crates/htmlcss/src/svg/resources/svg_resources.rs
  • crates/htmlcss/src/svg/style/cascade.rs
  • crates/htmlcss/src/svg/style/mod.rs
  • crates/htmlcss/src/svg/style/stylesheet.rs
  • crates/htmlcss/src/svg/style/stylo_bridge.rs
  • crates/htmlcss/src/types.rs
  • crates/htmlcss/tests/architecture.rs
  • crates/htmlcss/tests/svg_architecture.rs
  • crates/htmlcss/tests/svg_checkpoint1.rs
  • crates/n0-model/Cargo.toml
  • crates/n0-model/README.md
  • crates/n0-model/src/animation.rs
  • crates/n0-model/src/model.rs
  • crates/n0-model/src/svg_animation.rs
  • crates/n0-model/tests/animation.rs
  • crates/n0-model/tests/paint_rfd_conformance.rs
  • crates/n0/ANIMATION.md
  • crates/n0/Cargo.toml
  • crates/n0/README.md
  • crates/n0/src/cache.rs
  • crates/n0/src/cache_drawlist_spike.rs
  • crates/n0/src/damage.rs
  • crates/n0/src/drawlist.rs
  • crates/n0/src/drawlist_vector_join_spike.rs
  • crates/n0/src/glyphless.rs
  • crates/n0/src/lib.rs
  • crates/n0/src/paint.rs
  • crates/n0/tests/architecture.rs
  • crates/n0/tests/cache.rs
  • crates/n0/tests/damage.rs
  • crates/n0/tests/glyphless_canary.rs
  • crates/n0_cli/Cargo.toml
  • crates/n0_cli/README.md
  • crates/n0_cli/src/main.rs
  • crates/n0_cli/tests/architecture.rs
  • crates/rframe/Cargo.toml
  • crates/rframe/README.md
  • crates/rframe/src/frame.rs
  • crates/rframe/src/lib.rs
  • crates/rframe/src/path.rs
  • crates/rframe/src/stroke.rs
  • crates/rframe/tests/architecture.rs
  • crates/rframe/tests/solid_paint_stack.rs
  • crates/websem/Cargo.toml
  • crates/websem/README.md
  • crates/websem/src/effective_values.rs
  • crates/websem/src/lib.rs
  • crates/websem/src/svg.rs
  • crates/websem/src/svg_animation.rs
  • crates/websem/src/svg_path.rs
  • crates/websem/src/svg_transform.rs
  • crates/websem/tests/architecture.rs
  • crates/websem/tests/best_effort.rs
  • crates/websem/tests/document_sessions.rs
  • crates/websem/tests/equivalence.rs
  • crates/websem/tests/groups_contract.rs
  • crates/websem/tests/paths_contract.rs
  • crates/websem/tests/reftest_oracle.rs
  • crates/websem/tests/shapes_contract.rs
  • crates/websem/tests/standalone_xml_entry.rs
  • crates/websem/tests/strokes_contract.rs
  • crates/websem/tests/support/mod.rs
  • crates/websem/tests/svg_animation_x.rs
  • crates/websem/tests/svg_stylesheet_intake.rs
  • crates/websem/tests/typed_fill.rs
  • crates/websem/tests/unsupported_corpus.rs
  • crates/websem/tests/viewport_contract.rs
  • crates/websem/tests/webpage_mockup.rs
  • docs/wg/consolidation/charter.md
  • docs/wg/consolidation/glossary.md
  • docs/wg/consolidation/goal.md
  • docs/wg/consolidation/index.md
  • docs/wg/consolidation/lock-graph-changes.toml
  • docs/wg/consolidation/n0-join-point.md
  • docs/wg/consolidation/paint-vocabulary-gap.md
  • docs/wg/consolidation/svg-engine-of-record.md
  • docs/wg/consolidation/svg-paint-cascade.md
  • docs/wg/consolidation/topology.md
  • docs/wg/consolidation/web-first.md
  • docs/wg/consolidation/web-renderer-adoption.md
  • docs/wg/feat-2d/htmlcss-svg.md
  • docs/wg/feat-2d/htmlcss.md
  • docs/wg/feat-painting/paint-model.md
  • fixtures/css/README.md
  • fixtures/test-html/L0/README.md
  • fixtures/test-html/README.md
  • fixtures/test-html/probe/inline-svg-flex-probe.html
  • fixtures/test-html/suites/L0.coverage.json
  • fixtures/test-html/suites/L0.exact.json
  • fixtures/web-first/README.md
  • fixtures/web-first/animation/README.md
  • fixtures/web-first/animation/bake_chromium.ts
  • fixtures/web-first/animation/cases.json
  • fixtures/web-first/animation/oracle-bake.json
  • fixtures/web-first/bake_chromium.ts
  • fixtures/web-first/html-inline-svg-currentcolor-rect.html
  • fixtures/web-first/html-webpage-mockup.html
  • fixtures/web-first/oracle-bake.json
  • fixtures/web-first/pages/.gitignore
  • fixtures/web-first/pages/README.md
  • fixtures/web-first/pages/article.html
  • fixtures/web-first/pages/bake_reference.ts
  • fixtures/web-first/pages/docs.html
  • fixtures/web-first/pages/landing.html
  • fixtures/web-first/primitives.json
  • fixtures/web-first/unsupported/README.md

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/consolidation-web-first

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Turns the Web-first prototype's next crux from a one-liner into a
decision-ready finding, grounded in empirical evidence.

The crux: HTML and SVG must share one browser-grade cascade, but the
Stylo build the workspace compiles (the servo engine) omits the SVG
paint properties, so `fill`/`stroke` cannot come from the shared cascade.

Evidence gathered (throwaway spikes, not committed):
- E1: of the 46 SVG-struct longhands in Stylo 0.16, 44 are gecko-only and
  absent under servo — the whole fill/stroke/marker/stop/mask/geometry
  set; only clip-path and mask-image (shared box props) survive.
- E2: CSS custom properties DO cascade HTML->inline-SVG and read back
  under servo — but as untyped token strings (no computed-value
  processing, no currentColor/url() paint-server/type semantics).
- E3: the gecko engine pulls bindgen/mozbuild/nsstring (a Gecko build
  tree) — not viable in a pure-Rust workspace.
- E4: the SVG value types compile unconditionally under servo; only the
  property declarations are engine-gated — so a stylo fork is plausible
  but unverified at build depth and high-maintenance.

The doc lays out five options (gecko engine / stylo fork / custom-property
carrier / status-quo attribute read / track upstream) with
feasibility+cost+tradeoffs, recommends keeping the status quo near-term,
not adopting the lossy carrier as the cascade of record, and scoping a
timeboxed stylo-fork feasibility spike as the path most faithful to "one
browser-grade cascade" — and files it as an owner registry decision.

Linked from the Web-First Amendment and the cluster index.

Gate evidence (docs step): git diff --check clean; all cross-links resolve;
claims E1/E2 reproduced by parsing stylo's longhands.toml and by running a
csscascade cascade spike (both removed after); no code paths touched.
@softmarshmallow

Copy link
Copy Markdown
Member Author

Added a decision-ready finding for the identified next crux — SVG paint in the shared cascade (docs/wg/consolidation/svg-paint-cascade.md, commit ef385958).

Empirically established: 44 of 46 SVG-struct longhands in Stylo 0.16 are gecko-only and absent under the servo engine the workspace compiles — the whole fill/stroke/marker/stop/mask/geometry set (only clip-path/mask-image survive). So SVG paint cannot come from the shared cascade today; the prototype reads it from presentation attributes/inline style, outside the cascade.

The finding evaluates five options (gecko engine · stylo fork · custom-property carrier · status-quo attribute read · track upstream) with feasibility/cost, recommends keeping the status quo near-term and not adopting the lossy custom-property carrier as the cascade of record, and files how SVG paint enters the shared cascade as an owner registry decision. Still a draft; no merge without your GO.

Advances the amendment's deferred join-point crux (does n0 emit the
common resolved contract, or join only at the drawlist boundary?) into a
decision-ready gap analysis grounded in n0's real downstream types.

Reframes the binary A-vs-B into a per-fact boundary, classifying each
fact in n0's ItemKind stream:
- opacity scopes / clips / painter order / geometry / bounds: structurally
  neutral -> converge HIGH (common contract).
- paint stacks / strokes: neutral concepts carried as n0-model value types
  -> HIGH, but coupled to the (deferred) leaf-vocabulary-seat decision.
- corner smoothing (squircle): an authoring parameter resolved in n0's
  painter -> must be resolved to neutral geometry BEFORE the contract, else
  it leaks an n0/authoring concept (forbidden).
- shaped text: bound to n0's private font registry + own oracle -> the
  DECIDING fact; needs a neutral shaped-text + font-key boundary that two
  producers can emit, which does not exist yet.

Recommendation: do not take a uniform decision; record a per-fact
boundary; couple it with the leaf-vocabulary seat; and gate the deciding
text spike on a second shaped-text producer (per "two real producers
first"). Files it as an owner registry decision, coupled with the
leaf-vocabulary seat. Explicitly NOT ripe to decide now.

Linked from the amendment's "n0's join point" bullet and the cluster index.

Gate evidence (docs step): git diff --check clean; cross-links resolve;
classification grounded in crates/n0/src/drawlist.rs ItemKind and the
private TextFontRegistry; no code paths touched.
@softmarshmallow

Copy link
Copy Markdown
Member Author

Advanced the second deferred crux — where n0 joins the shared downstream (docs/wg/consolidation/n0-join-point.md, commit 4d8a78e2).

A gap analysis against n0's real ItemKind stream reframes the binary "emit the common contract vs join at the drawlist" into a per-fact boundary:

  • opacity / clips / painter-order / geometry / bounds → converge high (neutral);
  • paint stacks / strokes → high, but coupled to the deferred leaf-vocabulary seat;
  • corner smoothing (squircle) → must be resolved to neutral geometry before the contract, else it leaks an authoring parameter;
  • shaped text → the deciding fact: bound to n0's private font registry + own oracle; needs a neutral shaped-text + font-key boundary two producers can emit, which does not exist yet.

Recommendation: don't take a uniform decision; record a per-fact boundary; couple it with the leaf-vocab seat; gate the deciding text spike on a second shaped-text producer (per "two real producers first"). Explicitly not ripe to decide now. Filed as an owner registry decision. Still a draft; no merge without your GO.

Makes the prototype runnable from the command line, answering "what can we
do from a CLI today." Previously rframe+websem were library-only, exercised
only through cargo test.

- rframe::render_png(&Frame, w, h) -> Vec<u8>: the downstream plus PNG
  encoding, for a host that just wants a file to write.
- crates/websem/examples/render.rs: a thin host (goal.md's "CLI is always a
  thin host" — meaning lives in the engine). It reads an SVG or
  HTML-with-inline-SVG file, compiles via websem into the source-neutral
  rframe::Frame, renders + encodes via rframe, and writes a PNG. Argument
  parsing, file I/O, and raster size are the host's only concerns.

Usage:
  cargo run -p websem --example render -- <input.svg|input.html> <out.png> [WxH]

Verified end-to-end:
- svg-currentcolor-rect.svg   -> 64x64 PNG, all 4096 px #16a34a.
- html-inline-svg-...html     -> 128x128 PNG, the 64x64 SVG green on a
  transparent canvas.
- Unsupported constructs fail explicitly (exit 1, clear message), no shims:
  <circle> -> "unsupported element <circle>";
  fill="url(#g)" -> "unsupported fill value \"url(#g)\"".

Scope: the host is an example, not a productized CLI — a standalone
svg->png CLI host crate (goal.md's render ecosystem, #13) is
the next step if wanted. The host only exercises the current slice
(solid-fill <rect>); everything else is an explicit error.

Gate evidence: cargo fmt --check clean; cargo clippy --no-deps (lib+tests+
examples) 0 warnings; cargo test -p rframe -p websem 8 passed 0 failed;
git diff --check clean.
Answers "do we have a real-world-like HTML webpage design to render?"

Honest framing: the Web-first slice is not an HTML/CSS layout engine — the
HTML entry compiles the inline <svg> and ignores surrounding HTML (a page
with no inline SVG errors: "no <svg> element in document"). Real HTML/CSS
webpage rendering lives in the legacy crates/grida htmlcss renderer, which
the amendment forbids for this path.

What this adds is a webpage *design* (header / hero / sidebar cards / bottom
row / footer) expressed as 27 solid-fill inline-SVG <rect>s inside an HTML
document — renderable by the current slice, and exercising the real feature:
the brand color authored once in the HTML <style> `.brand` rule cascades into
the inline SVG via `fill="currentColor"`.

- fixtures/web-first/html-webpage-mockup.html — the fixture.
- crates/websem/tests/webpage_mockup.rs — a data test (no render) asserting it
  compiles to a multi-rect layout and that the brand purple (#4a3aa7, authored
  only in the HTML <style>) resolves onto an inline-SVG rect.
- Renderable end to end:
    cargo run -p websem --example render -- \
      fixtures/web-first/html-webpage-mockup.html /tmp/page.png
  -> 640x400 PNG, 27 nodes; header + hero accent are the cascaded brand purple.

Gate evidence: cargo test -p rframe -p websem 9 passed 0 failed; clippy
--no-deps 0 warnings; fmt --check clean; git diff --check clean.
Adds a seed corpus of genuine, self-contained real-world webpages that serve
as the harness the Web-first engine is measured against — the target, not a
demonstration of current capability. Rich by design (real semantic HTML, real
text content, images, real CSS); "minimal, one concept per file" is the rule
for unit fixtures, not for a real-world page corpus.

Honest framing: the Web-first engine renders NONE of these yet (websem+rframe
handle solid-fill SVG <rect> only). These are the ground it grows toward; the
ground truth is Chromium. Same relationship fixtures/test-html has with the
legacy renderer — an input corpus + a browser oracle, not a pass today.

- fixtures/web-first/pages/article.html  — blog/magazine article (hero SVG
  illustration, drop-cap, prose, blockquote, table, syntax-highlighted code,
  author card, related grid, footer).
- fixtures/web-first/pages/landing.html  — SaaS landing page (product-dashboard
  SVG, feature grid, stats band, testimonial, 3-tier pricing, gradient CTA).
- fixtures/web-first/pages/docs.html     — docs page (3-column nav/content/TOC,
  callouts, code blocks, API table, pager).
- bake_reference.ts — bakes Chromium reference renders (DSF=1, 1200px wide,
  full-page) with all http(s) aborted, so a page needing any external resource
  renders broken. README + .gitignore.

Self-containment verified: all imagery is inline <svg>, all CSS inline, system
fonts, ZERO external resources — Chromium rendered all three with the network
off and blocked 0 external requests. Full-page sizes: article 1200x6561,
landing 1200x4621, docs 1200x3929.

The reference renders are regenerable + environment-pinned (system-font text),
so they are gitignored rather than committed — the refbrowser discipline
(inputs + generator committed; browser oracles rendered on demand).

Gate evidence: self-containment audit (0 external URLs / <link> / <script src> /
@import / url(http) / non-data <img>); Chromium bake blocked 0 external
requests; git diff --check clean.
@softmarshmallow

Copy link
Copy Markdown
Member Author

Added a real-world webpage harnessfixtures/web-first/pages/ (commit 664ee83f).

Three genuine, self-contained real webpages as the target the Web-first engine grows toward (not a demo of current capability): article.html (magazine article), landing.html (SaaS landing), docs.html (docs page). Rich by design — semantic HTML, real prose, inline-SVG imagery, real CSS (grid/flex, gradients, tables, code blocks). ~27–36KB each.

Honest framing: the engine renders none of these yet (websem+rframe do solid-fill SVG <rect> only). Ground truth is Chromium — same relationship fixtures/test-html/ has with the legacy renderer.

Self-containment is verified, not assumed: zero external URLs / <link> / <script src> / @import / url(http) / non-data <img>; Chromium baked all three with the network aborted and blocked 0 external requests. Full-page: article 1200×6561, landing 1200×4621, docs 1200×3929.

bake_reference.ts regenerates the Chromium references; those renders are gitignored (regenerable + environment-pinned), following the refbrowser discipline (inputs + generator committed, browser oracles on demand). Still a draft; no merge without your GO.

Patrol / captured-essence ledger:
- Preserved the one-document/one-cascade -> Frame -> private drawlist -> painter route and the tiny n0 source-neutrality canary.
- Kept the current standalone entry only as html5ever foreign-content scaffolding; no legacy adapter, node model, grida.fbs, frozen wasm surface, or backend escape hatch was introduced or changed.
- Replaced silent malformed-token filtering and non-uniform viewBox stretching with explicit proving-shell rejections. Added five rejection fixtures; arbitrary SVG remains outside the admitted capability claim.
- Made Skia optional at the rframe boundary, kept the drawlist crate-private, and corrected identity from stable to frame-local until a real incremental producer forces provenance.

Gate evidence:
- cargo test -p rframe -p websem: 17 tests passed, including the n0 canary and 5 viewport rejection probes.
- cargo test/check -p rframe --no-default-features and RUSTDOCFLAGS=-Dwarnings cargo doc -p rframe --no-default-features --no-deps passed.
- cargo tree -p websem --edges normal -i skia-safe reported no normal backend edge.
- cargo clippy -p rframe -p websem --all-targets --no-deps, cargo fmt --all -- --check, and git diff --check passed.
- No scoreboard command or score was produced; FLIP remains sealed.
Patrol / captured-essence ledger:
- Replaced the six-pixel sample with a closed manifest and full RGBA comparison for every root primitive; retained the existing Chromium pixels and made the baker refuse to overwrite a differing baseline.
- Added committed oracles for both HTML-authored primitives and a non-zero-origin, uniform-scale viewBox fixture, preserving the inline/standalone currentColor proof.
- Kept article/landing/docs as target-only pages. The page baker now creates a fresh output directory, records encoded PNG dimensions and environment provenance, blocks external resources, and double-captures on fresh pages; it does not claim an engine reftest or page capability.

Gate evidence:
- Chromium 149.0.7827.55 verified all 4 enumerated primitive oracles twice; cargo test -p websem --test reftest_oracle passed closed enumeration, provenance hashes, zero differing RGBA pixels, and double-run CPU/PNG determinism.
- The thin CLI rendered svg-viewbox-uniform-offset-rect.svg at 80x40 through websem -> Frame -> the shared painter.
- The page baker deterministically captured article 1200x6561, docs 1200x3929, and landing 1200x4621 with zero external requests.
- pnpm -C packages/grida-reftest run typecheck, cargo fmt --all -- --check, and git diff --check passed.
- No similarity score, scoreboard run/report, or FLIP pass claim was produced.
Patrol / captured-essence ledger:
- Preserved every non-conflicting charter obligation while making the ratified Web-First Amendment sequencing explicit; the legacy-adapter route remains superseded only for the new Web path.
- Registered D-L for SVG paint in the shared cascade and D-M for the coupled leaf-vocabulary seat / per-fact n0 join point. No option, registry outcome, merge, or owner GO was inferred.
- Corrected the SVG-paint finding: the scaffold reads direct fill attributes only, SVG-namespace style collection and presentation hints are absent, and a Stylo longhand patch alone is insufficient.
- Kept the one-rectangle n0 canary quarantined as an invariant probe and the solid-fill leaf provisional; stroke, gradient, and text vocabulary remain owner-gated.

Gate evidence:
- The charter now sequences proving shell -> SVG vector -> shaped text -> resources -> CSS box/layout -> page composition with explicit entry and exit evidence.
- D-L and D-M appear consistently in the index, amendment, findings, charter registry, and glossary; all new relative link targets exist.
- git diff --check passed. This documentation commit changes no runtime behavior.
- FLIP remains unratified; no scoreboard run/report/score or capability-pass claim was produced.
Patrol ledger: preserve the Stylo cascade and computed-value mappings, Taffy layout caveats, mature SVG semantics, text/resource behavior, fixture/oracle conditions, and n0 frame/time/damage/resource invariants. Do not adopt the leaking process-global DOM, context-free Stylo handles, ambient fixed environment or Grid policy, inline-SVG serialize/reparse, the temporary SVG matcher, backend objects/pictures in semantic contracts, legacy nodes, or a third kernel. Deliberate drops: none.

Decision evidence: inventory the mature renderer, n0 chassis, and rframe proving shell as three distinct paths; correct the HTML/SVG gate census; stage D-M into vector and text evidence; define exact high/low join ownership; classify every n0 drawlist compiler read; and require an independently constructed vector-input, multi-frame, damage/cache, raster, and mixed-composition spike before owner GO.

Gate evidence: cargo test -p grida --lib -- htmlcss::tests --test-threads=1 (155 passed); cargo test -p grida --lib -- htmlcss::svg --test-threads=1 (47 passed); htmlcss SVG checkpoint/architecture and HTML import snapshot/architecture integration tests passed; pnpm fmt:check; pnpm --filter www types:check; CI=1 NEXT_TELEMETRY_DISABLED=1 pnpm --filter www build (Turbopack, 181 pages); git diff --check. Two adversarial topology reviews are clean. No production behavior changed and no scoreboard run/report/score was produced.
Patrol / captured essence:
- Move all 56 mature HTML/CSS/SVG source files and both SVG gate suites from grida into the unpublished htmlcss crate; deliberate renderer drops: none.
- Preserve Stylo cascade, Taffy layout, paragraph shaping, resource providers, SVG DOM/style/layout/paint, and direct-Skia behavior.
- Move the Markdown stylesheet byte-for-byte to its sole renderer owner and remove the stale legacy symlink.
- Keep grida as a compatibility consumer; only parse_and_style and styled_of cross the crate-private importer seam.
- Keep the normal websem edge backend-free; its thin example host now renders .html/.htm/.svg files to deterministic CPU PNGs.
- Admit no legacy node/SceneGraph/FBS or speculative n0/rframe semantics into htmlcss.

Known transitional debt:
- The mature renderer still returns SkPicture, uses a process-global Stylo slot, serializes/reparses inline SVG, and retains a standalone-SVG matcher.
- The CLI currently accepts one self-contained file, explicit raster size, and PNG output only; directories, external resources, and other formats remain parked.

Gate evidence:
- python3 bin/check-legacy-pixel-sweep 5c36067: PASS, 65/65 declared L0.exact PNGs byte-identical, corpus f1892dcdab29442e. This is an extraction A/B gate, not a Chromium conformance score.
- cargo test -p htmlcss -- --test-threads=1: 216 pass.
- cargo test -p websem: architecture/equivalence/oracle/viewport tests plus 2 discovered CLI acceptance tests pass.
- Dedicated <=3-color SVG circle and HTML flex+inline-SVG probes pass; complex L0 fixtures remain integration smoke only.
- grida importer architecture/snapshot and legacy seam locks pass; rframe/websem and n0 canaries remain green.
- Affected cargo check and clippy --no-deps, cargo fmt --all -- --check, cargo metadata --locked, and git diff --check pass.
- Normal cargo run -p websem --example render produced the pre-extraction-identical SVG PNG hash 41ad38c8e5a7dc1b1423276e2a083e13b0b1cd5bf6032d2e369369f6bb6b7e28.
- FLIP remains unratified; no scoreboard run, report, score, or landed-capability claim was made.
Patrol and boundary:
- websem is the backend-free Web semantic producer/proving shell, not the product host
- htmlcss already exposes the required render/render_svg provider seam; no producer contract change was needed
- n0_cli owns arguments, source I/O, host fonts, CPU surface, and PNG encoding only; no legacy, websem, rframe, or speculative n0 production edge

Captured essence:
- physically move the HTML/SVG executable and its strict extension/size/PNG contract to the n0 binary
- preserve system-font fallback, self-contained resource policy, transparent CPU raster, deterministic encoding, exact probes, and file round-trip
- retire the websem example while naming the direct htmlcss/SkPicture seam as transitional
- retain now-unused websem htmlcss/skia dev declarations temporarily: the mandatory crate-cut Cargo.lock additions-only gate requires their subtraction after this cut lands

Gate evidence:
- pre-move websem example: 2 tests passed
- post-move n0_cli: 2 render tests + 3 architecture locks passed
- standalone SVG, HTML with inline SVG, and landing page outputs cmp byte-identical before/after (sha256 41ad38c8..., bd1c3b85..., 93d93bea...)
- declared legacy sweep: 65/65 L0.exact PNGs byte-identical; corpus f1892dcdab29442e
- cargo test/check/clippy --locked for n0_cli, websem, and htmlcss passed
- cargo fmt --check, cargo metadata --locked, and git diff --check passed
- no scoreboard run/report/score; FLIP remains sealed
Patrol ledger:
- Exact Stylo 0.16 exposes only 2 of 46 SVG-struct longhands under Servo; Gecko imports Mozilla build dependencies outside the standalone Cargo constraint.
- An isolated Servo-only fill/stroke patch required one additional pointer-sized computed-style slot and no Gecko dependency.
- The existing cascade had three independent ingress gaps: SVG-namespace styles, presentation hints, and strict SVG/XML document entry.

Captured essence:
- Record typed paint values, defaults, inheritance, currentColor child resolution, parse-invalid versus computed-invalid behavior, and typed URL/fallback preservation without claiming resource binding.
- Record presentation precedence, document-order SVG styles, strict XML namespace/case behavior, direct SVG root, and explicit non-HTML document mode.
- Quarantine the unproven remaining longhands, production encoding/DTD/base/resource policies, and fork upgrade ownership. D-L remains owner-only.

Gate evidence:
- Patched Stylo 0.16 cargo check --no-default-features --features servo: PASS.
- HTML/inline-SVG cascade probe: native fill/stroke, precedence, inheritance, currentColor, invalid values, defaults, presentation hints, and SVG style order: PASS.
- Strict standalone SVG/XML probe: direct root, namespaces, case sensitivity, SVG style, inheritance, malformed-input rejection, non-HTML mode, and :root: PASS.
- cargo test --locked -p n0_cli: 5/5 PASS.
- cargo fmt --all --check, locked metadata, and git diff --check: PASS.
- No scoreboard run, report, score, capability claim, or registry decision.
Patrol: Stylo 0.19.0 predates servo/stylo#383. Its exact official merge a64923b5d5c67313c81c5056f5e30ec0babb04d6 supplies 11 basic Servo SVG paint longhands; 24 SVG-struct longhands remain Gecko-only. Pin every directly consumed Stylo-family crate at one immutable official source, refuse packaging while registry semantics lag, and lock provenance. No private fork, floating branch, or registry fallback.

Cut/adopt: adapt the upstream AttributeProvider, Device-environment, and Flex API changes. Declare one deterministic static-desktop pointer profile and explicitly quarantine newly parsed unsupported radial clip extents. Production SVG presentation-hint ingress and computed-paint consumption remain untouched; D-L decides dependency provenance only. The fixed 1280x720 cascade environment remains the next host seam.

Captured-essence ledger: owner GO on 2026-07-23 takes D-L for official upstream, preferring the first release containing servo/stylo#383 and using this exact merge until then. Upstream font quantization changes 8 snapshot values from 11.900001 to 11.90625 and 10 from 16.38 to 16.375; every affected change precedes the layout sections, whose bytes remain identical. No deletion or legacy behavior drop.

Gate evidence: full locked tests pass for csscascade, htmlcss, websem, n0_cli, and grida; n0-model and n0 canaries pass; the focused SVG-paint and provenance probes pass; grida-canvas-wasm checks; affected locked check and no-deps clippy pass; fmt and diff checks pass. The declared 65-fixture L0.exact A/B sweep is byte-identical, corpus f1892dcdab29442e. Basic SVG, inline-SVG HTML, and the landing page each render twice byte-identically and match their pre-pin hashes 41ad38c8, bd1c3b85, and 93d93bea. No FLIP-sealed scoreboard command was run.
Patrol ledger:
- Production and tests consume csscascade::dom + adapter + cascade only.
- The retired rcdom/tree stack had no workspace consumers; its StyleRuntime returned default values.
- The 1,897-line experiment duplicated the promoted adapter/cascade implementation.

Captured essence:
- Arena parsing remains in dom; Stylo trait integration remains in adapter; stylesheet resolution remains in cascade.
- resolve_and_print remains as the one diagnostic over the live path.
- README and ARCHITECTURE now record the actual boundary, process-global lifetime defect, and explicit-environment gap.

Deliberate drops:
- Unused RcDom wrapper, stub styled-tree/builder/serializer, four obsolete examples, and accidental Hello World binary.
- No production API consumer, fixture, rendering behavior, dependency, or lockfile entry changed.

Gate evidence:
- cargo test --locked -p csscascade --all-targets
- cargo test --locked -p htmlcss -p websem -p n0_cli -- --test-threads=1
- cargo test --locked -p grida --test html_import_architecture --test html_import_snapshot -- --test-threads=1
- cargo check --locked -p csscascade -p htmlcss -p websem -p n0_cli -p grida -p grida-canvas-wasm
- cargo clippy --locked --no-deps -p csscascade --all-targets
- cargo clippy --locked --no-deps -p htmlcss -p websem -p n0_cli -p grida
- Legacy pixel sweep: 65/65 L0.exact PNGs byte-identical; corpus f1892dcdab29442e
- Repeated CLI hashes unchanged: 41ad38c8e5a7dc1b, bd1c3b859b09e3ba, 93d93bead64e936e
- cargo fmt --all --check; git diff --check

No scoreboard run, report, or score was produced.
Patrol ledger:
- Official Stylo now exposes typed basic SVG paint, but websem still reads a direct fill attribute and htmlcss SVG still uses its temporary matcher.
- Inline SVG in the mature renderer is serialized and reparsed; standalone SVG uses the same storage type, not the shared Stylo session.
- L0.exact uses threshold 0 with AA exclusion and a content mask, so 1.0 is configured Chromium similarity rather than raw PNG byte identity.

Captured essence:
- Code comments and fixture guidance now distinguish dependency provenance from production paint ingress/consumption.
- n0_cli is named as the host of the extracted mature renderer, not evidence of chassis convergence.
- The legacy A/B sweep remains genuinely byte-for-byte and now hashes canonical render-relevant suite semantics plus every declared input byte.

Deliberate drop:
- Only the human-facing suite description is excluded from corpus identity. Every other existing or future manifest key remains fail-closed in the canonical hash.
- No render semantics, fixture input, oracle, threshold, dependency, or output changed.

Gate evidence:
- cargo test --locked -p websem -p htmlcss -p n0_cli -- --test-threads=1
- cargo clippy --locked --no-deps -p websem -p htmlcss -p n0_cli
- Legacy pixel sweep vs ee2901c: 65/65 L0.exact PNGs byte-identical; semantic corpus 3a55fc5e17844af6
- JSON parse checks; check-legacy-pixel-sweep --help
- cargo fmt --all --check; git diff --check

No scoreboard run, report, or score was produced.
The resolved contract grows its second geometry kind and websem compiles the
SVG2 path-data grammar into it. Every answer in that grammar was measured
against Chromium 149 rather than read off a spec, and two constructs refuse by
name because measurement said they must.

rframe gains a checked `PathData`: a canonical absolute command stream
(move/line/quad/cubic/close), the fill rule, the tight local bounds solved
once so producer and consumer cannot disagree, and whether every contour
closed. It carries no arc command — an arc is authored syntax whose lowering
decides the pixels, so that choice stays with the producer that can verify it
against a browser — and no rational conic, because the one producer that needs
one is not admitted yet.

websem's `svg_path` resolves the grammar with the donor's question list and
Chromium's answers: a number consumes at most one trailing comma, a dot must
carry a digit, and the canonical form is reached by normalizations each
measured pixel-neutral *on anti-aliased geometry* first. `fill-rule` is
consumed as a typed cascaded value, so SVG2 precedence, inheritance and
keyword case-insensitivity come from the one cascade; csscascade admits it as
a presentation hint.

Two deliberate divergences, both declared. A malformed `d` refuses the whole
path at its byte offset instead of rendering Chromium's valid prefix. And the
elliptical arc refuses: Chromium rasterizes an arc through the same conics as
an `<ellipse>` (byte-identical over every shared row), while Blink's cubic
*normalizer* — rendered by Chromium itself — differs from Chromium's own `A` by
77 pixels at up to a 170-per-channel delta.

Patrol gaps this rung had to close before it could admit anything: the CSS `d`
property (Chromium paints a stylesheet's `d: path(…)` in place of the
attribute), the marker properties, `vector-effect`, `shape-rendering`, CSS
motion path, the `all` shorthand, and vendor aliases of names already listed.
The CSS scan now strips comments and vendor prefixes, refuses an escaped
property name outright, and reads the sheet the cascade actually compiles
rather than each text node — every one of those was a measured leak.
Thirteen new cells, all byte-exact: one triangle spelled four ways (closed,
unclosed, relative, H/V — four byte-identical oracles), a cubic, an `S`
continuation, a `Q`+`T` pair, both fill rules on a self-intersecting star plus
the rule inherited from a `<g>`, two subpaths, a path in a scaled group, and a
`d` that draws nothing.

The corpus's declared departure narrows again. Straight edges already agreed
byte-exactly under rotation; now the cubic, smooth-cubic and quadratic cells do
too. Curves as such are not the boundary — the weighted rational conic is, and
no path cell carries a tolerance.

`svg-path-closed-move-only-contour.svg` earns its place: `M x y Z` is a
zero-length *closed* contour, and dropping it moves 96 pixels of the surviving
triangle. Its coordinates are fractional on purpose, because the integer
axis-aligned geometry the rest of the corpus uses hides exactly this class of
difference.

`unsupported/` gains the arc, a malformed `d`, path data with no leading
moveto, a trailing-dot number, a stylesheet `d`, `pathLength`, and a real
`<marker>` reference — with each row saying whether its refusal is
over-refusal (`pathLength`) or load-bearing (`marker-*`).
Records what was measured rather than what was expected: the arc's conic
finding (Chromium's `A` is byte-identical to its `<ellipse>`, and Blink's cubic
normalizer is not what Chromium paints), the AA boundary narrowing a third time
to the weighted rational conic alone, the six CSS patrol leaks, and the
corrected account of which Stylo properties are gated and how.

The sharpest entry is the round's own finding: `M x y Z` is a zero-length closed
contour, dropping it moves up to 96 pixels of the *surviving* geometry, and the
law that blessed the drop passed only because its coordinates were integer and
axis-aligned. A fuzz of 40 fractional-coordinate triangles diverged 40 out of
40. The claim "measured, not assumed" was the defect — the measurement had been
taken on geometry that cannot show the difference.

One finding is recorded rather than fixed: a SMIL animation of a consumed
attribute is active at load in Chromium while a Base render paints the authored
value and declares nothing. Systemic, predates this rung, and gets its own.
…okes rung

A stroke becomes a second painted fact per node, and the tiger's last blocker
goes with it.

rframe gains a `Stroke`: paints, a width in the node's local space, a cap, a
join, a miter limit — and two deliberate absences. There is no alignment field,
because a Web stroke straddles its geometry and that is the only alignment a Web
source can express; an aligned stroke grows the type when a producer needs one.
And there is no invisible stroke: construction resolves a zero width or an empty
paint stack to `Ok(None)`, so no consumer re-derives "is this visible".
`FrameNode.bounds` stays the *geometry's* bounds — ink lies outside it, and
`Stroke::outset()` states how far, counting both the join's miter room and the
square cap's corner at radius·√2.

websem reads every stroke property as a typed cascaded value, which buys `8px`,
`0.5em`, inheritance through a `<g>`, stylesheet overrides, keyword
case-insensitivity, and the right fallback for a negative width (the declaration
fails its non-negative grammar, so the cascade drops it — which is what Chromium
paints). csscascade admits the five stroke properties as presentation hints,
plus `font-size`, which nothing paints yet but which is the basis an `em` length
resolves against; a bare-number attribute value is retried as `px`, reproducing
the SVG attribute mode Blink parses these in.

`<line>` needed no geometry kind: it compiles to a two-command path. Chromium's
`<line>` is byte-identical to the equivalent `<path>` (measured), a line has no
interior for a fill to cover, and the caps, joins and zero-length rules then
come out identical for free.

Two rules the measurements forced. A zero-extent `<rect>`/`<circle>`/`<ellipse>`
disables rendering of the element *including its stroke* — a naive stroke of a
zero-extent box would draw a line — while a `<path>` is exempt, because a
zero-extent path is a zero-length segment that strokes as a dot. And a dash
array that would paint nothing (`none`, all-zero, invalid) is admitted rather
than refused, because Chromium renders those solid.

What still refuses, by name: `stroke-opacity` and dashing (the compositing and
dashing halves), a percentage width (the normalized-diagonal basis), a width in
a viewport-relative or font-metric unit (this build's device is pinned at
1280x720 and its font metrics are placeholders, so `1vw` computed 12.8 where
Chromium paints 0.64), a width whose reach cannot be represented, and paint
servers and context paints. A negative box extent now disables that one element
instead of aborting the whole render, which is what Chromium does.
Twenty-four cells (corpus 46 → 70), 23 of them byte-exact: the centred pen, the
stroke over the fill, the cascaded width in every spelling, inheritance through
a `<g>`, a stroke on every admitted geometry, all three caps including the
zero-length dot, all three joins, the miter-limit bevel, the elliptical pen a
non-uniform scale produces, a `<line>` whose fill paints nothing, and a
zero-extent rect that paints nothing at all.

The one exception is the round join at a *closed* contour's closing corner: 4
pixels at delta 3, declared with that join's arc as its boundary. Every cap,
every other join, and both the stroked circle and ellipse are byte-exact — so
strokes land inside the corpus's declared rational-conic departure and mostly
below it.

`unsupported/` gains the five refusals: `stroke-opacity`, dashing, a percentage
width, `vector-effect` and `paint-order`. The last two were provably inert while
strokes refused and became load-bearing the moment strokes painted — which is
the reason the earlier rungs kept them patrolled.
… register

The tiger renders: 240 paths, 241 groups, --strict, exit 0, zero declared
degradations, both admissions byte-identical, and 96.27% of its pixels
byte-identical to Chromium's render of the same file. It needed exactly what the
last four rungs admitted and nothing else, which is what makes it a milestone
rather than a target.

Records the rung's own findings alongside it: the contract's second painted
fact and the two absences that keep it honest, `<line>` as a two-command path,
the zero-extent rule as an *element* rule, and the dash array that paints
nothing being admitted because Chromium renders it solid.

The adversarial round's three unit-basis defects get their own paragraph,
because they are one lesson: this rung consumed the first cascaded *length*, and
a length is only as good as a basis this build may not have. A viewport-relative
width computed 12.8 where Chromium paints 0.64; `ex` resolved from placeholder
font metrics; and `em` was right from a stylesheet and wrong from a presentation
attribute. My law had tested only unitless, `px` and `em`-at-the-default.

The tiger file itself is deliberately not committed: it is AGPL and this repo is
Apache-2.0/MIT, which is what fixtures/local/ exists for. Capability stays gated
by the committed byte-exact corpus; the tiger is a measurement, reproducible
from one command.
…nd sampled

The tiger proved the rung ladder end to end but cannot be committed: it is
AGPL and lives untracked in fixtures/local/. So the capability it demonstrated
had no committed cell, and nothing in the corpus exercised the rungs
*together* — 70 cells, each isolating one construct.

The cub is an original drawing covering the same feature list in 17
materialized nodes instead of 240: a viewBox-only root, a container with a
transform and a nested <g>, fill/stroke/stroke-width/stroke-linejoin/
stroke-linecap inherited through both, cubics and quadratics, relative command
runs, multi-subpath paths, round caps and joins, fill="none" and stroke="none",
rects in a group, and a <line>. It renders --strict with zero declared
degradations at zero differing pixels, and it is drawn deliberately without a
<circle> or <ellipse> so the declared conic departure stays where the strokes
rung left it: near-elliptical shapes are cubic approximations, which bake
byte-exact.

It ships as a pair — the animated document and its static Base projection — so
one scene gates both the Base view and four exact-nanosecond samples. Five
frames, all byte-identical to Chromium.

The sampling corpus is therefore now plural: cases.json carries a fixtures
array (schema 1) with each fixture's declared frame shape, the baker loops it
with a per-fixture initial viewport and validates each declaration
structurally — including that a fixture's authored value differs from its first
sample, so Base and Sample(0ns) can never silently coincide — and each
fixture's oracles live under chromium/<id>/. The rect-x pixels moved
directories and the bake re-verified them byte-identical, which is the proof
the rename lost nothing.
…shape

The suite laws now iterate cases.json's fixtures, taking each fixture's
declared dims as its initial viewport (the cub's root is viewBox-only, so
passing anything else would render a differently-sized scene than the oracle)
and reading its animated node by the declared index rather than a hard-coded
nodes[1]. The declaration is load-bearing: a fixture that stops materializing
an element, or that moves its animated rect, fails at the node-count assertion
instead of quietly weakening every law below it.

The damage law generalizes to "one node changed, over the union of where it was
and where it went", computed from the frames themselves; the rect-x rectangle
stays pinned by hand beside it, because a union derived from the same frames
the diff came from would be satisfied by a bounds regression that moved both.

The host proof adds the property a composition makes checkable: every pixel
that differs between the cub's Base frame and a sample lies in the animated
rect's own rows. Sixteen nodes of curves and strokes render identically at
every time, both admissions are byte-identical, and the frozen pair is the same
frame. Negative controls: perturbing the declared node index, and a one-digit
fill change in the source, each fail the laws they should.

Also drops an unused import the groups rung left in groups_contract.rs — the
only warning in the test gate.
…found

The addendum records what the scene fixture is for, what its two absences say
about the admitted slice (the animated element must be a top-level rect; a
sampled document can reach currentColor by neither door, since <style> and
style= are dynamic-inventory blockers and color= is a declared boundary), and
why it carries no <circle> or <ellipse>.

It also records a defect found while verifying the cub at a second size. The
scene gates byte-exact at 96x96; at 48x48 — a 1x viewport mapping — 242 of 2304
pixels differ from Chromium. Bisected: a closed contour with a non-butt cap at a
~1-device-pixel stroke width. A cap cannot exist on a closed contour, and
Chromium's raster is cap-invariant there (measured: 0 differing pixels between
its own butt and round captures); this engine's is not, because it paints a
stroke by filling the outline StrokeRec::apply_to_path returns and that outline
is not cap-invariant at that width. Butt is byte-exact, so the other two caps
are silently wrong pixels.

No committed cell combines all three triggers and the tiger never declares a
linecap, which is why five rungs of byte-exact bakes never showed it. The fix is
its own rung — caps are inert per *contour*, so a path mixing open and closed
contours cannot be normalized wholesale — so it is recorded with its
reproduction rather than patched here.
`pnpm fmt:check` (oxfmt) failed on the three crate READMEs this branch
touched. The change is table-column alignment only — `git diff -w` reports
nothing but the separator rows, whose dashes lengthened to match.

Fixes the node workflow's "Check formatting" step.
The additions-only gate compared the two lockfiles as text: any removed line
failed. That encodes the right invariant for a crate cut — a refactor must not
quietly change what the workspace depends on — but it cannot express a decision
whose *subject* is dependency provenance. D-L pins Stylo at a revision; a branch
carrying both a crate cut and that pin can never satisfy the rule as written,
and the only outs were to bypass the gate or switch it off.

Read the lockfiles as package graphs instead. A dropped package now fails
unconditionally — something the textual check could not even name — and a
withdrawn version fails unless docs/wg/consolidation/lock-graph-changes.toml
enumerates that exact from/to move and names the decision behind it. Additions
stay free, which is what the gate is called.

The ledger opens with D-L's eighteen moves. Pinning the seven non-Stylo ones
back was attempted first and refused by cargo: stylo_static_prefs 0.19.0
requires toml ^1.1.2, and stylo 0.19.0 requires indexmap ^2, which requires
hashbrown ^0.16.1. No package is dropped and none renamed.
The gate used to be a line filter; it now decides whether a registered decision
authorises a graph move, and a gate that cannot fail is not a gate. Seven
controls, six of them negative: an unlisted bump fails, a dropped package fails
even when the ledger names it, a missing ledger authorises nothing, and a row
must match the version that landed rather than only the one withdrawn. The two
positive cases pin the shape of a pass — an authorised move, and pure additions
with no ledger at all.

The gate reaches git through one function, so the controls substitute it and
feed the decision logic hand-written lockfiles. No commits are invented and no
Python test framework enters the tree.
D-L decided dependency provenance; this addendum measures what that decision
does to Cargo.lock and why the additions-only gate had to learn about it.

Eighteen versions move, no package is dropped, and none is renamed. Seven
crates are members of the pinned tree, two are its direct registry
requirements, and the rest follow from stylo_static_prefs requiring toml ^1.1.2
and stylo requiring indexmap ^2. Compliance was attempted first: all seven
non-Stylo versions were offered back to cargo with --precise and all seven were
refused, each with its forcing edge named. The bumps are the pin, not drift.
`patrol_stroke_width_units` documented three ingresses and implemented two. It
walks ancestors reading the presentation attribute and the `style` attribute,
so it never saw a `<style>` rule — and a sheet is not attributable to one
element without selector matching, which is why the leg it claimed could never
have lived there. Measured, the same declaration in three places:

  stroke-width="2ex"            refuses by name
  style="stroke-width:2ex"      refuses by name
  <style>rect{stroke-width:2ex} renders, exit 0, at 16 units

That last row is silent wrong pixels, which this slice does not do.

The document-level scan that already walks every `<style>` element's text now
carries a second question alongside "does this declare a property the cascade
drops": "does it declare a stroke-width whose basis this build lacks". Same
unit list, same token test, so one rule covers all three spellings. Strict
refuses the document; best-effort declares once against the sheet and renders —
the same shape the neighbouring sheet finding already had, since neither can
name the elements a rule matches.

The patrol's docstring now describes the two legs it has, and points at the
document-level scan for the third. `DeclarationIgnored` gains "as authored": a
declaration can now depart by being misresolved, not only by being dropped.

Laws: the four spellings sit in one strokes-contract case, a best-effort case
pins what each admission does with each, and a control holds that the sheet leg
refuses the unit and not the word — `stroke-width: 8`, `0.5em` and `2px` still
render from a sheet, and a `1ex` belonging to another property in the same rule
is not this patrol's business. Both fail without the fix.
The cub found this at a second size and the register recorded it unfixed: a
closed contour stroked at about one device pixel diverged from Chromium under a
round or square cap, ~84 of 2304 pixels, while butt was byte-exact. Re-measuring
moved every part of the diagnosis:

- The register blamed filling `StrokeRec::apply_to_path`'s outline. A centred
  SVG stroke does not go that way — it sets `PaintStyle::Stroke` and hands Skia
  the cap, the same stroker Blink drives.
- It is not the fill. A stroke-only closed contour diverges identically.
- It tracks the *device* width, not the authored one: the same document at 2x
  diverges at an authored 0.5 and agrees at an authored 1. The threshold sits
  between 1 and 1.25 device pixels.
- An *open* contour is byte-exact at every width and every cap — so this is not
  thin-stroke rasterisation in general, it is a cap appearing where the contour
  rejoins.

SVG says a cap is inert on a closed contour, and Chromium agrees (its butt and
round captures of one are byte-identical). So n0 normalises the cap to butt when
every contour is closed. That is one draw, one composite pass, and byte-exact at
every width probed.

Except when a contour has no extent. `M44 32 Z` under a square cap is a dot
Chromium paints, and SVG2 §13.2 makes the cap the only thing that renders it —
normalising it away erased the dot, which the corpus caught. The predicate now
declines to normalise anything that closes on the point it opened at, comparing
on-curve endpoints only: a false positive merely keeps the authored cap, while a
false negative would delete a dot.

A path that *mixes* open and closed contours needs both caps at once, and one
paint cannot carry both. Splitting the draw was measured and is worse — it fixes
the thin case and then diverges by 32 to 47 pixels at 1.25 and 2 units, because
the two runs' anti-aliased edges composite twice where they overlap. So websem
refuses that combination by name instead. It over-refuses, since the error only
appears at about a device pixel and the compiler cannot know the device; serving
it properly means stroking each contour to an outline and unioning them, which
is its own rung.

svg-scene-cub is now byte-exact at 48x48 as well as 96x96.
Three cells at the combination no committed cell held: a closed contour, a
non-butt cap, and a one-device-pixel stroke width. The existing cap cells stroke
an open line sixteen units wide, and the closed-contour cell takes the default
butt — between them they cover both halves of the trigger and never its
intersection, which is why five rungs of byte-exact bakes said nothing and a
scene at a second size had to find it.

All three verify byte-identical to Chromium 149, so the stroke family is now 26
of 27 exact with only the declared conic tolerance remaining.
The addendum records the fix and, at equal length, what re-measuring overturned
in the original diagnosis: the wrong consumer was blamed, the fill was
incidental, the threshold is a device width rather than an authored one, and an
open contour was byte-exact all along — so this was never thin-stroke
rasterisation in general.

Also recorded: the zero-length-dot exception the corpus caught within minutes,
and why the mixed-contour case refuses rather than splitting the draw, with the
measurement that ruled splitting out.
…me as CSS does

Two silent-wrong-pixel holes, both in the fixes that claimed to close this class.

**The cap normalisation covered one arm of three.** It landed in
`ItemKind::PathStroke` only, so `<circle>` and `<ellipse>` — closed contours,
cap-bearing, admitted — kept painting a cap Chromium does not. Measured against
Chromium 149 on 48x48, differing pixels of 2304, per geometry and per cap:

  device width | closed path | oval  | rect | line (open)
  0.5 · 1      | 84–95       | 84–95 | 0    | 0
  1.25 and up  | 0           | 0     | 0    | 0

So this is per *arm*, not per closed contour: `draw_rect` never takes the
thin-stroke path and needs nothing, `draw_oval` does. `LineStroke` keeps the
authored cap — a line is open, and Chromium's own captures of one are not
cap-invariant, which the probe confirms. `TextStroke` strokes closed glyph
outlines but no admitted source reaches it, so it is named and left until text
lands and can be measured.

The law is now whole-geometry rather than per-element, because a per-element law
is exactly what missed this: it iterates every closed geometry the slice admits,
at the widths where the divergence lived, and asserts all three caps are pixel-
identical. It fails without the fix.

**A CSS property name is case-insensitive; one patrol leg was not.**
`style="STROKE-WIDTH:1vw"` compiled to a stroke 12.8 units wide — the cascade's
pinned 1280px device — where Chromium paints 0.64, with no refusal and no
declaration. The guard was a case-sensitive `contains`. The sheet leg never had
the bug, since it compares with `eq_ignore_ascii_case`; the docstring asserting
the two legs together cover all three spellings was false until now.
Four cells: a circle and an ellipse, each under a round and a square cap, at a
one-device-pixel width. With the three closed-path cells already committed that
is seven cells over the intersection the corpus never held — a closed contour,
a non-butt cap, and a width near one device pixel.

The gap survived twice because the older cap cells stroke an *open* line sixteen
units wide, and the closed-contour cell takes the default butt: each half of the
trigger was covered and their intersection never was. Adding the path cells
closed it for paths and left the ovals painting the same wrong pixels, which is
what these four gate.

The README's stroke tally and tolerance ceiling are corrected in the same commit,
since both are statements about this manifest: 30 of 31 stroke cells byte-exact,
and the corpus admits at most 6 differing pixels (svg-circle-viewbox-scaled, at
delta 3) or at most delta 8 (svg-ellipse-fill, in 1 pixel) — never both at once,
which the old "worst cell is 6 pixels at delta 3" read as a single bound.
The rewritten lock gate compared name and version. That is blind to
substitution: this workspace tracks Stylo by `git+…?rev=…` with no checksum, so
repointing that URL at a fork, or moving the rev, leaves the version untouched
and passes as "0 dropped" — on precisely the axis the authorising decision is
about, since the charter puts "a floating branch and private source fork"
outside it.

The comparison is now the whole coordinate: version, source and checksum. Three
rules follow. A dropped package still fails unconditionally. A package
re-published under an unchanged version *and* source — same coordinate, new
checksum — now fails unconditionally too, because the bytes behind something
immutable moved and no decision makes that legitimate. And a move that changes
the source fails unless its ledger row spells out `from_source` and `to_source`
verbatim.

Running it against `main` found what the weaker comparison had been passing:
eleven packages moved from crates.io to the pinned revision, and two of them —
`servo_arc 0.4.3` and `to_shmem_derive 0.1.0` — did so at an unchanged version,
so the old gate reported them as untouched. The ledger now records the
registry-to-git move on every one, which is what the decision actually did.

Five more controls, all negative: a fork swap with a silent ledger, with a row
that omits the sources, with a row naming the wrong destination, a swap that
passes only when both sources are declared, and a re-publish that fails even
with a row. Twelve controls in total.
Seven statements this branch left behind its own work, each corrected against a
measurement rather than a guess:

- The SVG compiler's module doc still declared the pre-rung slice — solid-filled
  rect, circle and ellipse — three rungs after containers, paths, lines and
  strokes landed. It now enumerates the admitted surface, and its cascaded-patrol
  list names what `patrol_computed_style` actually reads (`display: contents`,
  `stroke-opacity` and `stroke-dasharray` were missing; shape `stroke` was listed
  and is now consumed).
- The oracle gate's own doc claimed "if any RGBA pixel differs from Chromium"
  while six of the rows are checked against declared tolerances. It now says
  which six, why, and that the bounds are measured values rather than headroom.
- The n0 README described the admitted slice as "rectangles"; it is rectangles,
  ellipses and paths, filled and stroked, with the closed-contour cap normalised.
- The corpus README opened by calling itself a "first architecture prototype"
  and carried a superseded status note about a proving shell. It now says what
  the corpus is, and why the directory is named for a ratified amendment rather
  than a phase — `test-svg/` and `test-html/` hold the legacy renderer's corpora,
  which is the distinction the name carries.
- The pages README claimed the engine "handles solid-fill SVG `<rect>` only".
  Rewritten to point at the shipped slice instead of restating a floor, so it
  cannot go stale again the next time that slice grows.
- The vector-join spike's header said the leaf-vocabulary decision "still blocks"
  it. That decision was taken on 2026-07-23 on this very evidence; the header now
  says what it is — the independent witness behind a join already taken, with the
  production lowering named — and which leaves genuinely remain open.
- The register reported a corpus count from three commits earlier and called the
  tiger "reproducible from that one command". The command lives in an untracked
  file, so the tiger is now stated plainly as a reported observation rather than
  a gate; the committed corpus is what reproduces from a clean checkout.
…ivate ids

The two crates the SVG path rests on shipped no README at all. `websem` decides
what this engine will and will not render, `rframe` is the contract it emits,
and a reader arriving at either found only source. Both now have one, written at
csscascade's scale rather than n0's: what the crate owns, the pipeline position,
a module table, and an explicit **Anti-goals** list — websem is not a layout
engine, not a text shaper, not a painter, not a second cascade, not a coverage
claim; rframe is not an authored source, not a file format, not a renderer, not a
second engine's contract. The admitted slice keeps exactly one home
(`crates/n0_cli/README.md`) and the others point at it rather than copying it.

Three of the five new crates were also missing from the maps a stranger actually
reads. `AGENTS.md` gains rows for websem, rframe and animation-sampling — the
table already described `n0_cli` as running "websem → rframe → n0" while
defining neither — the documented check command gains all three, and the root
README's workspace list gains animation-sampling. CLAUDE.md is a symlink to
AGENTS.md and needs no edit.

`rframe` also stopped exporting every type twice: `pub mod frame|path|stroke`
became private, leaving the flat root every consumer already preferred, and the
four deep imports collapsed with it.

**The private vocabulary is out of the code.** Decision ids — D-C, D-L, D-M, D-N
— are the register's own shorthand and belong in `docs/wg/consolidation/`, where
the glossary defines them. Outside that tree they are tokens a stranger cannot
resolve, and there were eighteen: four in `Cargo.toml` `description` fields,
which is the worst case because those become published crates.io metadata, plus
crate READMEs, rustdoc a reader sees via `cargo doc`, test module docs and
assertion messages. Each now states the substance instead, and the four places
where a link genuinely helps say what they link to rather than naming a ticket.
"proving shell" went with them: it described a thing that no longer exists.

Not done, deliberately: the `patrol_*` functions keep their name. The register
uses "patrol" in exactly the code's sense at a dozen sites, so there is no
collision to fix and renaming 29 identifiers would buy nothing.
… front page

Thirteen of the twenty fixtures in `unsupported/` had no reader. The four that
did were pinned one `include_str!` at a time, so a file could be added — or an
old one could quietly start rendering — with nothing to notice. The corpus
documented the slice's edge to humans and to nothing else.

`unsupported_corpus.rs` reads the directory with `read_dir` and holds it against
a declared table, so an undeclared file fails and a declared-but-missing one
fails too. For each entry it asserts the departure names its construct in *both*
admissions, and the split is itself the law: seven are document-level — the
viewport grammar and root sizing decide the canvas, so best-effort cannot soften
them into a hole without inventing pixels — and thirteen are attributable, so
best-effort renders the rest and declares them by name at a structural path.
There is no third outcome, which is the point: the property under gate is the
programme's invariant stated over a whole directory rather than one construct,
*nothing here renders silently*, and a fixture that started rendering would have
nowhere to land.

The register gains a front page in two places. `index.md` replaces a prose
paragraph with a table of the seven papers, each with its genre, the decision it
carries and whether that decision is settled — including that the conformance
rule is unratified and no score may be computed. The engine-of-record paper
gains a `Current state` section ahead of its eight dated addenda, so what is
true at the tip is readable without reconstructing it from the log: the
admission model, the admitted slice, the corpus counts, and what is not claimed.
`svg.rs` was 2675 lines with nothing in the tree to tell a reader that a full
`preserveAspectRatio` implementation lives inside it.

The obvious remedy — split it into patrol, transform, viewport, paint and shape
siblings — is the wrong one, and the branch's own history says so: the strokes
rung's 339-line diff touched the error type, an attribute table, the computed
patrol, five shape compilers, the fill read and the stroke read, in one commit.
Those are five of the six proposed files. Splitting would convert a one-file
rung diff into a six-file scatter and leave ~40 private helpers needing
`pub(crate)` in both directions — the shape of one algorithm, not six things.

So only the grammar comes out. `svg_transform.rs` is a token list in and one
affine out: no document, no cascade, no error type, no element, and — measured
before moving it — zero outward calls except two shared string helpers. That is
the same seam `svg_path.rs` already sits on.

The rest is answered with a region map in the module header: entries and
session, departures, shapes, paint, viewport, each naming its types, so the
implementation is findable without being scattered. Three pre-existing rustdoc
warnings about private intra-doc links went with it; `cargo doc -p websem` is
now clean.
@softmarshmallow softmarshmallow changed the title feat(consolidation): Web-first prototype — source-native models → one resolved contract feat(consolidation): the n0 path becomes the SVG engine of record Jul 27, 2026
@softmarshmallow
softmarshmallow marked this pull request as ready for review July 27, 2026 05:11
@softmarshmallow
softmarshmallow merged commit ae50a86 into main Jul 27, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant