Commit Graph

13 Commits

Author SHA1 Message Date
Levi Neuwirth e2979dfccd Packet 2B: both candidates consume the frozen text apparatus
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
2026-07-30 21:42:05 -04:00
Levi Neuwirth ad4f6ed4f3 Pin-13 schema amendment 3: the provider invariant moves into the type
Appends to 775d7ba, which is left untouched.

That amendment put the non-empty check in a constructor and in Deserialize,
and left `Inherited { provider: String }` public. So the struct-literal path
bypassed both -- and the very first caller took it: C1's adapter rows were
written as `IntegrationOwnership::Inherited { provider: "...".to_string() }`,
never touching the constructor that was supposed to be guarding them. A
checked constructor sitting beside a public field is a suggestion. The field
is the API.

Provider is now a newtype whose inner String is private, so every route in
-- Provider::new, Deserialize -- runs the same check, and an empty provider
cannot be constructed on any path outside this module. The compiler proved
it during the change: converting the field turned my own struct-literal
test sites into type errors, which is exactly the class of site that had
been slipping through. I also compiled a deliberate bypass from outside the
module to confirm it is rejected rather than assume it:

    error[E0308]: mismatched types
      provider: String::new(),
                ^^^^^^^^^^^^^ expected `Provider`, found `String`

and reverted that probe byte-identically.

Two new tests beyond the constructor and wire cases already present. One
pins the serialization consequence a type error cannot express: every
Provider that exists has been checked, so no serialized row can carry an
empty one. The other round-trips a whole adapter row and asserts the
provider survives intact, since the shape a real report carries this in is
the row, not the enum alone.

C1's two construction sites are converted to the checked constructor. No
report values change -- both candidates already named real providers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
2026-07-30 20:39:32 -04:00
Levi Neuwirth 775d7baced Pin-13 schema amendment 2: an inherited provider must actually name something
Appends to d76287c, which is left untouched.

That amendment justified IntegrationOwnership::Inherited's provider field
as a claim "a reader can check against the dependency graph rather than
take on trust" -- and then accepted "" and "   " for it, in Rust and
through serde alike. An unnamed provider is not a weaker claim of
inheritance. It is the same claim with its evidence removed, and it renders
as Inherited in every table it reaches.

Fixed the way this crate already fixed the identical hole in CheckOutcome's
reason strings: a checked constructor, plus a hand-written Deserialize over
a private wire shadow so the JSON path runs the same check. A hand-edited
report is exactly where an unnamed provider would arrive from, so guarding
only the Rust constructor would have guarded the path nobody takes.

Seven tests. Six fail when the check is reverted to `if false`, verified by
doing it and restoring: the constructor on empty and on whitespace-only,
the deserialize path on both, the same empty provider nested inside a whole
adapter row (the shape a real report carries it in, not just the enum
alone), and the accepting case for a real provider. CandidateOwned keeps
deserializing unchanged.

No candidate report changes: both candidates already name real providers,
so this closes a hole rather than correcting a value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
2026-07-30 19:02:46 -04:00
Levi Neuwirth d76287cd68 Pin-13 schema amendment: adapter rows carry typed integration ownership
Append-only amendment to 694d135. The oracle, the verifier, the scoring
rule, the PASS results and eligibility are all untouched; this changes only
what a cost row is able to say.

The first pair of real candidate reports proved the schema could not carry
what Packet 2B was chartered to record. Both candidates reached AT-SPI
through AccessKit -- C1 via the integration bundled with eframe, C2 via
wiring it wrote by hand -- and they reported that as opposite rows:

  C1:  NotBuilt     accesskit-0.24   "no separate AccessKit-native readback was built"
  C2:  Implemented  accesskit-0.24   "the in-process tree this binary constructs by hand"

C1's row was false as written. The AccessKit path was present and
exercised; every one of its five check-5 readbacks travelled it. NotBuilt
means uncovered scope, and that scope was covered.

Relabelling it Implemented would have fixed the falsehood and still lost
the measurement, because "inherited or candidate-owned?" would have
survived only as prose in notes -- which is precisely how the two
candidates diverged in the first place. So AdapterStatus::Implemented now
requires a typed integration_ownership: Inherited { provider } or
CandidateOwned. Inherited names its provider so the claim can be checked
against the dependency graph instead of taken on trust. NotBuilt keeps no
ownership field at all: a platform reached by any route is Implemented.

