From d442530eccc65aa0cd6a908907ace8b9387018ed Mon Sep 17 00:00:00 2001 From: Burin Choomnuan Date: Wed, 2 Sep 2026 22:29:43 +1000 Subject: [PATCH] fix(deps): survive jolt #813, and declare the jolt floor glitter already had Jolt shipped three changes worth reacting to. Only one of them actually reached this project, and it arrived through a dependency rather than through glitter's own code. jolt #813 (RFC 0014) moved the host-class provider table out of the runtime: a library now declares the JDK classes it implements in its own deps.edn, and a class nobody claims fails to resolve. Nothing claimed java.time.ZoneId until jolt-lang/time#11 landed, so on a jolt carrying #813 `jolt flights` died outright and glitter.nexus.action-log-test was silently dropped from the suite. 29 tests to 26, 78 assertions to 67, exit 0. A green `bb test` did not catch that. Only the changed count did, which is worth remembering next time the toolchain moves. Fixed by pinning time at 33c5e98, two commits past the v0.0.7 tag, because no release carries the declaration yet. A local :jolt/provides was tried first and is deliberately NOT here: upstream declares all ten classes under one umbrella `jolt.time`, jolt keys the duplicate-claim error on the install namespace rather than the declaring coordinate, and the two spellings therefore refuse to resolve together. The other two jolt changes do not reach glitter, checked rather than assumed. #802 flipped ffi/write's argument order and the fixed-array descriptor, and glitter has neither a write call site nor an [:array ...] layout anywhere in src, test or examples. It renamed nothing, so all 223 defcfn bindings, foreign-callable and the free-callable bookkeeping stand. Also declares :jolt/min-version "0.7.24" (jolt #804), the floor this project already had in prose for correct local dates. It is pinned at glitter's real floor rather than the newest version because a library's floor is enforced on its consumers, measured against a dependency declaring one. Note it cannot fire today: the runtime that reads the key first shipped in v0.8.0, well above the floor. Docs: corrects the "jolt swallows the child's exit code" claim, stated flatly in the present tense across six files. Jolt fixed that in v0.7.28 and the task form now exits correctly. The recommendation stands but the reason changed, because a 0.7.24 floor leaves a live v0.7.24 to v0.7.27 window where the task form still swallows failures. Verified on jolt v0.8.0-45-g5e0e7a96: 29 tests, 78 assertions, all 27 live-GTK smokes green. --- CHANGELOG.md | 43 ++++++++++++++++++-- CONTRIBUTING.md | 20 ++++++--- README.md | 12 +++--- deps.edn | 72 ++++++++++++++++++++++++++++++--- docs/guide/examples.md | 8 ++-- docs/guide/nexus.md | 43 ++++++++++++++++++++ docs/guide/testing-and-tasks.md | 19 ++++++--- 7 files changed, 190 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 396bb3b..248774c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -126,14 +126,49 @@ pass. - **`.clj-kondo/hooks/jolt_ffi.clj` rewrites `jolt.ffi/defcfn` into an equivalent `defn`** so clj-kondo and clojure-lsp can see through the FFI macro. Without it every `gtk-*`/`g-*` binding reports as unresolved. -- **Never gate CI on `jolt `**: a `deps.edn` `:tasks` entry does not - propagate its child process's exit status (verified against jolt v0.6.3), so - `jolt test` prints failures and still exits 0. Use `jolt -M:` or a - `bb.edn` task. +- **Never gate CI on `jolt `**: on jolt v0.6.3 a `deps.edn` `:tasks` + entry did not propagate its child process's exit status, so `jolt test` + printed failures and still exited 0. Jolt fixed this in v0.7.28 and the task + form now exits with the command's status (measured as 7 on v0.7.29), but the + advice stands for a different reason: glitter's floor is v0.7.24, so the task + form is unreliable on part of the range it supports. Use `jolt -M:` or + a `bb.edn` task. - The codebase is uniformly `clojure-lsp`-formatted, including the ten files ported verbatim from Replicant. An earlier decision exempted those to preserve upstream diffability; it was reversed in favour of one project-wide style. +- **`deps.edn` declares `:jolt/min-version "0.7.24"`**, the oldest Jolt glitter + runs on, so the floor that was already documented in prose is now + machine-readable. The key is + [jolt#804](https://github.com/jolt-lang/jolt/pull/804), and a runtime below a + declared floor refuses to load rather than run. It does not fire today: the + Jolt that reads the key is v0.8.0 or newer, well above the floor, so a Jolt + old enough to have the UTC-date bug ignores it. It is declared for the next + break, which is why upstream shipped the key ahead of needing it. +- **Jolt's `jolt.ffi` arena release ([#802](https://github.com/jolt-lang/jolt/pull/802)) + does not affect glitter, checked rather than assumed.** That release moved + `ffi/write`'s value ahead of its offset, a change no runtime check can + distinguish since both spellings are integers, and flipped a fixed array + descriptor to `[:array element-type count]`. glitter has no `ffi/write` call + site and no `[:array ...]` descriptor anywhere in `src`, `test` or + `examples`, so neither can reach it. Nothing was renamed or removed in that + release either, so all 223 `defcfn` bindings, `foreign-callable` and the + `free-callable` retain/release bookkeeping stand unchanged; the new + arena-owned `ffi/callback` is an option, not a replacement. Verified against + `jolt v0.7.29-25-gd4e92a43`, and again on `v0.8.0-45-g5e0e7a96`, with the + unit suite and all twenty-seven live-GTK smokes green. +- **The pinned `jolt-lang/time` SHA moves two commits past `v0.0.7`, to pick up + its `:jolt/provides` declaration.** jolt + [#813](https://github.com/jolt-lang/jolt/pull/813) (RFC 0014) moved the + host-class provider table out of the runtime: a library now declares the JDK + classes it implements in its own `deps.edn`, and a class nobody claims fails + to resolve. Until + [jolt-lang/time#11](https://github.com/jolt-lang/time/pull/11) landed, nothing + claimed `java.time.ZoneId`, so on a jolt carrying #813 `jolt flights` died + outright and `glitter.nexus.action-log-test` was silently dropped from the + suite: 29 tests down to 26, 78 assertions to 67, exit 0. A green `bb test` + did not catch that; only the changed count did. The pin is deliberately ahead + of the `v0.0.7` tag because no release carries the declaration yet. ### Known gaps diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c3b5e0..a6bffe1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ this repo, but the pull request build is the authority. You need Jolt and a native GTK4: ```sh -jolt --version # developed against v0.6.3; re-verified on v0.7.24 +jolt --version # developed against v0.6.3; re-verified on v0.7.24 and v0.7.29 brew install gtk4 # macOS; on Linux use your distro's gtk4 + glib dev packages ``` @@ -37,6 +37,13 @@ from ~1.15ms to ~13.6us. See for the measurements. On an older Jolt the demo still runs, it just defaults its departure field to the UTC date. +`deps.edn` states that floor as `:jolt/min-version "0.7.24"`, so it is +machine-readable rather than only written down here. A Jolt below a declared +floor refuses to load the project instead of running it. Note this does not +change anything for an older Jolt today: the runtime that reads the key is +v0.8.0 or newer, comfortably above the floor, so a Jolt old enough to have the +date bug ignores the key entirely. It is there for the next breaking change. + `deps.edn`'s `:jolt/native` declares `glib-2.0`, `gobject-2.0`, `gio-2.0` and `gtk-4`, with Homebrew paths for darwin and `.so` names for linux. @@ -101,10 +108,13 @@ Each of these was a real bug at some point. Most are explained at length in [`docs/guide/`](docs/guide/index.md); the short forms are here so a reviewer can point at a number. -1. **Never gate CI on `jolt `.** A `deps.edn` `:tasks` entry doesn't - propagate its child process's exit status (verified against jolt v0.6.3), - so `jolt test` prints failures and still exits 0. Use `jolt -M:`, or - a `bb.edn` task; those already do it correctly. +1. **Never gate CI on `jolt `.** On jolt v0.6.3 a `deps.edn` `:tasks` + entry didn't propagate its child process's exit status, so `jolt test` + printed failures and still exited 0. Jolt fixed that in v0.7.28, and on + v0.7.29 the task form exits correctly (measured as 7). Keep using + `jolt -M:` or a `bb.edn` task anyway: glitter runs on jolt from + v0.7.24, and the fix only lands at v0.7.28, so the task form can still + swallow a failure on a supported version. 2. **`insert-before` covers two different cases**: a genuinely new child, and repositioning an *already-parented* child (a keyed reorder). diff --git a/README.md b/README.md index 53d3f81..33aa07e 100644 --- a/README.md +++ b/README.md @@ -124,11 +124,13 @@ smokes, each of which mounts a real window and exits non-zero on failure. **In CI, invoke the alias form, not the task form**: `jolt -M:test`, -`jolt -M:keyed`, and so on. Verified against jolt v0.6.3: a -deps.edn `:tasks` entry does not propagate its child process's exit status, so -`jolt test` reports failures on stdout and still exits 0, while `jolt -M:test` -correctly exits 1. The task shorthand is fine interactively; it cannot gate a -build. +`jolt -M:keyed`, and so on. On jolt v0.6.3 a deps.edn `:tasks` entry did not +propagate its child process's exit status, so `jolt test` reported failures on +stdout and still exited 0. Jolt fixed that in v0.7.28 and the task form now +exits with the command's status, measured as 7 on v0.7.29. The alias form is +still what CI should use, because glitter supports jolt from v0.7.24 and the +fix only arrives at v0.7.28, so the task form is unreliable across part of the +range this project runs on. ### Or via `bb` diff --git a/deps.edn b/deps.edn index cd80997..745e24a 100644 --- a/deps.edn +++ b/deps.edn @@ -1,5 +1,57 @@ {:paths ["src"] + ;; The oldest jolt glitter runs on, declared so a runtime below it refuses + ;; to load rather than run wrong (jolt #804). glitter is a library, and a + ;; library is the natural declarer here: it knows what its bindings need + ;; and the app pulling it in does not. + ;; + ;; 0.7.24 is the release that made (t/today) answer the machine's zone + ;; instead of UTC: the TZ-restore fix and the memoization that makes it + ;; affordable. Both are absent from v0.7.23. Without them + ;; examples/glitter/flights.clj opens its departure field on YESTERDAY for + ;; the first 10 hours of every AEST day, silently. That is the exact shape + ;; of failure this key turns into a message. See docs/guide/nexus.md. + ;; + ;; This does NOT fire today, and it is worth being honest about why. The key + ;; is only honoured by a jolt that reads it, and the runtime that reads it + ;; (#804) first shipped in v0.8.0, far above this floor. So every jolt that + ;; can enforce the floor already clears it, and every jolt old enough to have + ;; the date bug ignores the key the way it ignores any key it does not know. + ;; Until v0.8.0 shipped on 2026-09-01 the sharper form was "no released jolt + ;; reads the key at all"; that is no longer true, and the conclusion is + ;; unchanged. The declaration is forward protection, in place for the next + ;; break, which is the whole reason upstream shipped the key before needing + ;; it rather than after. + ;; + ;; Think twice before RAISING this, because a floor is enforced on consumers, + ;; not just here. A project that declares no floor of its own still gets + ;; refused at resolve-project when a dependency declares one it cannot meet, + ;; and the message names the dependency (:jolt/required-by). Measured against + ;; a :local/root dep declaring "0.9.0": the consumer is stopped before any of + ;; its code runs. So whatever glitter declares becomes a hard stop for + ;; glitter-gl, glitter-uikit and anyone else downstream, over a constraint + ;; they never chose. Raise it only for something glitter genuinely needs, and + ;; only to a version that is actually released. + ;; + ;; Deliberately NOT raised to 0.7.29 for jolt #802's ffi/write argument-order + ;; break: glitter has no ffi/write call site and no [:array ...] layout + ;; descriptor, so that break cannot reach it. Raising the floor for a change + ;; glitter does not use would refuse working runtimes for nothing, and a + ;; library's floor propagates to every consumer. + ;; + ;; And a correction worth keeping, because jolt's own changelog invites the + ;; opposite conclusion by using ffi/write as this key's motivating example: + ;; NO floor value protects an ffi/write consumer. Not "0.7.29", not "0.8.0", + ;; none. The populations are disjoint by construction, and that is provable + ;; rather than merely likely: #804 (which added the code that READS this key) + ;; has #802's merge commit as its SOLE parent, ahead by exactly one, so no + ;; jolt exists that reads the key and still carries the old write order. A + ;; runtime with the old order predates the reader and skips the key. Measured + ;; elsewhere: released v0.7.29 against a project declaring "0.8.0" ran 19 of + ;; 37 tests, dropped two namespaces silently, and exited clean. The key + ;; guards breaks from #804 FORWARD, and nothing before it. + :jolt/min-version "0.7.24" + ;; jolt-lang/time provides the java.time.* surface as a pure-Clojure ;; host shim, with juxt/tick (pulled transitively via jolt-lang/time's ;; own deps.edn) as the idiomatic date/time API on top — used by @@ -10,10 +62,20 @@ ;; just JVM Clojure) as of when they were pinned — re-verify live ;; before bumping either. ;; - ;; The time SHA is v0.0.7, and it is a FLOOR, not just a snapshot: it is - ;; the release that made (t/today) answer the machine's zone instead of - ;; UTC. Pinning anything older silently puts flights.clj's departure - ;; field back on the UTC date. See docs/guide/nexus.md. + ;; The time SHA is a FLOOR, not just a snapshot, and it now sits two commits + ;; PAST the v0.0.7 tag rather than on it. Two separate reasons, both of which + ;; break silently if the pin moves back: + ;; + ;; v0.0.7 itself is what made (t/today) answer the machine's zone instead of + ;; UTC. Pinning older puts flights.clj's departure field back on the UTC date. + ;; + ;; 33c5e98 is jolt-lang/time#11, which declares the java.time classes the + ;; library provides (jolt #813 / RFC 0014). Without it, a jolt carrying #813 + ;; cannot resolve java.time.ZoneId at all: `jolt flights` dies, and + ;; glitter.nexus.action-log-test is silently dropped from the suite, 29 tests + ;; down to 26 with a clean exit. It is untagged because no release carries it + ;; yet, so this pin is ahead of v0.0.7 deliberately. Move it to the tag once + ;; one ships. See docs/guide/nexus.md. ;; ;; The two use different Maven-coordinate conventions on purpose: ;; io.github.jolt-lang/time is a normal git-coordinate dep, but @@ -26,7 +88,7 @@ :git/sha "b6b12097ba973141fe87c1b4d6a27ddcce5d5929"} io.github.jolt-lang/time {:git/url "https://github.com/jolt-lang/time" - :git/sha "b11798fb597a0b685c960056e03f1bd2fee229c2"} + :git/sha "33c5e9831c046cfcbe68bc0c9c33e92568eaebb1"} org.clojure/tools.logging {:git/url "https://github.com/jolt-lang/logging" :git/sha "ec6747e55583118d881faf4ab41e68d7e587ce4c"}} diff --git a/docs/guide/examples.md b/docs/guide/examples.md index 6b61c2c..f04ac3d 100644 --- a/docs/guide/examples.md +++ b/docs/guide/examples.md @@ -173,9 +173,11 @@ invisible to something: 4. **If it's a smoke**, add it to `bb.edn`'s `smokes` list: otherwise `bb smokes` won't run it and CI-by-hand won't catch a regression in it. -A smoke must exit non-zero on failure. Do **not** gate it on `jolt `: -a `deps.edn` `:tasks` entry doesn't propagate its child's exit status, so -`jolt smoke` prints failures and still exits 0. Use `jolt -M:` or a +A smoke must exit non-zero on failure. Do **not** gate it on `jolt `. +On jolt before v0.7.28 a `deps.edn` `:tasks` entry didn't propagate its +child's exit status, so `jolt smoke` printed failures and still exited 0. That +is fixed now, but glitter's floor is v0.7.24, so the task form still can't be +trusted across every version this project supports. Use `jolt -M:` or a `bb.edn` task. See [`testing-and-tasks.md`](testing-and-tasks.md) for the full rationale and [`CONTRIBUTING.md`](https://github.com/jlt-commons/glitter/blob/main/CONTRIBUTING.md) for the invariant list. diff --git a/docs/guide/nexus.md b/docs/guide/nexus.md index 9a8d7cc..55419e6 100644 --- a/docs/guide/nexus.md +++ b/docs/guide/nexus.md @@ -583,3 +583,46 @@ v0.7.24 carries both: (t/today), jolt v0.7.23-12 (#712, no memo) ~1.15 ms (t/today), jolt v0.7.24 (#712 + #716) ~13.6 us ``` + +### The floor is declared, not just written down + +`deps.edn` carries `:jolt/min-version "0.7.24"`, so the floor above is +machine-readable and not only prose. The key comes from +[jolt#804](https://github.com/jolt-lang/jolt/pull/804): a runtime below the +declared floor refuses to load the project instead of running it, and the +floor is read from every dependency's `deps.edn` as well as the project's. A +library is the natural place to declare one, because it knows what its +bindings need and the app pulling it in does not. + +Be clear about what this buys today, though, because it is less than it +sounds. The key is honoured only by a jolt that reads it, and the runtime +that reads it first shipped in v0.8.0, which sits far above 0.7.24, so every +jolt able to enforce the floor already clears it, and every jolt old enough to +carry the UTC-date bug ignores the key the way it ignores any key it does not +recognise. Nothing +changes for an older Jolt: `jolt flights` still runs there and still defaults +its departure field to the UTC date. + +So the declaration is forward protection rather than a guard that fires now. +That is the same reason upstream shipped the key before it was needed instead +of at the next break, and it is why the value here is the floor glitter +actually has rather than the newest version around. + +One limit of the mechanism is worth stating, because jolt's own changelog +points the other way by using `ffi/write`'s moved argument order as the +motivating example for the key. **No floor value protects an `ffi/write` +consumer.** The code that reads `:jolt/min-version` arrived in +[#804](https://github.com/jolt-lang/jolt/pull/804), whose sole parent is the +merge commit of [#802](https://github.com/jolt-lang/jolt/pull/802), the change +that flipped the argument order. One commit, no siblings. So the two +populations are disjoint by construction: no jolt reads the key and still +carries the old order, and any runtime that does carry it predates the reader +and skips the key in silence. A sibling project measured it: released v0.7.29 against a +project declaring `"0.8.0"` ran 19 of 37 tests, dropped two namespaces +silently, and exited clean. + +That does not weaken the floor above, because the TZ-restore boundary is the +case the mechanism does handle: runtimes on both sides of v0.7.24 can read the +key. It only means a floor cannot be used to guard a break older than the +reader, which is worth knowing before reaching for one to solve a +write-order-shaped problem. diff --git a/docs/guide/testing-and-tasks.md b/docs/guide/testing-and-tasks.md index b72ecea..759bffa 100644 --- a/docs/guide/testing-and-tasks.md +++ b/docs/guide/testing-and-tasks.md @@ -119,12 +119,21 @@ hanging, no manual window-close needed to run these in CI. ## The exit-code trap: `jolt ` vs `jolt -M:` -**Verified against jolt v0.6.3**: a deps.edn `:tasks` entry does not +**Verified against jolt v0.6.3**: a deps.edn `:tasks` entry did not propagate its child process's exit status. `jolt test` (the task form) -prints failures to stdout and still exits 0; `jolt -M:test` (the alias -form) correctly exits non-zero. This isn't a glitter-specific quirk to -work around; it's how the `:tasks` wrapper behaves, and it applies to -every task in `deps.edn`, not just `test`. +printed failures to stdout and still exited 0; `jolt -M:test` (the alias +form) correctly exited non-zero. This was never a glitter-specific quirk; +it was how the `:tasks` wrapper behaved, for every task in `deps.edn`. + +**Jolt fixed it in v0.7.28**, under "A failing `:tasks` shell command +exited 0". Re-measured on v0.7.29-25-gd4e92a43 with a task whose command +exits 7: both `jolt -M:fail` and `jolt fail` answer 7. + +The alias form is still the right thing for a gate, for a narrower reason +than the original one. glitter declares `:jolt/min-version "0.7.24"`, and +the fix only arrives at v0.7.28, so anyone running a supported jolt in +between still gets a task form that swallows the failure. The alias form +is correct across the whole supported range, which is what a gate needs. **Always use `-M:` (or a `bb.edn` task, which already does this) to gate a build.** The task shorthand (`jolt test`, `jolt keyed`, ...) is fine