Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions .github/workflows/full-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
# platform-free: packages/layout and packages/dragon (as ci.yml) on ubuntu-latest, except the native files.
# native (ubuntu-24.04-arm, 4 shards): every file that uses a native toolchain, by name or content (test-shards.ts), with
# swift.org Swift 6.4.0 and Kotlin 2.4.20 required, and a run that reports "blocked (owner tooling)" fails.
# chrome (macos-26, 3 shards): every other file; macOS 26 is where the Chrome captures are byte-identical to the Mac's
# chrome (macos-26, 4 shards): every other file; macOS 26 is where the Chrome captures are byte-identical to the Mac's
# (capture-proof.yml), with kotlinc 2.4.20 pinned and checked.
# Caches are only restored here, never saved: a dispatched run tests an unreviewed tree at master's cache scope.
# regen: `pnpm regen --check` split as regen-on-ci.yml splits it (the Chrome steps on macos-26, lanes-host on xcode-27); on a
# tree at its fixed point both halves change nothing.
# tree at its fixed point both halves change nothing. They run beside the shards, and the landing driver skips them
# (input regen=false): its build has already regenerated the tree to its fixed point.
# The summary job merges the shards' JSON reports into full-test-results (every test's state) and fails on any failure.
# Trigger: workflow_dispatch with a commit sha (the landing driver), or the `fulltest` label on a pull request.
name: full-test
Expand All @@ -22,6 +23,11 @@ on:
description: Commit to test (reachable from a branch of this repository)
required: true
type: string
regen:
description: Run the regen fixed-point check (false only for the landing driver, whose build already ran pnpm regen to its fixed point on the tree)
required: false
default: true
type: boolean
pull_request:
types: [labeled]
permissions:
Expand Down Expand Up @@ -152,7 +158,7 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
shard: [1, 2, 3, 4]
runs-on: macos-26
timeout-minutes: 120
steps:
Expand Down Expand Up @@ -191,11 +197,11 @@ jobs:
echo "$RUNNER_TEMP/kotlinc/bin" >> "$GITHUB_PATH"
"$RUNNER_TEMP/kotlinc/bin/kotlinc" -version 2>&1 | grep -q 'kotlinc-jvm 2.4.20' || { echo "::error::not kotlinc 2.4.20"; exit 1; }
echo "ANDROID_HOME=${ANDROID_HOME:-unset}"; find "${ANDROID_HOME:-/nonexistent}/platforms" -maxdepth 1 2>&1 | tail -3
- name: vitest run (every other file, shard ${{ matrix.shard }}/3)
- name: vitest run (every other file, shard ${{ matrix.shard }}/4)
run: |
set -o pipefail
mkdir -p "$RUNNER_TEMP/report"
list=$(node scripts/test-shards.ts plan chrome ${{ matrix.shard }} 3) || { echo "::error::the shard planner failed"; exit 1; }
list=$(node scripts/test-shards.ts plan chrome ${{ matrix.shard }} 4) || { echo "::error::the shard planner failed"; exit 1; }
files=(); while IFS= read -r f; do files+=("$f"); done <<< "$list"
npx vitest run "${files[@]}" --reporter=default --reporter=json --outputFile="$RUNNER_TEMP/report/chrome-${{ matrix.shard }}.json" 2>&1 | tee "$RUNNER_TEMP/shard.log"
- name: No native run was blocked (owner tooling)
Expand Down Expand Up @@ -227,7 +233,7 @@ jobs:
merge-multiple: true
- name: Every test's state, and the failures
env:
EXPECT: platform-free.json native-1.json native-2.json native-3.json native-4.json chrome-1.json chrome-2.json chrome-3.json
EXPECT: platform-free.json native-1.json native-2.json native-3.json native-4.json chrome-1.json chrome-2.json chrome-3.json chrome-4.json
run: |
cat > "$RUNNER_TEMP/summary.mjs" <<'EOF'
// Merges the shards' vitest JSON reports. A missing report is a failure: the run proves nothing about its files.
Expand Down Expand Up @@ -268,10 +274,12 @@ jobs:
name: full-test-results
path: ${{ runner.temp }}/full-test-results.json

