Decisions
0006. Purity: edge files, lint and a pure painter
The functional rewrite (L0, phases P1 to P6) keeps impure code in nine edge files and a few marked KEEP sites, checked by scalafix DisableSyntax, with a pure painter, a synchronous release stack and one fiber gauge.
Status
Accepted
Date
2026-10-17 (D1, D2, the purity plan); landed on 2026-10-19 (L0 done).
Context
- The user asked for a cost-effective functional rewrite, with the skills
functional-designandscala-functional-designas the rules: impure code only for performance, and only behind an effect. - The audit of 2026-10-17 found 93 impure sites. "Whiles everywhere" was not correct (about 12 real loops, most in edge code). The shape was the problem: about 23 volatile or nullable fields, four copies of "run every step, keep the first error", exceptions for expected outcomes, a painter that wrote into the jatatui buffer while it decided, and
java.util.concurrentwhere Kyo has primitives. - Classification: 29 sites stay behind one seam each, 2 kernels stay, about 50 are replaced and 13 are deleted.
- A JVM shutdown hook must restore the terminal. Kyo RC7
Finalizer.closeonly spawns a fiber to drain aScope, so a hook cannot runScopefinalizers. - L1 (scroll, overlays) changes the painter, and on a pure painter these features are cheaper and testable with properties. So L0 came before L1 (D1).
Decision
- Impure code is only in an edge file or on a line with
// scalafix:ok DisableSyntax.<id>; KEEP: <reason>. An edge file wraps one foreign API: line 1// scalafix:off DisableSyntax, line 2// EDGE: <API>. There are 9 edge files, all intui4s.internal, and 15 KEEP sites. - scalafix DisableSyntax checks the main sources in
sbt check;scripts/edge-check.shchecks the headers and the comments in CI. - The painter is pure:
Paintgives aPlan(ops and damaged row spans).Canvas.scalahas the only two loops over cells,Canvas.writeandCanvas.diff, each with a law and a property test (P5). The old painter stays in the tests as the oracle. - The release of a run is one synchronous stack (
ReleaseStack, P3): each acquire pushes its release;runAllruns them once, last first; a failing step does not stop the next. One shutdown hook per run. - The live fibers of a run are one Kyo
AtomicRefgauge (FiberGauge, P2); start and add are one bracket. - An enum, not a Boolean, selects runtime behaviour. Property tests use kyo-test-prop with explicit
Genvalues.TuiErrorisNoStackTracewith a non-null cause (A4).
Alternatives considered
- Stay with the imperative runner. The user rejected the shape of the code as "badly written", and L1 needed a testable painter.
- A Kyo
Scopefor the terminal release. Not possible from a JVM hook:Finalizer.closeonly spawns a drain fiber (KyoScope.scala:346-383). - A purity script, a ratchet and TODO headers. Cut by the skill review: scalafix DisableSyntax and one CI grep are enough.
- Move the edge files into a package
internal.edge. Cut by the skill review (ninegit mvfor no behaviour). - ScalaCheck. Not used: kyo-test-prop exists for RC7.
- P7, a typed data model. Out of the rewrite (D2 c). Two casts hold by construction.
- A third kernel over cells. Forbidden: a new write is a new
Op.
Consequences
- Good: the branch shrank by 2,149 lines net (151 files, +6,843/-8,992). Suppressions went 105, 49, 47, 38, 29, 19, then 15 KEEP sites.
- Good: the restore order is one list, and
ReleaseStackTestchecks it with 100 seeded rounds and a negative control. - Good: the oracles and property tests pin real fixes: the clip counts cells, not UTF-16 chars (
ClipTest), and an incremental frame equals a full frame (StepPropertyTest). - Bad: the edge files and KEEP sites need review by hand; the lint cannot judge them.
- Bad:
Component.initialis pure, so a per-mount effect value arrives throughonMount.
References
- Roadmap: 2.4, D1, D2. Skill review amendments A1 to A11.
- Handoff: P0 to P6 and "L0 DONE 2026-10-19". Decision log: D20, D23, D24 (open, #46).
- CONTRIBUTING.md: "Purity", "Rules for code" (the release stack, the fiber gauge).
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.
0007. Compiler flags from sbt-tpolecat
sbt-tpolecat sets the scalac options; warnings stay warnings on a developer machine, and CI (SBT_TPOLECAT_CI) turns them into errors.