Inheriting an integration is not inheriting the semantics drawn on top of
it. A candidate that inherits a bridge still writes the accessible nodes
for whatever it painted itself, and that work is counted under
ReportPart::AccessibilityTreeConstruction, not here -- which is what makes
the two rows comparable rather than a single "has accessibility" flag.

Five tests, each of which fails if its guard is removed: the two-candidate
comparison the old schema could not express, Inherited carrying a
checkable provider, an Implemented row missing integration_ownership
refused at deserialize, NotBuilt refusing an ownership field, and a
round trip that must not alter a row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
2026-07-30 18:46:52 -04:00
Levi Neuwirth 694d135bfc Packet 2B apparatus: the neutral candidatekit and the check-5 oracle
Pin 13 requires an oracle committed and reviewed before any candidate is
scored against it. Both halves land here, before C1 or C2 exists, so
neither candidate can shape what it will be measured by.

round2-candidatekit is the only code the two candidates share. It loads
and validates Packet 2A's fixtures, probe table, reference rasters and
regions; it renders nothing, resolves no hit test, and builds no
accessibility node -- a dependency deny-list test reads its own manifest
at test time to keep it that way, including the dotted-subtable and
target-cfg forms a line scanner would otherwise miss. Check 3 is pinned
NOT RUN by the recipe's 1.2 ruling: a report claiming Pass or Fail for it
is refused, and the supplementary F-D field is one the scoring function
never reads, so it cannot reach the criterion cell by construction rather
than by convention. A check-5 NOT RUN now requires typed evidence that the
platform bus was unreachable, because Round 0 already proved it reachable
for both candidates -- "we did not build it" is scope, and scope is not
environmental.

round2-a11y-oracle derives every byte string a live AT-SPI readback must
compare against, from the digest-validated fixtures alone: the expected
name three ways, the at-spi2 role sets, a precommitted alternative form
per prohibited outcome, the visual-order form, and the per-segment source
atoms. The fixtures digest is unchanged at acc13c0d, so Packet 2A stays
frozen.

verify.py gains a check-5 mode whose classifier is a pure function, unit
tested without a bus, and which fails closed on platform, digest, fixture
completeness, the name/hex/length triple, atom concatenation, and any
string that would classify as two different outcomes.

The scoring policy, settled over eight review rounds and worth stating
because most of it was won by finding the opposite in the code first:

  - An accepted-role exact match wins from anywhere in the forest. It used
    to be first-match, so tree order decided a disqualifying check.
  - Composition is scoped to one run subtree and outranks any per-node
    diagnosis. Application-wide concatenation both passed a run half-drawn
    as a canvas and failed a correct split run because the window carried
    a label.
  - Contributor order stays significant, since 8.1 requires logical order;
    only non-contributor order is required not to matter.
  - name-empty means an attempted static-text exposure without a name.
    absent-from-tree means a drawing-only or unrelated tree, and nothing
    else -- it is the outcome 8.3 says this check will most likely catch,
    and it was nearly unreachable while gated on role presence.
  - A byte-exact PASS outranks an alternative-form match. F-C's
    "drops unresolved codepoints" form is its own first segment, so a
    legitimate per-run split was being failed on a check that eliminates.
    That collision is structural for any two-segment fixture with one
    unresolved segment, not an F-C accident.

Recorded as a finding, not fixed: the recipe's claim that visual-order
assembly diverges for F-D "and only there" is false -- F-B diverges too,
and any non-palindromic RTL run of two or more graphemes will. F-D's real
distinguishing property is an interior RTL run. The same stale wording is
compiled into fixtures.json and covered by its frozen digest, so
correcting it would move acc13c0d out from under Packet 2A.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
2026-07-30 15:49:07 -04:00
Levi Neuwirth 93deb18ed6 Record the check-5 asymmetry, and refuse to guess its size
Packet 2A closed with an open question its criterion cannot answer. Check 5
(accessibility) is disqualifying and scored PASS/FAIL, and the recipe pins it
per platform so it does not encode one candidate's stack — but a neutral
criterion does not make the cost neutral. Two candidates can both read PASS
having paid different prices, and the cell has nowhere to say so.

Ruled 2026-07-29: check 5 stays candidate-neutral PASS/FAIL and is not
adjusted for who had further to walk; Packet 2B measures the cost on a
separate row; the final ruling weighs that cost without retroactively
relabelling a PASS.