# Both regen halves start after the Chrome shards, so a full test holds at most 3 macOS jobs at once (the free plan runs 5).
# Both regen halves run beside the test shards (with the 4 Chrome shards, one of the 6 macOS jobs waits for the free plan's 5). The landing driver's
# dispatch skips them (regen=false): its build already ran pnpm regen on the tree, which exits 0 only at a fixed point, and
# committed the outcome, so they would re-check a proven fixed point.
regen-chrome:
needs: [resolve, chrome]
if: always() && needs.resolve.result == 'success'
needs: resolve
if: github.event_name != 'workflow_dispatch' || inputs.regen
runs-on: macos-26
timeout-minutes: 240
steps:
Expand Down Expand Up @@ -316,8 +324,8 @@ jobs:
run: pnpm regen --check --skip lanes-host

regen-host:
needs: [resolve, chrome]
if: always() && needs.resolve.result == 'success'
needs: resolve
if: github.event_name != 'workflow_dispatch' || inputs.regen
runs-on: xcode-27
timeout-minutes: 60
steps:
Expand Down
11 changes: 11 additions & 0 deletions docs/goals/milestone-2-proof/notes/PM-2026-10-04.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,14 @@
**Landing order:** pipeline PRs first, then features. No new feature lanes start until the targets are met. In-flight feature work finishes.

**Pending owner decision:** comparing NaN as NaN in the android units-m2 suite. That would remove the Mac's last device lane.

## 2026-10-05: @supports — #200 lands as a conservative subset of T067 R11 (MQ-S follows)

