140 lines
7.8 KiB
Markdown
140 lines
7.8 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.
|