My first draft of this entry stated the asymmetry as "C1 inherits an
accessibility tree, C2 must build one", and that overstates both sides. C1
inherits AccessKit and its platform bridge, but NOT correct semantic nodes for
custom-rendered text — a score canvas draws glyphs egui knows nothing about,
so the nodes carrying the source string, role and bounds are C1's to create
either way. Inheriting the bridge is not inheriting the semantics. C2 inherits
no accessibility integration from vello, but is free to integrate AccessKit or
another bridge, so it does not face a from-scratch stack; what it owns is the
additional integration and wiring. The delta is real, narrower than
"has it / doesn't have it", and concentrated in wiring rather than in the
semantic node construction both candidates must do regardless.

So 2B reports observed facts at that granularity — inherited vs candidate-owned
per part of the path, dependencies added over the Round 1 baseline, which
platform adapters are actually implemented (an adapter not built is scope not
covered, and is reported as such rather than absorbed into a PASS), the
integration each candidate writes itself, and the resulting maintenance
surface. It does not presume the magnitude: writing down a guess at the gap
and then confirming it is the same failure as choosing a tolerance after
seeing a candidate's output.

The general rule, for later rungs: an eligibility gate answers "may this
candidate proceed", not "what will this candidate cost". Merging the two into
one cell loses the second, and the second is the one the project lives with
after the ruling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
2026-07-29 19:18:52 -04:00
Levi Neuwirth ffe313cd73 Packet 2A: the candidate-neutral text fixtures, emitter, and differential
Four crates, none of which knows anything about a candidate. This is the
apparatus Round 2 will judge with, built and mutation-tested before either
candidate exists — the role Round 1's oracle tranche played.

round2-textkit — the W3 3E stand-in. Resolves two declared faces from an
explicit path list (pin 9), shapes and itemizes five fixtures with
rustybuzz/unicode-bidi/unicode-segmentation, asserts all five W3 section 5
invariants, and emits fixtures.json + an 80-probe hit-test table (none
dropped).

round2-svgref — pin 10's explicit-glyph SVG emitter. Never emits <text>, and
asserts it: a <text> element would let the viewer's shaper pick glyphs and
reintroduce exactly the re-shaping this round exists to forbid.

round2-diff — the bounded visual differential, D1-D4, zero dependencies.

round2-reference — composes the three into reference rasters and D4 regions,
and executes the text mutations M4/M5/M6 against the real frozen fixtures.

Defects found by testing rather than by reading, and what each cost:

* The stand-in had its own quantizer. `(v*1024).round()/1024` is
  ties-away-from-zero; QuantizedCoord is ties-to-even (Appendix D). W3
  section 5 says text quantization "is not a second convention", and naming
  the divergence in a doc comment is not the same as being allowed to take
  it. Now routed through QuantizedCoord, with four half-grid tests at +/-0.5
  and +/-2.5 units chosen because the conventions DISAGREE there, plus one
  recording a tie where they agree. is_on_grid claimed exactness while
  accepting anything within 1e-6; it is now an exact round-trip.
* The validator accepted any self-consistent file. Named checks say nothing
  about individual glyph ids or quantized offsets — tens of thousands of
  numbers a candidate will be scored against. A whole-artifact SHA-256
  closes it; four tests fail without it.
* stable_id: u128 did not survive JSON. Measured,
  82875741697311382809239399464544864365 came back as 8.287574169731139e+37.
  A provenance id that changes when a tool reads and rewrites a file is not
  an identity. Now a decimal string (W3-F5).
* SpikeProvenance was lossy while claiming to mirror every 3E field: a Debug
  rendering for `source`, a length for `dependencies`. It lost nothing
  measurable only because these fixtures' dependency lists are empty, which
  is an accident of the fixtures. Now canonical_bytes under W3's own field
  names (W3-F6).
* A cross-crate string-slice: the reference composer extracted <path>
  fragments by searching for another crate's markers. It worked, and a
  formatting change there would have broken it silently. Now an API call, so
  a rename is a build failure. Verified byte-identical across the refactor.
* Two harnesses printed their own bad news and exited 0.

M4/M5/M6 measured: M4 kills D1 at 221 px (D4 blind at 1.20%); M5 kills D4 at
13.28%/13.19% on the two composed-acute regions; M6's emitter refusal fires
on U+05D0 before any raster, and forced past it D4 reads 95.37/71.26/34.69%.

