Exhaustive interaction-driven visual parity (drive every event over every element, capture the dynamic result, compare against a reference)
develop specs/develop/interaction-visual-parity.kmd
DRAFT — parent design the owner named (2026-07-05). The Koder UI-parity tooling (`/k-manifest` · `/k-parity` · `/k-mirror` + the `/k-test-gen-*` engine) today compares mostly STATIC structure/snapshots and drives only a SUBSET of pointer events against ONE surface class (the Kroma tiling terminal, via `kroma_test_state` + the #088 introspection oracle). It does NOT drive EVERY mouse/pointer interaction over EACH inventoried element and observe the real dynamic behaviour (movement, resize, hover/press effects, popovers, animation) to compare it against the reference app. This spec defines that missing capability — the unifying "drive-and-compare" layer that sits above the existing interaction-test families. Implementation is deferred pending owner prioritisation vs. the Kolide-on-Kroma slices; this document exists to scope and ratify the direction first.
Quando esta spec se aplica
Todos os triggers
- Decide how the Stack proves per-element interaction parity of a Koder UI against a reference app
- Extend /k-manifest to declare per-element interaction capability, or /k-mirror to drive events
Corpo da especificação
Exhaustive interaction-driven visual parity (DRAFT)
The gap (evidence)
The owner asked (2026-07-05): do our frameworks behind /k-parity / /k-mirror / /k-manifest execute
every mouse event — hover, hint/tooltip, right-click, left-click, wheel up/down, drag-and-drop, hot-corner,
… — over each inventoried graphic element, to visually compare the real dynamic behaviour (movements,
resizes, effects)? An audit of origin/master (2026-07-05) found no:
/k-manifestinventories elements; the per-elementbehaviorsfield is free-text, not a structured, drivable enumeration of supported events/gestures/states./k-mirrorcompares static trees — a one-shot#088 KROMA_INTROSPECTdump vs. the reference AT-SPI tree — and its perf dimension only reads the RFC-012 corpus. It does not synthesise events per element. Its "drive-to-state" mode is the open kroma#124 (pending)./k-parityis a static feature × variant matrix (zero runtime).- The only live event-driving path is
kroma_test_state(engines/sdk/kroma/src/test_state.rs) + the #088 introspection oracle (kroma#092) — and it drives a subset (press/release, right-click routing, selection drag, header drag/double-click,wheel_up) against the kterm surface only. The/k-test-gen-*generator engine that would emit these systematically targets Flutter/Compose/web, not Kroma-native Rust (kroma#092 "Why").
Current per-event coverage for Kroma-native UI (the baseline this spec must close):
| Event / behaviour | Today |
|---|---|
| left-click / press-release / selection-drag / header drag+double-click (maximize) | DRIVEN-LIVE, hand-written, kterm only (pointer-interaction-tdds P1/P3/P5) |
| right-click / context-menu routing | DRIVEN-LIVE, kterm only |
| wheel-up | driver live, scroll-offset oracle deferred (px math unit-only) |
| wheel-down | GAP (no primitive) |
| hover (effect/state) | SPEC-ONLY (states.kmd overlays; only a negative assert exists) |
| hint / tooltip | GAP (states.kmd R3 = "not enforced") |
| hot-corner (as an interaction) | SPEC-ONLY (exists only as a motion ripple in animation-parity) |
| generic double-click / drag-and-drop / free resize | GAP |
| visual comparison of the dynamic result (movement/resize/effect/animation-over-time, per element, vs a reference) | GAP for Kroma (motion/animation-parity targets Flutter/web) |
Intent
Every interactive element in a Koder UI surface's inventory MUST be exercised with the full set of input events it can receive, and the dynamic result of each (geometry delta, state overlay, popover open, animation curve, effect) captured and compared — both against the element's spec and, where a reference app exists, against the reference's observed behaviour. "Compare what really happens in practice", not just the at-rest structure.
The four pillars (each maps to an existing/pending piece — this spec unifies them)
- Declare (manifest).
/k-manifestemits, per element, a structured interaction descriptor: the events it accepts (hover/hint/click{left,right,middle}/double-click/press/wheel{up,down}/drag/drop/long-press/hot-corner), its hit-region(s), and the observable it produces (state overlay / popover / geometry change / animation). Replaces the free-textbehaviorsstring with a drivable schema. (New — extends/k-manifest; feeds kroma#092 slice 1.) - Drive (harness). A synthetic-input harness drives that full event set headless against a running
surface, for every Kroma surface class (kterm, kolide panel/overview/notifications/greeter/settings,
kompose), not just kterm. Extends
kroma_test_state(test_state.rs) with the missing primitives (hover,wheel_down,tooltip-dwell, genericdouble_click,drag_and_drop,hot_corner) reading back through the #088 introspection oracle. (Extends kroma#092 beyond its slice-1 subset + surface.) - Capture + compare (mirror).
/k-mirrorgains a drive-to-state + visual-capture mode: for each inventoried element it fires the event, records the dynamic result (grim frame(s) / frame-diff / animation curve via the motion-parity prongs) and diffs it against the reference app's observed result. Supersedes the static-snapshot limitation (builds on kroma#124 drive-to-state +animation-parity-tddstemporal prongs +visual-regression-tddscapture). - Generate (test engine).
/k-test-gen-*emits these as regression tests per declared element (the reflexive "Kora generates its own TDDs" capability — kroma#092 slice 5 generalised from pointer to the full event set + visual/animation oracles).
Relationship to the existing interaction-test family (this is the superset, not a replacement)
pointer-interaction-tdds.kmd(P1–P5) — the pointer-desktop subset (capture routing, wheel-step, gesture-cancel, title-bar). Becomes the first driven slice under this umbrella.gesture-tdds.kmd— touch (explicitly excludes pointer-desktop). The touch analogue of pillar 2.animation-parity-tdds.kmd/ motion — the temporal comparison engine pillar 3 reuses to capture the dynamic result.visual-regression-tdds.kmd— static goldens; pillar 3 reuses its capture for the per-event frames.interaction/states.kmd— the hover/pressed/focus/hint contract pillar 2 must drive-and-verify per element (today only specified, not driven).
Slices (draft — refine on ratification; NOTHING implemented yet)
- S1 — Manifest interaction schema (pillar 1): the drivable descriptor +
/k-manifestemitter. Blocks kroma#092 slice 1. - S2 — Full-event harness (pillar 2): extend
kroma_test_statewith the missing primitives + generalise beyond kterm. Extends kroma#092. - S3 — Mirror drive-to-state + visual capture (pillar 3): kroma#124 + the per-event frame/curve capture and reference diff.
- S4 — Reflexive generation (pillar 4): kroma#092 slice 5, the full-event + visual/animation generator.
- S5 — Roll out per Kroma surface (kterm → kolide surfaces → kompose), each producing a per-element interaction-parity report vs. its reference.
S1 — Interaction descriptor schema (normative — implemented 2026-07-07)
S1 (pillar 1, "Declare") is the drivable contract every later pillar consumes: the harness (S2) drives
exactly the events a descriptor lists, the mirror (S3) captures exactly the observable it names, and the
generator (S4/#092) templatizes one test per descriptor entry. It supersedes the free-text behaviors
string for interactive elements — behaviors was prose a machine can't drive; this is an enumerated,
drivable structure. behaviors remains allowed for non-interactive narration (e.g. "auto-refreshes every
30s"); anything a pointer/gesture can trigger moves to interactions.
R1 — Per-object interactions array
Each object in a koder.manifest.json (see commands/k-manifest.md FASE 3) MAY carry an interactions
array. Every interactive element (button, menu, field, tab, divider, header, hot-corner, resizable region…)
MUST carry one; a non-interactive element (static text/image) omits it. Each entry:
{
"event": "<one of the closed event enum below>",
"hit_region": "self" | "<named-subregion>" | { "x": <n>, "y": <n>, "w": <n>, "h": <n> },
"observable": {
"kind": "<one of the closed observable enum below>",
"detail": "<the assertion the driver checks after firing — concrete, e.g. 'context menu anchored within this element's rect', 'height grows by one tile row', 'hover overlay = 8% white'>"
},
"spec_reference": "<the .kmd that governs this behaviour, or null>"
}
R2 — Closed event enum (drivable set)
hover · hint (tooltip-dwell) · click_left · click_right · click_middle · double_click ·
press · release · wheel_up · wheel_down · drag · drop · long_press · hot_corner.
A descriptor MUST NOT invent events outside this set (an unknown event is a schema error, not an
extension point — extend the enum here first). This mirrors S2's harness primitives one-to-one, so
"declared but undrivable" is impossible by construction.
R3 — Closed observable-kind enum (what the oracle asserts)
state_overlay (hover/press/focus visual per interaction/states.kmd) · popover (menu/tooltip/dialog
opens) · geometry_change (move/resize/maximize — a rect delta) · animation (a temporal curve, captured
via the motion-parity prongs) · selection (text/item selection state) · navigation (surface/route
change) · none (accepted but no observable — e.g. a swallowed event). Each maps to a #088-introspection
assertion: geometry_change→rect delta, popover→new node + anchor, state_overlay→state flag/colour,
animation→frame/curve diff, selection→selection range, navigation→active-surface id.
R4 — Emitter (/k-manifest) and consumer (#092 slice 1)
interactions is an LLM-deferred field (like behaviors): koder-manifest gen scaffolds the object
skeleton and reserves the field; the /k-manifest LLM pass fills it from the source (the widget's event
handlers + its governing spec). The kroma#092 slice-1 "manifest surface declaration" consumes this array
directly — each {event, hit_region, observable} is one pointer-interaction TDD the k-test-gen-pointer
generator emits. S1 delivers the schema + the emitter wiring; S2 delivers the harness that drives it.
Not in scope
Touch/gesture parity (owned by gesture-tdds.kmd); the reference-app collectors themselves (AT-SPI /
gschema, already in /k-mirror); perf/latency of each action (owned by ui-interaction-perf.kmd /
RFC-012 — though pillar 3 can co-capture timing).
Status
Parent design proposed 2026-07-05. S1 (the interaction descriptor schema, §S1 above) ratified +
implemented 2026-07-07 (owner /k-go): the drivable schema is normative and wired into /k-manifest;
kroma#195 tracks it and unblocked kroma#092 slice 1. S2–S5 UNGATED 2026-07-07 (owner /k-go: "rodar o
156 agora de uma vez") — the timing call vs. Kolide-on-Kroma is resolved in favour of running the epic now at
full breadth; S2 (full-event harness) is under way (kroma#197), then S3–S5. Still not a mandatory gate
(no spec-triggers entry) until the epic ratifies end-to-end. Tracking epic:
engines/sdk/kroma/backlog/in-progress/156-exhaustive-interaction-visual-parity-epic.md.