#200 (CASC slice 1) decides @supports at build time with one answer for every target. It decides "true" only when Chrome keeps
the declaration and a web support-profile row proves every longhand it sets; it decides "false" only on a grammar violation whose
keywords the grammar lists; anything else (unshipped grammar values, vendor or legacy values, <general-enclosed>, selector(), an
undecidable operand) is refused. Every decided condition is checked against CSS.supports in the pinned Chrome
(supports-parse.test.ts). This never contradicts R11's web answer, but it is narrower: R11's per-target native answer (web answer
AND that target's profile row, so `@supports not (display: grid)` takes its fallback natively), the committed CSS.supports corpus
with --check, and css/supports.ts remain MQ-S's job. Ruling: land #200; MQ-S is the follow-up, additive. MQ-PREF is MQ-R2 and waits
for #140, the MQ-R1 runtime PR and PNT1 (T067 stop condition).
112 changes: 112 additions & 0 deletions docs/goals/milestone-2-proof/notes/T-svg-a-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# SVG-a: inline SVG drawn natively (coverage-rank lane 16, reach 395). Binding worker spec (PM ruling 2026-10-05)

Read AGENTS.md, docs/goals/milestone-2-proof/lane-contract.md, and the REPL-a notes (T051-repl-a.md, T048-repl0.md). Base: origin/master.
PM ruling: interior-sample pixel proof plus Chrome-proven outline geometry (the PNT2 transform precedent), and two sequential,
unstacked PRs, SVG-a1 then SVG-a2.

## Scope

**Supported, end of SVG-a2:**
- An `<svg>` element in an HTML tree, laid out as a block-level replaced box on REPL-a's replaced-box model:
- `display: block`, or blockified as a flex item;
- sized by the `width` and `height` attributes (presentational hints with author CSS winning, as for img) or by CSS;
- with no size, the default object size 300x150.
- `viewBox` (four numbers, positive width and height) with the initial `preserveAspectRatio` (xMidYMid meet).
- Children `<path d>`, `<rect x y width height>` and `<circle cx cy r>`, as direct children of the `<svg>`.
- `fill`, `stroke` and `stroke-width` as CSS properties: a new `svg` property family, all inherited, with Chrome's initial
values black, none and 1px. They are also presentation attributes on svg, path, rect and circle, which author rules beat.
- fill and stroke take `<color>` (currentcolor included) or `none`.
- stroke-width takes a non-negative px length, or a unitless number (CSS allows the SVG quirk), computed to px.
- Path data: the full SVG 1.1 / SVG 2 path grammar (M, L, H, V, C, S, Q, T, A, Z, absolute and relative, implicit
repetition). Arcs are converted to cubics per SVG 2 Appendix B.2.4, as Blink does.
- Painting: fill, then stroke (the initial `paint-order`), non-zero fill rule, the initial butt cap, miter join and miter
limit 4.
- iOS: a CAShapeLayer per shape (fill layer, then stroke layer).
- Android: a Dragon view drawing an android.graphics.Path with Paint FILL then STROKE.

**Measured facts** (Chrome 145, probe in /tmp/dragon-svg-a1/packages/parity/zz-probe.mts):
- `<svg>` with no size: 300x150.
- With only a viewBox: it fills the container width, with height from the ratio (refused in a1, below).
- `width="24" height="12" viewBox="0 0 24 24"`: getScreenCTM is [0.5 0 0 0.5 6 y] (meet, centred).
- getBBox is the tight bounds of the fill geometry (cubic and arc extrema, not control points).
- A shape's getBoundingClientRect is that bbox mapped through the CTM.
- `stroke-width` computes to "2px" from the attribute "2"; initial fill rgb(0, 0, 0), stroke none, stroke-width 1px.
- Inherited fill reaches the shapes from any ancestor.

## Refused (each has a reject fixture and a named package)

| Case | Code | Package |
|---|---|---|
| An inline-level `<svg>` (display inline, the default) | DRAGON_UNSUPPORTED_VALUE (REPL-a's inline-replaced message) | INL2 (inline replaced) |
| `<svg>` with a viewBox but without both width and height | DRAGON_UNSUPPORTED_VALUE | SVG-ratio |
| width or height attribute other than a number or px | DRAGON_UNSUPPORTED_VALUE | SVG-units |
| `stroke-dasharray`, `stroke-dashoffset` | DRAGON_UNSUPPORTED_PROPERTY (they are unknown properties today; the message names the package) | SVG-dash |
| `fill-opacity`, `stroke-opacity`, `opacity` on a shape | DRAGON_UNSUPPORTED_PROPERTY or VALUE | SVG-opacity |
| `<g>`, `<use>`, `<defs>`, `<symbol>`, `<text>`, `<line>`, `<polyline>`, `<polygon>`, `<ellipse>`, `<image>`, gradients, patterns, `<clipPath>`, `<mask>`, `<style>` | DRAGON_UNSUPPORTED_ELEMENT | SVG-b (structure), SVG-paint (servers) |
| `fill`/`stroke` `url()` and the context-* keywords | DRAGON_UNSUPPORTED_VALUE | SVG-paint |
| A `transform` attribute or CSS transform on an svg child, and `preserveAspectRatio` | DRAGON_UNSUPPORTED_ATTRIBUTE | SVG-transform |
| `stroke-linejoin`, `stroke-linecap`, `stroke-miterlimit` other than initial, and `fill-rule: evenodd` | DRAGON_UNSUPPORTED_PROPERTY | SVG-stroke |
| `rx`/`ry` on rect | DRAGON_UNSUPPORTED_ATTRIBUTE | SVG-b |
| A percentage stroke-width (relative to the normalized diagonal) | DRAGON_UNSUPPORTED_VALUE | SVG-units |
| Text or non-shape elements inside `<svg>`, and an `<svg>` nested in an `<svg>` | DRAGON_UNSUPPORTED_ELEMENT | SVG-b |
| SVG on native in SVG-a1 (until SVG-a2 lands) | DRAGON_UNSUPPORTED_ELEMENT [ios, android] | SVG-a2 |

## Proof

1. **Layout** (existing lanes): the `<svg>` box is compared with Chrome as every replaced box is: the host engine lane, and
chrome-dual exact.
2. **Computed values** (chrome-dual): fill, stroke and stroke-width on every element, authored against compiled, exact.
3. **Outline geometry, strict.**
- Dragon's SVG geometry module (TypeScript, translated with the engine in SVG-a2) computes, for every shape:
- the user-space path, as Blink's Path holds it (arcs as cubics);
- its tight bounds, as Skia's SkPath::computeTightBounds computes them in float32;
- the CTM (the svg content box's position and the viewBox transform);
- the shape's client rect.
- The capture records getBBox, getScreenCTM and getBoundingClientRect of every shape.
- They must be equal as doubles, with no allowance.
- A planted fault for each of these must fail its fixtures: arcs as lines, control-point bounds, viewBox ignored, and
meet as slice.
4. **Pixels (SVG-a2, device):** a paint-samples module `svg.ts`. It adds sample points at least 2 device px from every
shape edge and every stroke edge (computed from the proven outline), plus points outside every shape inside the svg box.
Each point's expected colour is the Chrome screenshot pixel. The unchanged pixel gate applies.
- Planted faults: stroke under fill, fill rule even-odd, stroke width not scaled by the viewBox, and fill and stroke
colours swapped. Each must fail device-pixels.
5. All fixtures run ltr and rtl, at DPR 1 (host), 2, 2.625 and 3.

**Support rows:**
- `element:svg`, `element:path`, `element:rect`, `element:circle`;
- `fill:<color>|none`, `stroke:<color>|none`, `stroke-width:<length>`;
- `attribute:viewBox`.
Each row names its passing fixtures. Web rows come with SVG-a1; ios and android rows come with SVG-a2's device run.

## Split

- **SVG-a1 (compiler and web), off master:**
- the fixture reader's svg subtree (foreign elements, self-closing tags, case-kept attribute names);
- the tags, attributes and refusals;
- the svg property family;
- svg as a replaced box (its children are fallback content to layout);
- presentation attributes as hints;
- the geometry module with its strict differential;
- web CSS output;
- fixtures `svg-basic` (path, rect and circle), `svg-viewbox` (meet offsets, in px and unitless), `svg-paint` (inheritance,
currentcolor, attribute against CSS precedence, none) and `svg-flex` (svg as a flex item), each ltr and rtl;
- the reject fixtures and the web rows.
- Native targets refuse svg as SVG-a2.
- **SVG-a2 (native), off master after a1 merges:**
- the geometry module as an engine root (translated, TS = Swift = Kotlin vectors);
- the shape lowering and emitters for UIKit and Android;
- paint-samples `svg.ts` and its plants;
- the native rows after the driver's device run.

Each PR stays at or under about 150 KB reviewed.

## Ports and licences

Blink's svg/ and layout/svg/ files are LGPL (KDE): class A references, implemented from SVG 2 and pinned by the Chrome
differential. Skia's SkPath bounds and SkGeometry extrema code is BSD and may be ported. Every cited file is registered in docs/ports.json.

## Stop conditions

- The strict differential disagrees with Chrome and the cause can't be shown with evidence.
- getBoundingClientRect at a fractional DPR needs Chrome's zoom path, and that path can't be reproduced exactly.
3 changes: 2 additions & 1 deletion packages/parity/test/chrome-ports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ const DATA = /(\.(json|jsonl|png|jpg|jpeg|gif|bmp|webp|ico|svg|wasm|css|html|xht
const isCode = (f: string) => SCRIPT.test(f) || C_STYLE.test(f) || SHELL.test(f);

function allTrackedFiles(): string[] {
const git = (args: string[]) => execFileSync('git', ['-C', ROOT, 'ls-files', '-z', ...args, '--', ...SCAN_TOPS], { encoding: 'utf8' }).split('\0');
// The tracked file list is past execFileSync's default 1 MiB output buffer (ENOBUFS), so the buffer is raised.
const git = (args: string[]) => execFileSync('git', ['-C', ROOT, 'ls-files', '-z', ...args, '--', ...SCAN_TOPS], { encoding: 'utf8', maxBuffer: 1 << 28 }).split('\0');
const files = new Set([...git(['--cached']), ...git(['--others', '--exclude-standard'])]);
return [...files].filter((f) => f !== '' && existsSync(join(ROOT, f))).sort(); // existsSync: a tracked file deleted in the working tree
}
Expand Down
13 changes: 13 additions & 0 deletions packages/parity/test/land-devices-ci.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,19 @@ describe('only failed jobs are a verdict (#193 review)', () => {
});
});

describe('the driver\'s full test skips the regen check its build already made', () => {
it('dispatches full-test.yml with regen=false, an input whose regen jobs run only when it is true', () => {
expect(fullTestWorkflow(() => null).inputs).toEqual(['regen=false']);
const yml = readFileSync(repoPath('.github/workflows/full-test.yml'), 'utf8');
expect(yml).toMatch(/\n {6}regen:\n {8}description: [^\n]+\n {8}required: false\n {8}default: true\n {8}type: boolean\n/);
for (const job of ['regen-chrome', 'regen-host']) {
const block = yml.slice(yml.indexOf(`\n ${job}:\n`), yml.indexOf(`\n ${job}:\n`) + 300);
// Beside the shards (needs only resolve), and skipped for a dispatch with regen=false.
expect(block, job).toContain(` ${job}:\n needs: resolve\n if: github.event_name != 'workflow_dispatch' || inputs.regen\n`);
}
});
});

describe('LAND_TEST=ci (the full test on CI)', () => {
it('proves a commit on its own scratch branch, and a failed run names its failing tests', () => {
expect(scratchRef(testBranch(SHA))).toBe(`refs/heads/land-test/c-${SHA.slice(0, 12)}`);
Expand Down
Loading
Loading