F-D is labelled SUPPLEMENTARY in code, in fixtures.json, in the summary, and
in every generator's output, with check 3 NOT RUN — enforced by the
validator, because a ruling recorded only in prose loses to whichever record
the reader opens.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
2026-07-29 18:40:16 -04:00
Levi Neuwirth 565b0f8f79 Round 2 recipe: the precommitted stand-in, oracles, and differential
Packet 2A's governing artifact, under CONTRACT_EDITOR_T4_SPIKE pin 13 —
written and reviewed before any candidate can consume it, because a
tolerance chosen after seeing a candidate's output is not a tolerance.

Ruling A permits "geometry/scene equivalence plus a bounded visual
differential under a controlled backend, NOT pixel equality". That phrase
had never been given a number. Section 10 gives it four rules; section 11
lists the mutations that must kill, and every one is now executed by a
binary that exits non-zero when a required kill does not happen.

Three things here were corrected by measurement, not by review:

* The em size. Revision 1 pinned 64 px, which puts Pagella's stems at
  5.4 device px — inside D1's structural blind spot of 2*EDGE_BAND_PX+1 = 5
  px. D1 would have reported `pass` on every stem in the fixture set. The em
  size is now derived FROM the blind spot: 128 px, stems 10.8 px.
* M4. Assigned to D4 by analogy with a dropped glyph; executing it showed
  the analogy is false. An `ff` ligature and two `f` glyphs carry nearly the
  same ink (0.07% of image mass, 1.20% in-region against a 2% tolerance), so
  a mass rule is the wrong instrument for a shape substitution. D1 sees it
  at 221 px outside the band. D4 keeps its threshold: tightening it to 1.20%
  would leave 1.3x over M10's measured 0.785%, and a threshold that close to
  a known-good AA variant is not a tolerance. D1 and D4 are complementary
  and neither is sufficient.
* The origin. The 1/1024 invariant caught this recipe's own stated constant:
  1.6 * 1024 = 1638.4 is not representable. Now 1638/1024.

Section 8 pins the check-5 accessibility oracle per platform rather than in
one toolkit's vocabulary — naming only AccessKit would have favoured C1, and
a criterion that encodes one candidate's stack is not a criterion.

Section 1.2 records the 2026-07-29 ruling: check 3 is NOT RUN (no
Arabic-capable face; pin 9), F-D is a separate Supplementary row that must
not upgrade it, and eligibility is unaffected because check 3 is not
disqualifying.

Section 12 routes six findings back to the W3 amendment, three of them found
by building against 3E rather than by reading it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
2026-07-29 18:39:52 -04:00
Levi Neuwirth c10449a964 T4 round 1 record: both candidates PASS, and the two carry-forwards that outlive the verdict
The Round 1 result and the evidence it rests on: C1 and C2 each reproduce
all 27 precommitted sample points on both required adapters, 108 samples
in total, every one exactly opaque black or opaque white. Neither
candidate is eliminated at this rung.

Anchored to three SHAs, per pin 12 -- root baseline 0a35697, oracle
0a35697, candidate harness c20bc93. The authoritative run was made from a
detached worktree at the baseline with the harness extracted into it, so
the glyph outlines came from frozen epiphany-glyphs and not from a working
tree the parallel genesis-ops track has been editing throughout.

Two findings are recorded because they matter after this round is over.

A blank target is a silent pass shape and it actually happened here: C1
failed all 15 ink points while passing all 12 background points, which is
the signature of nothing being drawn rather than of a fill bug. Had this
round tested only background points, or only "renders without erroring",
a blank target would have passed it. Every later round needs at least one
assertion that can only succeed if ink was deposited.

And nominal 8x AA is not the same mechanism on both sides. Pin 4 asks for
an identical sample count and gets one, but C1's 8x is a hardware
multisample attachment while C2's is vello's compute AA into a
single-sample texture. Round 1 is indifferent; Round 4 is not, since AA is
in the deciding latency numbers, so the report prints the mechanism beside
the number rather than leaving "8 == 8" to imply parity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
2026-07-29 11:10:05 -04:00
Levi Neuwirth c20bc93247 T4 round 1: both candidates render the compound paths, and the oracle now refuses to be edited into agreement
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
2026-07-29 10:59:49 -04:00
Levi Neuwirth 0a35697d8e T4 round 1: the precommitted fill oracle, and nothing has rendered against it
Pin 13 makes this sequencing a hard rule -- "points that must be ink" chosen
after seeing candidate output is not a test -- so this crate draws nothing and
depends on no rendering, tessellation or windowing crate. It derives the oracle
from the typed glyph outlines and writes it out as data. Candidates render
against it in a later packet.

