hub v0.45.0: floor-UI separation + effective-floor source + per-box MinAgent conditional floor

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-11 15:33:14 +02:00
parent 37e60b46d2
commit bbecf0592e
15 changed files with 730 additions and 44 deletions
+51
View File
@@ -0,0 +1,51 @@
// Package semver is THE hub's controller/agent version comparator (house rule: one comparator).
// Extracted from web.compareVersions (v0.45.0) so the store's managed-floor MinAgent gate can share
// it without an import cycle (store/api don't import web). web.compareVersions now delegates here.
package semver
import (
"strconv"
"strings"
)
// Compare returns >0 if a>b, 0 if equal, <0 if a<b. Inputs are bare "X.Y.Z" (a leading "v" is
// tolerated). Returns 0 on any parse error — callers that must not act on a malformed version check
// for it explicitly before comparing.
func Compare(a, b string) int {
a = strings.TrimPrefix(a, "v")
b = strings.TrimPrefix(b, "v")
ap := strings.SplitN(a, ".", 3)
bp := strings.SplitN(b, ".", 3)
if len(ap) != 3 || len(bp) != 3 {
return 0
}
for i := 0; i < 3; i++ {
ai, e1 := strconv.Atoi(ap[i])
bi, e2 := strconv.Atoi(bp[i])
if e1 != nil || e2 != nil {
return 0
}
if ai != bi {
if ai < bi {
return -1
}
return 1
}
}
return 0
}
// Valid reports whether s is a bare X.Y.Z (v-prefix tolerated) — the shape Compare acts on.
func Valid(s string) bool {
s = strings.TrimPrefix(s, "v")
p := strings.SplitN(s, ".", 3)
if len(p) != 3 {
return false
}
for _, part := range p {
if _, err := strconv.Atoi(part); err != nil {
return false
}
}
return true
}
+38
View File
@@ -0,0 +1,38 @@
package semver
import "testing"
func TestCompare(t *testing.T) {
cases := []struct {
a, b string
want int
}{
{"0.81.0", "0.82.0", -1},
{"0.82.0", "0.81.0", 1},
{"0.81.0", "0.81.0", 0},
{"v0.81.0", "0.81.0", 0}, // v-prefix tolerated
{"0.100.0", "0.81.0", 1}, // numeric, not lexicographic
{"1.0.0", "0.99.9", 1}, // major
{"1.2.10", "1.2.3", 1}, // patch numeric
{"garbage", "0.81.0", 0}, // parse error → 0 (callers guard with Valid)
{"0.81", "0.81.0", 0}, // too few parts → 0
}
for _, c := range cases {
if got := Compare(c.a, c.b); got != c.want {
t.Errorf("Compare(%q,%q) = %d, want %d", c.a, c.b, got, c.want)
}
}
}
func TestValid(t *testing.T) {
for _, ok := range []string{"0.81.0", "v0.82.0", "10.20.30"} {
if !Valid(ok) {
t.Errorf("Valid(%q) = false, want true", ok)
}
}
for _, bad := range []string{"", "0.81", "0.81.0-rc1", "a.b.c", "dev"} {
if Valid(bad) {
t.Errorf("Valid(%q) = true, want false", bad)
}
}
}