Decisions
0004. Public Kyo APIs only
The runner calls no private[kyo] member and uses no reflection; where kyo-ui keeps a rule private, the runner copies its semantics and the surface guard pins what it reads.
Status
Accepted
Date
2026-10-13 (the rule of C3, review item RR N9). The rule grew with L1b on 2026-10-19.
Context
- kyo-ui keeps its own runner rules private. The key dispatch and the region walk are in
kyo.internal, and the typeAttrsof the element attributes isprivate[kyo]. - The compiler rejects a direct call to a
private[kyo]member from packagetui4s. Two ways around it exist: a source file in packagekyo, or reflection. - The prototype runner copied about 400 lines of these rules (
Dispatch242 lines,Mount181 lines at the time). - The kyo-ui AST is sealed, so tui4s cannot add its own node types. A component node is a kyo-ui
Reactivewhose signal is in a global weak registry (internal.ComponentSpec). UI.scrollIntoViewreaches only aprivate[kyo]sink. Under tui4s it does nothing.
Decision
- The runner uses only public Kyo and kyo-ui APIs. It calls no
private[kyo]member, has no source file in packagekyo, and uses no reflection. - It reads the attributes through the public member
Element.attrsand never names the typeAttrs. - Where kyo-ui keeps a rule private, the runner copies its semantics (
internal.Dispatch,internal.Mount) and tests them. - The surface guard (
kyo-ui-surface.txt, a tasty-query test) pins each member that the runner reads. For L1b it also pinsAttrs.focusRestore,Attrs.cssClasses, the cases ofPosition,Display,FlexWrapandStylesheet.Entry, and the CSS ofpositionanddisplayin kyo-ui'sCssStyleRenderer. - Where a private rule cannot be copied (
scrollIntoView), tui4s implements the browser rule itself (follow-focus,aria-activedescendant) and writes an upstream draft (ADR 0019).
Alternatives considered
- A source file in package
kyo. It compiles. Rejected: "not an option for a library that uses only public APIs" (the scroll-into-view draft). The notes give no other reason. - Reflection. Rejected by the same rule. Spike S1 also measured a reflection probe for the settle. Under load (3 JVMs on 2 CPUs) it gave 5.58 split frames per 10 000 events with the Kyo scheduler values, and 8 of 10 test runs passed. The public
Scheduler.status()probe passed 10 of 10 (ADR 0005). - A runner SPI upstream first (the draft "a synchronous change notice on
Signal", 2026-10-03). Not posted. tui4s does not wait for it; the draft stays a request.
Consequences
- Good: a Kyo bump cannot break tui4s silently through a private member. The guard shows each change of a pinned member (ADR 0003).
- Good: tui4s is an example of a third-party kyo-ui backend that uses only the public surface.
- Bad: the copied rules can drift from kyo-ui. The guard reduces this risk but does not remove it.
- Bad:
UI.scrollIntoViewis a no-op in the terminal, with no error. An app uses the focus oraria-activedescendantinstead. - Note:
experiments/ui-state/framehas a diagnostic fixture in packagekyo.framespike. It is not part of a published module.
References
- Review item RR N9 (runner report); the C3 rule; L1b (2026-10-19).
- Upstream drafts: kyo-ui scroll-into-view; kyo-ui change notice (ADR 0019).
- CONTRIBUTING.md: "UI architecture and API design" ("Use only Kyo's public APIs"); "Rules for code" (the RR N9 item).
0003. Pin Kyo 1.0.0-RC7 and JDK 25
Kyo and kyo-ui stay at 1.0.0-RC7 for the first release, on JDK 25 and Scala 3.9.0, and a tasty-query surface guard pins the kyo-ui members that the runner reads.
0005. A SOFT frame contract and an own watcher loop
One frame per event with all its consequences, 0 split frames with no load and at most 1 per 10 000 under load; the runner watches signals with its own loop and a repair tick because of two Kyo RC7 Signal bugs.