Twenty-seven sample points across five glyphs, all derived programmatically by
even-odd point-in-path over the flattened PathCommand outline, never chosen by
eye, each at least eight device pixels clear of any outline edge so
antialiasing cannot explain a result. Every background point is proven to lie
inside a BOUNDED HOLE -- enclosed by the outer silhouette yet unfilled -- since
a point merely outside the glyph is passed trivially by a renderer that fills
holes solid. fClef instead carries one ink point inside each of its three
filled subpaths, tagged by subpath index so the oracle proves every component
is covered rather than three points that could all land in the bowl.

The status model distinguishes a satisfied disjoint-component result from a
failed hole result: a requirement enum, per-requirement required/satisfied
pairs, and one authoritative satisfied flag.

Mutations, each with kill evidence in the test module. A perturbed ink point is
rejected outside the glyph. Shrinking the clearance floor admits strictly more
points, so the filter is not a no-op. Every hole point sits inside a naive
outer-contour-only fill but outside the real whole-outline fill -- the positive
proof that inner-subpath handling is load-bearing, which stands in for a
fill-rule flip that CANNOT reclassify anything on correctly wound contours.
And filling only fClef's largest subpath misses both dot points, which is the
disjoint-component analogue and the reason that class exists.

Two proofs about fClef's topology, because one was not enough. Vertex-level
non-nesting cannot see two subpaths whose edges cross with no vertex of either
inside the other, so it is paired with pairwise bounding-box disjointness,
which rules out crossing, touching and containment together. The grid-based
corroboration is named for what it actually checks -- one finer 0.005 grid --
rather than claiming any resolution.

The oracle's adaptive flattening is authoritative for the recorded signed
areas; an earlier coarser fixed-step measurement is marked superseded where it
appears. The first four-glyph pass is kept in DECISIONS.md under an explicit
SUPERSEDED -- NEVER COMMITTED banner, because its two findings are what caused
the contract amendment, and deleting the discovery would erase the reason.

Verified: 12/12 tests; fmt clean; cargo doc clean; regeneration byte-identical
by SHA-256 from the crate directory. All 27 coordinates were additionally
cross-validated by an independent point-in-path implementation with zero
mismatches, and the new bounding-box assertion was mutation-killed against a
nested glyph to prove it is not vacuous.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
2026-07-28 21:25:07 -04:00
Levi Neuwirth 496dfd5640 T4 round 0: iced is eliminated, and the probe that cleared it proved nothing
Round 0 is the ladder's cheapest hard gate: for each candidate, a desk survey
and a demonstrated accessibility route -- a window exposing one node with a
role and a name, read back through the platform adapter. It is first precisely
so that a candidate with no accessibility story costs one round instead of
four.

C1 (egui/eframe 0.35.0) passes on a first-party route: egui-winit's accesskit
feature is literally dep:accesskit_winit, so the chain runs eframe ->
egui-winit -> accesskit_winit -> accesskit_unix and inherits that crate's
window-lifecycle handling. C2 (vello 0.9.0 + winit 0.30.13) passes on the
manual accesskit_winit route the contract names, wired into the same
ApplicationHandler that owns the vello renderer.

C3 (iced 0.14.0) FAILS, and the interesting part is that it first reported as
a pass. The probe registered a hand-built two-node tree through
accesskit_unix::Adapter, which takes no window handle at all -- only handlers
-- and registers with AT-SPI from process identity. It read back cleanly. It
also happened to label its button exactly as iced's own view() labelled a
button, so the transcript looked as though iced had produced it. Deleting iced
from that probe would produce the identical readback. Round 0 asks whether the
CANDIDATE exposes a route; a process-level side channel answers a different
question.

