C1 (egui + lyon) and C2 (vello) land together, from one commit, because
scoring either against an apparatus the other had already moved would not
be a comparison. Both were built against the pin-13 oracle frozen at
694d135 and its three schema amendments, all committed before either
candidate existed.
Both pass: Pass / Pass / NOT RUN / Pass / Pass, 80/80 hit-test probes each,
five live out-of-process AT-SPI readbacks each, no bus-unreachable
evidence claimed. Check 3 is NOT RUN by the 1.2 ruling, so both criterion
cells are NOT RUN and both candidates are eligible -- check 3 is not
disqualifying, and the checks that are (2 and 5) both pass.
Neither candidate re-shapes. Each takes glyph ids and offsets from the
resolved data and draws those glyphs at those positions; outline
extraction and path conversion are candidate-owned, the staff-to-device
transform is shared because re-implementing it would inject a divergence
check 1 is not measuring. D1 is 0 on all ten rasters, worst D2 0.14%
against a 2% tolerance, worst D4 1.22%. No tolerance was touched.
Round 1's binaries are byte-identical. Both candidates added Round 2 entry
points beside them rather than editing frozen evidence.
The cost measurement, which is why the packet exists:
ReportPart C1 C2
TextRendering 651 342
HitTestResolution 228 254
AccessibilityTreeConstruction 64 109
AccessibilityIntegrationWiring 62 199
FixtureAndReportPlumbing 1845 1925
Product-side accessibility -- tree construction plus integration wiring --
is 126 for C1 against 308 for C2: 2.4x, 182 lines. C1 inherits eframe's
AccessKit path and writes a single 62-line file to reach it; C2 inherits
nothing from vello and writes 199 lines of adapter lifecycle, event loop,
and bridge setup. C1 also writes *less* semantic-node code, 64 against 109.
That figure survived four reattributions, and the earlier ones were wrong
in ways worth recording so the next measurement is not made the same way:
- The first reading, 190 vs 30, was backwards. C1's tree row counted a
file that also held cosmetic glyph rendering; C2's excluded wiring that
belonged in it. Both errors pushed the same direction, which is exactly
why the number looked like a clean story.
- The second attribution put verifier subprocess orchestration under
AccessibilityIntegrationWiring, so C1's row grew 692 -> 1011 -> 1066
across two review rounds while measuring nothing but how much review its
harness attracted. That machinery is spike apparatus, and it is
FixtureAndReportPlumbing now.
- The mapping was disjoint before it was exhaustive: a 37-line file sat in
no part at all. Both candidates now assert every source file is claimed
by exactly one part and fail naming any that is not.
- C2 serialized two rows for one part while C1 serialized one. My own
comparison script summed them silently, which is how it survived a
review; it now refuses to aggregate and fails instead.
Three caveats belong with the number rather than under it. By non-comment
code the same comparison reads 55 vs 176, a 3.2x ratio -- same direction,
larger gap, because C1's files are proportionally more documentation; the
committed reports encode whole-file maintenance surface, so that is the
official figure and this is the sensitivity check. The dependency delta
points the other way: C1 carried accesskit at the Round 1 baseline and C2
carried none, yet both pull the same 16-17 AccessKit/AT-SPI/zbus crates
once a live tree exists, so inheritance saved code and not dependencies.
And roughly 1,900 lines per candidate is spike-only plumbing -- verifier
orchestration, evidence handling, report assembly -- that no real editor
would carry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
The shared harness plus the two candidate binaries for Round 1 of the
toolkit spike. C1 tessellates each Bravura outline with lyon and draws it
through egui's own paint pipeline; C2 builds one kurbo BezPath and one
Scene::fill. Both classify their readback against the frozen oracle from
0a35697 and report per-point.
Three things this had to get right, none of which is the rendering:
A candidate must not be able to pass by drawing nothing. C1 initially
failed all fifteen ink points while passing all twelve background points,
which is the signature of a blank target, not of a fill bug: the mesh
named TextureId::default() and egui-wgpu silently skips primitives whose
texture id is unregistered. It now registers a 1x1 opaque-white texture.
The harness must not substitute for a broken readback. A short buffer
used to yield (0,0,0,0), whose luma is 0, so every ink point would pass.
Buffer length, coordinate range, and sample opacity are now hard errors
rather than clamps or defaults.
The oracle must not be editable into agreement. deny_unknown_fields
catches structural drift, but semantic drift is the dangerous kind, and
a validator that checks the oracle against its own other fields accepts
any self-consistent file. OracleFile::validate now checks against
literals restated in the harness: the exact five-glyph roster with its
requirement mapping, subpath counts and point counts, the 27-point
census, 1920x1080, the 8 px clearance floor and every sample's clearance,
and the requirement-specific status flags. Thirteen mutations were run
against it -- deleting a glyph, renaming one, duplicating one, dropping a
point, retargeting every glyph together, lowering the floor, relaxing
spacing -- and all thirteen are rejected.
AA is nominally 8x on both, as pin 4 requires, but the mechanisms differ:
C1 uses a hardware multisample attachment (which is why it must request
TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES, since 8x on Rgba8Unorm is off
the WebGPU baseline) while C2 uses vello's compute AA into a
sample_count:1 storage texture. The run report prints the mechanism beside
the number so no later round reads "8 == 8" as parity. Round 1 is
indifferent to the difference; Round 4's timings will not be.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz