Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <task>`**: 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:<alias>` or a
`bb.edn` task.
- **Never gate CI on `jolt <task>`**: 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:<alias>` 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

Expand Down
20 changes: 15 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand All @@ -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.

Expand Down Expand Up @@ -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 <task>`.** 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:<alias>`, or
a `bb.edn` task; those already do it correctly.
1. **Never gate CI on `jolt <task>`.** 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:<alias>` 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).
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,13 @@ smokes, each of which mounts a real window and exits non-zero on failure.
</details>

**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`

Expand Down
72 changes: 67 additions & 5 deletions deps.edn
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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"}}
Expand Down
8 changes: 5 additions & 3 deletions docs/guide/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <task>`:
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:<alias>` or a
A smoke must exit non-zero on failure. Do **not** gate it on `jolt <task>`.
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:<alias>` 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.
43 changes: 43 additions & 0 deletions docs/guide/nexus.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
19 changes: 14 additions & 5 deletions docs/guide/testing-and-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,21 @@ hanging, no manual window-close needed to run these in CI.

## The exit-code trap: `jolt <task>` vs `jolt -M:<alias>`

**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:<alias>` (or a `bb.edn` task, which already does this) to
gate a build.** The task shorthand (`jolt test`, `jolt keyed`, ...) is fine
Expand Down