The verdict is recorded with dual attribution, because two distinct things
went wrong. The probe-design defect is the false positive above. The candidate
limitation -- which alone fails the round -- is that iced 0.14 ships no
accessibility integration anywhere (accesskit appears in no iced crate
manifest) and its stock runner hands application code neither the winit
ActiveEventLoop nor a pre-visibility Window, both of which every
accesskit_winit::Adapter constructor requires. That scoping matters and is
deliberate: iced_winit documents a conversion module for users implementing a
custom event loop, so a hand-built shell remains conceivable but unproven, and
would mean owning the shell. Upstream iced #552 is still open.

The evidence file keeps the verifier's factual READBACK: PASS beneath a
ROUND-0 RESULT: FAIL annotation rather than being rewritten. A corrected record
that erases the false positive teaches nobody why it was false.

Two findings carried forward. C1's frame node is unnamed -- its readback path
is application:'probe-egui' / frame:'' / button:'...' where C2 names its frame
-- which is non-disqualifying here but means a screen-reader user hears an
unnamed window, and round 3 must check it. And AT-SPI application registration
is gated behind two settings that are off by default; without both, probes
connect to the bus and enumerate zero applications, which would read as a
candidate failure rather than the environment absence it is.

The spike workspace lives outside the root workspace with its own lockfile;
the root gains one line, exclude = ["spikes"], and nothing else. Round 0's
probes depend on no epiphany crate, which is what makes this round independent
of the pinned source baseline the later rounds need.

Root gate unchanged: fmt clean, clippy -D warnings clean, 1371 tests passed 0
failed. Spike workspace gated separately: fmt clean, all probes build --locked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
2026-07-28 19:01:51 -04:00
Levi Neuwirth 42a8cc6dc5 T4 pin 7: the fixture recipe is approved, and the spike tree opens with it
Pin 7 of the spike contract holds that staff-by-measure dimensions do not pin a
workload, and the workload is what gets tessellated. This is that recipe: one
region, one staff instance per staff, a fixed seven-event bar of six eighths
plus a quarter, and integral per-measure quotas for rests, accidentals, ledger
notes and slurs. Nothing is sampled. The generator still takes a seed, but its
only role is replica minting as in the testkit fixtures -- content is fixed by
construction, which is strictly stronger than a fixed seed over a free
selection algorithm.

Three review rounds, and nearly every finding was the same mistake in a new
place: a claim about the pipeline inferred from an adjacent fact instead of
read from the code that emits.

Ties are not curves. The Curve doc says slurs engrave to one "and ties and
other span curves will follow" -- future tense; constrained.rs has a Slur arm
and no Tie arm, so ties trace as zero-extent anchors. The casting lines first
cited only transform curves produced upstream. Ties are now zero for the
primary ladder, and the decisive reason is not the miscount: every structural
anchor is emitted at the default x, which is the clef column, so casting files
all of them into the first system regardless of musical position. At the top
rung that would pile 2,400 anchors into the most likely damage target and
distort the per-system rebuild timing the whole spike turns on.

The census counts the parser's output, not the source string. H and V each
become a PathCommand::LineTo, so counting M/L/C/Z tokens undercounts exactly
the commands a tessellator consumes -- a sharp is 71, not 57. The census is now
defined as render_data(name).outline.len(), which measures the API candidates
actually see and removes the whole error class.

Ledger lines are emitted per staff step, not per off-staff note. A note one
space outside the staff emits none at all; the first ledger sits two steps out.
So off-staff pitches are pinned by absolute step rather than by "ledger
position", which would have produced about half the ledger ink it implied.

Also corrected: an all-eighths bar cannot yield a quarter rest (rest_glyph maps
value to glyph exactly), 4/4 uses timeSig4 which is single-subpath rather than
the multi-subpath coverage first claimed, barlines are glyphs and not strokes,
and the accidental cadence is one fixed cycle with bounded edge imbalance --
not equal thirds, since no rung's measure count divides by three.

Two-voice engraving is NOT unproven -- rs4_two_voice_counterpoint passes
against the real engraver. The narrower gap is collision handling, rest
displacement and deliberate stem-direction separation, and the ladder stays at
one voice as an isolation choice rather than out of doubt.

The remaining gate is separate and still open: no candidate code exists until
the generator emits its per-rung census, glyph histogram, SHA-256 of each
rung's canonical layout bytes, and the F4 engrave preflight -- and that output
is approved. Recipe approval is not generator approval.

Docs only. No Cargo manifest yet, so the root workspace is untouched; the
exclude line lands with the generator.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
2026-07-28 17:50:12 -04:00