288 lines
17 KiB
Markdown
288 lines
17 KiB
Markdown
# T4 toolkit spike — decisions and findings log
|
|
|
|
Governed by `spec/CONTRACT_EDITOR_T4_SPIKE.md`. This file records
|
|
implementation decisions and named deviations, per round.
|
|
|
|
## Round 0 — accessibility route + desk survey
|
|
|
|
**Environment prerequisite, not obvious from the contract:** on this
|
|
machine (sway, AT-SPI2 via `at-spi-bus-launcher` + `at-spi2-registryd`),
|
|
AT-SPI application registration is gated behind two settings that are
|
|
*off* by default even though the bus itself is always up:
|
|
|
|
```
|
|
gsettings set org.gnome.desktop.interface toolkit-accessibility true
|
|
gdbus call --session --dest org.a11y.Bus --object-path /org/a11y/bus \
|
|
--method org.freedesktop.DBus.Properties.Set org.a11y.Status \
|
|
ScreenReaderEnabled "<true>"
|
|
```
|
|
|
|
Without both, `Atspi.get_desktop(0)` enumerates **zero** applications even
|
|
while a probe process is alive, rendering, and actually connected to the
|
|
AT-SPI D-Bus (confirmed separately via `busctl --address
|
|
unix:path=$XDG_RUNTIME_DIR/at-spi/bus list`). This is a real environment
|
|
absence, not a candidate defect, and any later round run in a fresh
|
|
sandbox/session must redo both steps before trusting a `NOT RUN` verdict on
|
|
accessibility.
|
|
|
|
**Verifier substitution.** The contract's Round 0 evidence rule allows "a
|
|
small verifier binary using the `atspi` crate, **or an equivalent AT-SPI
|
|
client**". `a11y-verifier/verify.py` uses `gi.repository.Atspi` (the
|
|
official AT-SPI2 GObject-introspection binding — the same library behind
|
|
Orca and Accerciser) instead of the Rust `atspi` crate. This was a
|
|
deliberate substitution: the Rust crate's async zbus proxy API would have
|
|
had to be learned from source rather than from any working example, and
|
|
`pyatspi` was already confirmed reachable on this machine. It is a
|
|
standalone process, external to every probe, and performs a real tree walk
|
|
from the AT-SPI registry — it satisfies "a real client query of the tree",
|
|
not "printing your own struct".
|
|
|
|
**C1 (egui).** First-party route, the full chain being
|
|
`eframe` → `egui-winit` → `accesskit_winit` → `accesskit_unix`: `egui-winit`'s
|
|
`accesskit` feature is literally `dep:accesskit_winit`
|
|
(`egui-winit-0.35.0/Cargo.toml:55`), on by default through `eframe` in 0.35, and
|
|
`accesskit_winit` delegates to the platform crate. So C1 gets the
|
|
window-lifecycle handling that C3's bypass would have forfeited. No manual
|
|
wiring was needed. `probe-egui` draws one button; readback: **PASS**. See
|
|
`round0-evidence/c1-egui-readback.txt`.
|
|
|
|
**Carry forward — C1's frame node is unnamed.** C1's readback path is
|
|
`application:'probe-egui' / frame:'' / button:'EpiphanyProbeButton'`, where C2
|
|
and C3 both name their frame. The window title does not reach the AT-SPI frame
|
|
node under `eframe` 0.35's default wiring. **Non-disqualifying** — round 0
|
|
requires one node with a role *and* a name, and the button carries both — but
|
|
it is a real gap: a screen-reader user hears an unnamed window. Round 3
|
|
(accessibility semantics) must check it, since window identity is part of
|
|
navigation, and it should not be rediscovered there as a surprise.
|
|
|
|
**C2 (vello + winit).** Manual `accesskit_winit` route, exactly as named by
|
|
the contract: `probe-vello` builds the accessibility tree by hand
|
|
(`accesskit::TreeUpdate`) and drives it through
|
|
`accesskit_winit::Adapter::with_event_loop_proxy`, wired into the same
|
|
`winit::application::ApplicationHandler` that owns the vello
|
|
`RenderContext`/`Renderer`/`Scene` (the vello render pass is real, not a
|
|
stub — it draws a filled rounded rect every frame, following vello's own
|
|
`examples/simple` pattern at `linebender/vello@main`). Readback: **PASS**.
|
|
See `round0-evidence/c2-vello-readback.txt`.
|
|
|
|
**C3 (iced) — ROUND-0 RESULT: FAIL. Eliminated at round 0, adjudicated
|
|
2026-07-28 by coordinator review; no waiver sought or granted.** The initial
|
|
report recorded this as "PASS with a flagged deviation". That adjudication was
|
|
wrong and is corrected here. Under pin 14(c) C3's disqualifying set is not
|
|
passed; keeping it would require an explicit recorded ruling amendment, which
|
|
was declined.
|
|
|
|
**Dual attribution, because the two failures are different in kind.**
|
|
|
|
*Candidate limitation — this alone fails the round.* iced 0.14 ships **no
|
|
accessibility integration at all**: `accesskit` appears in no iced crate
|
|
manifest (verified across every `iced*` crate in the 0.14 tree). And its
|
|
**stock runner** exposes to application code neither a
|
|
`winit::event_loop::ActiveEventLoop` nor a pre-visibility
|
|
`winit::window::Window`; both appear only inside `iced_winit`'s own private
|
|
`ApplicationHandler` impl, with `create_window` at `iced_winit-0.14.0/src/lib.rs:350`
|
|
inside iced's runner. Every `accesskit_winit::Adapter` constructor requires
|
|
both and panics if the window is already visible.
|
|
**Scope this to the stock runner, deliberately:** `iced_winit`'s own docs offer
|
|
a `conversion` module "for users that decide to implement a custom event loop",
|
|
so a hand-built shell carrying a real route remains **conceivable but
|
|
unproven** — and it would mean owning the shell. Upstream iced #552 remains
|
|
open. "Provably closed" applies to the stock runner, not to iced in principle.
|
|
|
|
*Probe-design defect — why the first report read PASS.*
|
|
`accesskit_unix::Adapter::new()` takes **no window handle**, only handlers, and
|
|
registers with AT-SPI from process identity
|
|
(`accesskit_unix-0.22.1/src/context.rs`; `app_name()` reads
|
|
`std::env::current_exe()`). `probe-iced` therefore registered a **hand-built
|
|
static tree**, decoupled from iced's window, focus, and event lifecycle, with
|
|
every action discarded — while `view()` happened to label its button
|
|
identically, which is what made the transcript read as though iced produced it.
|
|
**Deleting iced from the probe would produce the identical readback.** That is
|
|
the disqualifying fact: round 0 asks whether the *candidate* exposes a route,
|
|
and a process-level side channel answers a different question. That
|
|
`accesskit_unix` sits one layer beneath `accesskit_winit` does not make it a
|
|
route *for the candidate* — that was the reasoning error, and it is recorded as
|
|
a probe defect rather than folded into the candidate's result.
|
|
|
|
Evidence is preserved rather than rewritten: `round0-evidence/c3-iced-readback.txt`
|
|
keeps the verifier's factual `READBACK: PASS` under a `ROUND-0 RESULT: FAIL`
|
|
annotation, so the false positive stays visible alongside its adjudication.
|
|
|
|
One consequence survives the corrected verdict and is worth carrying, because
|
|
it would apply to any future hand-built route: bypassing `accesskit_winit`
|
|
forfeits that crate's window-lifecycle handling — deactivation on window close,
|
|
multi-window disambiguation, focus-driven activation. Any real iced integration
|
|
would have to build and maintain that wiring itself rather than inheriting it,
|
|
which is a maintenance-surface fact, not merely a round-0 curiosity.
|
|
|
|
## Round 0 — desk survey
|
|
|
|
All version/date/MSRV figures were fetched live (crates.io API + GitHub
|
|
`Cargo.toml` at the released tag), not from memory or the contract's
|
|
2026-07-23 snapshot. See the Round 0 report for the full table.
|
|
|
|
Fill-rule documentation, quoted verbatim from source (not inferred from
|
|
behavior — that is round 1's job):
|
|
|
|
- **C1 (`lyon_tessellation` 1.0.20, via `lyon_path` 1.0.19):**
|
|
`pub enum FillRule { EvenOdd, NonZero }`, with
|
|
`DEFAULT_FILL_RULE: FillRule = FillRule::EvenOdd` and an explicit
|
|
`is_in(winding_number)` implementation for both.
|
|
- **C2 (`peniko` 0.6.1, vello's fill-style type):** `pub enum Fill { NonZero, EvenOdd }`,
|
|
each with a full doc comment ("All regions where the winding number of
|
|
the path is not zero will be filled" / "... is odd will be filled").
|
|
- **C3 (`iced_graphics` 0.14.0, `geometry::fill`):**
|
|
`pub enum Rule { NonZero, EvenOdd }`, doc pointing at the SVG
|
|
`fill-rule` spec, default `NonZero`.
|
|
|
|
All three document both rules explicitly. No candidate is eliminated on
|
|
this desk-survey item; round 1 is where it is actually tested.
|
|
|
|
## Round 1 — the precommitted oracle: SUPERSEDED FIRST PASS
|
|
|
|
> **SUPERSEDED DISCOVERY — NEVER COMMITTED.** This section records the
|
|
> four-glyph oracle built against the pre-Revision-6 contract, kept because its
|
|
> two findings are what *caused* the amendment. It was never committed and is
|
|
> not the oracle any candidate renders against; **the authoritative account is
|
|
> the Revision 6 amendment section below** (five glyphs, two check classes,
|
|
> explicit status model). In particular, `fClef`'s `background_satisfied =
|
|
> false` below is the superseded encoding — under Revision 6 `fClef` is a
|
|
> *satisfied* disjoint-component result — and every "committed" in this section
|
|
> describes an intent that this pass never reached.
|
|
|
|
**This is the oracle only — no candidate has rendered against it yet.**
|
|
`round1-oracle/` (new spike-workspace member) derives, for `gClef`, `fClef`,
|
|
`timeSig8`, `accidentalFlat` from `BravuraGlyphCatalog::render_data`: a
|
|
pinned staff-space -> device-pixel transform, Bezier-flattened outlines
|
|
(recursive de Casteljau, 0.0005 staff-space flatness tolerance), an even-odd
|
|
/ nonzero point-in-path classifier, and a grid search (0.01 staff-space
|
|
step) that derives >=3 ink and >=3 bounded-hole background sample points per
|
|
glyph, each with >=8 device-px clearance from any outline edge, entirely
|
|
programmatically — no coordinate here was chosen by eye. Output:
|
|
`round1-oracle/oracle.json` (committed, machine-readable) and
|
|
`round1-oracle/ORACLE_SUMMARY.md` (human-readable). **No rendering,
|
|
tessellation, or windowing crate is a dependency of this crate** — see its
|
|
Cargo.toml and module doc comment.
|
|
|
|
**Transform (pinned):** `device = (staff.x * scale + tx, ty - staff.y *
|
|
scale)`, `scale = 100` device px per staff space, `tx`/`ty` center each
|
|
glyph's own flattened bounding box in the pin-4 1920x1080 target — one
|
|
uniform rule applied identically to all four glyphs, not a per-glyph fudge.
|
|
|
|
**Subpath counts.** All four match the contract's recorded starting point
|
|
exactly: `gClef` 4, `fClef` 3, `timeSig8` 3, `accidentalFlat` 2 (asserted in
|
|
`subpath_counts_match_the_recorded_starting_point`).
|
|
|
|
**Finding — `fClef` has no bounded hole at all.** Its three subpaths (the
|
|
solid clef bowl, area 2.534, plus two disjoint dot subpaths, areas 0.153 and
|
|
0.148) are not nested — the search finds **zero** grid hits inside the outer
|
|
contour and outside the even-odd fill, at both the main search's resolution
|
|
and an independent finer corroboration (0.005 staff-space step, no
|
|
clearance floor at all;
|
|
`finding_corroboration::fclef_has_no_bounded_hole_at_any_resolution_or_clearance`).
|
|
Per Round 1's hole-check clause ("asserted by the derivation, not assumed"), no fallback/relaxed point was substituted:
|
|
`fClef`'s oracle carries 3 ink points and **zero** background points, and
|
|
`background_satisfied = false` is recorded in `oracle.json` alongside the
|
|
diagnostic. The other three glyphs each got 3 ink + 3 background points with
|
|
no relaxation needed (thousands of qualifying grid candidates for each).
|
|
|
|
**Finding — even-odd and nonzero agree on every hole point found.** Mutation
|
|
(b) (flip even-odd to nonzero on the selected hole points) was run for
|
|
`gClef`, `timeSig8`, `accidentalFlat`'s hole points and **none reclassify**:
|
|
Bravura's outer contour and its hole wind in *opposite* directions (the
|
|
well-formed-font convention), so nonzero winding cancels to 0 at exactly the
|
|
points where the even-odd crossing count is 2 — both rules correctly exclude
|
|
the hole. This is real information about the font data (recorded, not
|
|
smoothed over), and it means the literal "flip the rule" mutation does not,
|
|
for this data, demonstrate that fill-*rule* choice is load-bearing. A
|
|
supplementary mutation
|
|
(`mutation_b_supplement_hole_points_reclassify_under_naive_outer_only_fill`)
|
|
demonstrates the property the check actually needed — that respecting inner
|
|
subpaths at all is load-bearing — by showing every selected hole point *is*
|
|
inside a naive outer-contour-only fill (what a renderer ignoring holes
|
|
entirely would wrongly paint as ink) while being outside the real
|
|
whole-outline fill.
|
|
|
|
Full mutation evidence (a/b/b-supplement/c) is in
|
|
`round1-oracle/src/lib.rs`'s `tests` and `finding_corroboration` modules, run
|
|
via `cargo test -p round1-oracle -- --nocapture`.
|
|
|
|
## Round 1 — Revision 6 amendment (five glyphs, two check classes, status model)
|
|
|
|
**Both findings above changed the contract, not just this oracle.**
|
|
`fClef` having no bounded hole and criterion 1's fill-*rule* framing being
|
|
moot on Bravura's correctly (oppositely) wound contours were reported as
|
|
findings against the pre-Revision-6 contract; the coordinator's response was
|
|
to amend `spec/CONTRACT_EDITOR_T4_SPIKE.md` to Revision 6 rather than treat
|
|
`fClef` as a shortfall. Round 1 is now **five glyphs in two check classes**,
|
|
and this oracle is amended to match (uncommitted diff on top of the original
|
|
oracle; the mechanics below are unchanged — same flattening, same
|
|
point-in-path classifier, same transform, same clearance floor — only the
|
|
requirement model and the glyph set changed):
|
|
|
|
- **Bounded-hole check** (unchanged mechanics): `gClef`, `timeSig8`,
|
|
`accidentalFlat`, and now **`noteheadHalf`** (new: two subpaths, measured
|
|
`ring_signed_areas = [0.903, -0.368]` — the authoritative figure, now also
|
|
what the contract records; an earlier `[0.902, -0.367]` came from a coarser
|
|
fixed-step flattening). Still >=3 ink + >=3 background points per glyph, every
|
|
background point inside a bounded hole.
|
|
- **Disjoint-component check** (new): `fClef` alone, no longer folded into
|
|
the bounded-hole class it never actually belonged to. It carries **no
|
|
background requirement** — that is its design (bowl plus two solid,
|
|
disjoint dots) — and instead requires >=1 ink point inside **each** of its
|
|
three filled subpaths, each point tagged with `subpath_index` so the
|
|
oracle proves coverage of every component rather than three ink points
|
|
that could all land in the bowl. Verified two ways: a finer (0.005
|
|
staff-space step) grid corroboration (renamed
|
|
`fclef_has_zero_bounded_hole_grid_hits_at_a_finer_0_005_grid`, since the
|
|
old name overclaimed "any resolution" for what was actually one grid) and
|
|
a genuinely resolution-independent topological check,
|
|
`subpaths_are_mutually_non_nested` /
|
|
`fclef_subpaths_are_topologically_non_nested`, which asserts no subpath's
|
|
vertices lie inside another subpath — a property of the flattened polygon
|
|
itself, not of any sampling step, and what actually justifies the
|
|
stronger claim.
|
|
|
|
**Status model.** `GlyphOracle` now carries an explicit `Requirement` enum
|
|
(`BoundedHole` / `DisjointComponents`), per-requirement booleans
|
|
(`background_required`/`background_satisfied`,
|
|
`subpath_coverage_required`/`subpath_coverage_satisfied`), and one overall
|
|
`satisfied: bool` — the field to read. `fClef` is `satisfied = true` with
|
|
`background_required = false` and zero background points; a bounded-hole
|
|
glyph that failed to find enough background points would be
|
|
`satisfied = false`. The two are no longer distinguishable only by an absent
|
|
field, which was the defect the amendment closes.
|
|
|
|
**Fill-rule equivalence, now asserted, not merely reported.** The original
|
|
oracle's `mutation_b_...` test printed `reclassifies_under_nonzero=false` for
|
|
every hole point without asserting on it. `derive_bounded_hole_oracle` now
|
|
asserts `!ev.nonzero_filled` for every selected background point at
|
|
derivation time (fails loudly if the opposite-winding assumption is ever
|
|
false for a glyph's real data), and
|
|
`mutation_b_nonzero_rule_equivalence_on_hole_points_is_asserted` asserts the
|
|
same independently at the test level. `ring_signed_areas` — the measured
|
|
signed area of every subpath, in ring order — is now recorded on every
|
|
glyph's oracle output (`oracle.json` and `ORACLE_SUMMARY.md`), not just
|
|
observed in this log: `gClef [8.702, -0.691, -1.803, -0.509]`, `timeSig8
|
|
[2.674, -0.435, -0.515]`, `accidentalFlat [1.040, -0.257]`, `noteheadHalf
|
|
[0.903, -0.368]`, `fClef [2.534, 0.153, 0.148]` — all within measurement
|
|
tolerance of the contract's recorded values.
|
|
|
|
**New kill test.** `mutation_d_largest_contour_only_fill_misses_the_dot_points`
|
|
is the disjoint-component analogue of the outer-contour-only mutation kept
|
|
from the original oracle: it asserts that filling only `fClef`'s largest
|
|
subpath (the bowl) fails to contain either dot's ink point — the literal
|
|
"tessellator keeps only the largest contour" scenario Round 1 names as the
|
|
reason this check exists. The original outer-contour-only mutation
|
|
(`mutation_b_supplement_...`) is unchanged and still runs, now over all four
|
|
bounded-hole glyphs including `noteheadHalf`.
|
|
|
|
**Citations.** Every `pin 13 item N` citation in `round1-oracle/src/lib.rs`,
|
|
`src/main.rs`, `ORACLE_SUMMARY.md`, and this file has been replaced with a
|
|
direct citation to the Round 1 clause's own wording — pin 13 has no numbered
|
|
items in Revision 6 (or any prior revision), so those citations pointed at
|
|
nothing.
|
|
|
|
`oracle.json` and `ORACLE_SUMMARY.md` are regenerated from the amended code
|
|
(`cargo run -p round1-oracle` from `spikes/editor-toolkit/round1-oracle/`).
|