node-engine client: a candidate's composition key is its own, over mesh-sdk keys; the client no longer imports go/delivery (ADR 0634 §4; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 15:35:17 +01:00
parent 71c8b9f964
commit 5f8b8edd20
2 changed files with 58 additions and 5 deletions
+38 -3
View File
@@ -10,10 +10,11 @@ import (
"errors"
"fmt"
"regexp"
"strings"
"time"
seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/keys"
)
const (
@@ -56,7 +57,7 @@ var ErrUnknownVersion = errors.New("node-engine: unknown compose-candidate answe
var nameOK = regexp.MustCompile(`^[-/_=a-zA-Z0-9]+$`)
func checkAsk(a seat.ComposeCandidateInput) error {
if _, _, err := delivery.ParseCompositionKey(a.Candidate); err != nil {
if _, _, err := ParseCompositionKey(a.Candidate); err != nil {
return fmt.Errorf("node-engine: a compose-candidate ask names no candidate: %w", err)
}
return nil
@@ -89,7 +90,7 @@ func CheckAnswer(a seat.ComposeCandidateAnswer) error {
if a.Node == "" || !nameOK.MatchString(a.Node) {
return fmt.Errorf("node-engine: the answer's node name %q is empty or holds a dot, a wildcard, whitespace or a character a key refuses", a.Node)
}
if _, _, err := delivery.ParseCompositionKey(a.Candidate); err != nil {
if _, _, err := ParseCompositionKey(a.Candidate); err != nil {
return err
}
if a.Took < 0 {
@@ -181,3 +182,37 @@ func DecodeAnswer(data []byte) (seat.ComposeCandidateAnswer, error) {
}
return a, CheckAnswer(a)
}
var heads12 = regexp.MustCompile(`^[0-9a-f]{12}$`)
// CompositionKey is the key of a candidate the node is asked to compose: the group's check at its heads, `<escaped
// group>.<heads hash>`, the group escaped as a key part and the heads hash 12 lowercase hex digits (the checker's verdict
// key of the same check).
func CompositionKey(group, headsHash string) (string, error) {
if group == "" {
return "", errors.New("node-engine: the group name is empty")
}
if !heads12.MatchString(headsHash) {
return "", fmt.Errorf("node-engine: %q is not a heads hash, 12 lowercase hex digits", headsHash)
}
return keys.EscapeName(group) + "." + headsHash, nil
}
// ParseCompositionKey is the inverse of CompositionKey; it refuses a key that is not one, naming it.
func ParseCompositionKey(key string) (group, headsHash string, err error) {
i := strings.LastIndex(key, ".")
if i < 0 {
return "", "", fmt.Errorf("node-engine: %q is not a composition key, <group>.<heads hash>", key)
}
part, headsHash := key[:i], key[i+1:]
if !heads12.MatchString(headsHash) {
return "", "", fmt.Errorf("node-engine: %q is not a composition key: %q is not a heads hash, 12 lowercase hex digits", key, headsHash)
}
if part == "" || strings.Contains(part, ".") {
return "", "", fmt.Errorf("node-engine: %q is not a composition key: the group part is empty or holds a dot", key)
}
if group, err = keys.UnescapeName(part); err != nil {
return "", "", fmt.Errorf("node-engine: %q is not a composition key: %w", key, err)
}
return group, headsHash, nil
}
+20 -2
View File
@@ -8,13 +8,12 @@ import (
"testing"
seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The verb compose-candidate's ask and answer (ADR 0593 §7, §9): each round-trips, and a reader refuses what it could
// not take, naming it.
func TestComposeCandidateRoundTripsAndRefuses(t *testing.T) {
key, err := delivery.CompositionKey("mesh-sdk#7", "0123456789ab")
key, err := CompositionKey("mesh-sdk#7", "0123456789ab")
if err != nil {
t.Fatal(err)
}
@@ -95,3 +94,22 @@ func TestComposeCandidateRoundTripsAndRefuses(t *testing.T) {
}
}
}
// A composition key round-trips its group and heads hash, and refuses what is not one.
func TestCompositionKeyRoundTripsAndRefuses(t *testing.T) {
key, err := CompositionKey("fix/1.2", "0123456789ab")
if err != nil {
t.Fatal(err)
}
if g, h, err := ParseCompositionKey(key); err != nil || g != "fix/1.2" || h != "0123456789ab" {
t.Fatalf("round trip: %q %q %v", g, h, err)
}
for _, bad := range []string{"", "nogroup", "g.0123456789AB", "g.short", ".0123456789ab", "a.b.0123456789ab"} {
if _, _, err := ParseCompositionKey(bad); err == nil {
t.Errorf("%q was taken for a composition key", bad)
}
}
if _, err := CompositionKey("", "0123456789ab"); err == nil {
t.Error("a key with no group was made")
}
}