a73x

3bc953e2

feat: the upgrade version rule — strictly newer or refused

a73x   2026-08-26 15:01

Commit message
feat: the upgrade version rule — strictly newer or refused

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

build.zig
Old New
@@ -193,6 +193,11 @@ const mod_table = [_]ModSpec{
193 // file and later the CLI all have to agree on — protocol and nothing 193 // file and later the CLI all have to agree on — protocol and nothing
194 // else, so its tests need no hub, no daemon and no socket. 194 // else, so its tests need no hub, no daemon and no socket.
195 .{ .name = "wall", .path = "src/wall.zig", .layer = 1, .imports = &.{"protocol"}, .test_imports = &.{"testtmp"} }, 195 .{ .name = "wall", .path = "src/wall.zig", .layer = 1, .imports = &.{"protocol"}, .test_imports = &.{"testtmp"} },
196 // The upgrade vocabulary: the version skew rule and the manifest an
197 // exec-ing daemon leaves for its replacement. Protocol only — the
198 // manifest is a stranger's format (length-prefixed sections, unknown
199 // tags skipped), so it cannot depend on server types.
200 .{ .name = "upgrade", .path = "src/upgrade.zig", .layer = 1 },
196 // Shell integration: the OSC 133 mark scripts and what a spawn must add 201 // Shell integration: the OSC 133 mark scripts and what a spawn must add
197 // to hand them to a shell. Near-leaf on purpose — it writes files and 202 // to hand them to a shell. Near-leaf on purpose — it writes files and
198 // reads the environment, and knows nothing of ptys, servers or the 203 // reads the environment, and knows nothing of ptys, servers or the
@@ -542,7 +547,7 @@ fn docGate(b: *std.Build, target: std.Build.ResolvedTarget, check_step: *std.Bui
542 /// parsers — a test that is never built is not a test (decisions.md). 547 /// parsers — a test that is never built is not a test (decisions.md).
543 const test_order = [_][]const u8{ 548 const test_order = [_][]const u8{
544 "script", "select", "protocol", "client_core", "interact", "engine", "pty", 549 "script", "select", "protocol", "client_core", "interact", "engine", "pty",
545 "delta", "cmd", "wall", "shellint", "replica", "keymap", "webhub", 550 "delta", "cmd", "wall", "upgrade", "shellint", "replica", "keymap", "webhub",
546 "wallview", "sockpath", "muxa", "server", "client", "proxy", "mux", 551 "wallview", "sockpath", "muxa", "server", "client", "proxy", "mux",
547 "quic", "quic_server", "exe", "testtmp", "quic_client", "predict", "rawmode", 552 "quic", "quic_server", "exe", "testtmp", "quic_client", "predict", "rawmode",
548 "delaypipe", "xdg", "spawn", "handoff", "paint", "layout", "render", 553 "delaypipe", "xdg", "spawn", "handoff", "paint", "layout", "render",
src/upgrade.zig
Old New
@@ -0,0 +1,37 @@
1 //! The upgrade vocabulary: the version rule and the manifest an exec-ing
2 //! daemon leaves for its replacement. The manifest crosses inside one
3 //! process (a memfd surviving execve), yet it is encoded as if for a
4 //! stranger — length-prefixed sections, unknown tags skipped — because the
5 //! reader IS a stranger: a newer binary, or on rollback an older one, and
6 //! neither may be held to this build's struct layout.
7
8 const std = @import("std");
9
10 /// Skew rule: an upgrade candidate must outrank the running daemon.
11 /// SemanticVersion because our releases are `0.0.1-13`-shaped: the `-13`
12 /// is the prerelease field and numeric identifiers order numerically,
13 /// which is exactly the order the release sequence needs.
14 pub fn strictlyNewer(candidate: []const u8, mine: []const u8) error{BadVersion}!bool {
15 const c = std.SemanticVersion.parse(candidate) catch return error.BadVersion;
16 const m = std.SemanticVersion.parse(mine) catch return error.BadVersion;
17 return c.order(m) == .gt;
18 }
19
20 test "strictlyNewer: the next prerelease number is newer (0.0.1-14 over 0.0.1-13)" {
21 try std.testing.expect(try strictlyNewer("0.0.1-14", "0.0.1-13"));
22 }
23 test "strictlyNewer: prerelease numbers order numerically, not lexically (0.0.1-100 over 0.0.1-99)" {
24 try std.testing.expect(try strictlyNewer("0.0.1-100", "0.0.1-99"));
25 }
26 test "strictlyNewer: the same version is not newer" {
27 try std.testing.expect(!try strictlyNewer("0.0.1-13", "0.0.1-13"));
28 }
29 test "strictlyNewer: a downgrade is not newer" {
30 try std.testing.expect(!try strictlyNewer("0.0.1-12", "0.0.1-13"));
31 }
32 test "strictlyNewer: a release outranks its own prereleases" {
33 try std.testing.expect(try strictlyNewer("0.0.1", "0.0.1-13"));
34 }
35 test "strictlyNewer: garbage is an error, not a verdict" {
36 try std.testing.expectError(error.BadVersion, strictlyNewer("not-a-version", "0.0.1-13"));
37 }