CT13 (reserved's answers and its client): reserved answers released or error, and meshbuildsclient asks it

reserved answers module, version, artifacts and released (a boolean), or error alone; module, version and artifacts are
read as required only without error. contracts/go/clients/meshbuildsclient (artifact go-meshbuildsclient) has Reserved,
which answers the reservation or release, a Refusal for the error and an error for an answer that is not one.

Builds: design 47 G §679, §582, §586, §650

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 20:33:08 +01:00
parent 7fca7f0ac8
commit 02a4ef3497
9 changed files with 173 additions and 11 deletions
@@ -0,0 +1,53 @@
// Package meshbuildsclient is the mesh-builds seat's client (novox/hq ADR 0573 §1, ADR 0627 §1; design 47 G §586): the
// registry holders' call of `reserved`, its input and each of its answers, written once. The call is the caller's own (a
// bundle's stdio.Ask, a process's call of a seat verb), so the client holds no transport.
package meshbuildsclient
import (
"encoding/json"
"fmt"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
)
// Address is the seat verb asked, as a manifest's `invokes` names it.
const Address = "seat:" + seat.Seat + "." + seat.VerbReserved
// Call asks one verb of the seat, `seat:<seat>.<verb>`, with a body, and answers its JSON.
type Call func(address string, body any) (json.RawMessage, error)
// Refusal is the error answer of `reserved`: a name no entry carries, or a version neither reserved nor released, in the
// builder's words.
type Refusal struct{ Words string }
func (r Refusal) Error() string { return Address + ": " + r.Words }
// Reservation is a reserved or a released version with each artifact and its digest (design 47 G §679).
type Reservation struct {
Module string
Version string
Artifacts []seat.ReservedAnswerArtifactsItem
// Released is true when the catalog holds the version released and Artifacts are the digests its release recorded.
Released bool
}
// Reserved asks the seat for a module's, or a package's, reserved version. It answers the reservation or release, a
// Refusal for the answer `error`, and an error for an answer that cannot be read or lacks a module or a version without
// saying error: an answer that cannot be read is never a yes.
func Reserved(call Call, in seat.ReservedInput) (Reservation, error) {
raw, err := call(Address, in)
if err != nil {
return Reservation{}, fmt.Errorf("%s did not answer: %w", Address, err)
}
var a seat.ReservedAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return Reservation{}, fmt.Errorf("%s answered what cannot be read: %s", Address, raw)
}
if a.Error != "" {
return Reservation{}, Refusal{Words: a.Error}
}
if a.Module == "" || a.Version == "" {
return Reservation{}, fmt.Errorf("%s answered no reservation: %s", Address, raw)
}
return Reservation{Module: a.Module, Version: a.Version, Artifacts: a.Artifacts, Released: a.Released}, nil
}
@@ -0,0 +1,57 @@
package meshbuildsclient
import (
"encoding/json"
"errors"
"strings"
"testing"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
)
func answers(raw string, err error) Call {
return func(address string, body any) (json.RawMessage, error) {
if address != "seat:mesh-builds.reserved" {
return nil, errors.New("asked " + address)
}
return json.RawMessage(raw), err
}
}
// TestReservedReadsEachAnswer is design 47 G §586's check: the client's input and each answer read (§679's three).
func TestReservedReadsEachAnswer(t *testing.T) {
for _, tc := range []struct {
name string
call Call
want Reservation
refusal string
failedOn string
}{
{name: "a reservation", call: answers(`{"module":"m","version":"0.1.2","artifacts":[{"artifact":"m:go-m","digest":"sha256:aa"}]}`, nil),
want: Reservation{Module: "m", Version: "0.1.2", Artifacts: []seat.ReservedAnswerArtifactsItem{{Artifact: "m:go-m", Digest: "sha256:aa"}}}},
{name: "a release", call: answers(`{"module":"m","version":"0.1.1","artifacts":[{"artifact":"m:go-m","digest":"sha256:bb"}],"released":true}`, nil),
want: Reservation{Module: "m", Version: "0.1.1", Artifacts: []seat.ReservedAnswerArtifactsItem{{Artifact: "m:go-m", Digest: "sha256:bb"}}, Released: true}},
{name: "an error alone", call: answers(`{"error":"no entry carries the name x"}`, nil), refusal: "no entry carries the name x"},
{name: "no module and no error", call: answers(`{}`, nil), failedOn: "answered no reservation"},
{name: "a call that fails", call: answers("", errors.New("no responders")), failedOn: "did not answer"},
{name: "what cannot be read", call: answers(`[1]`, nil), failedOn: "cannot be read"},
} {
got, err := Reserved(tc.call, seat.ReservedInput{Name: "x", Version: "0.1.1"})
var r Refusal
switch {
case tc.refusal != "":
if !errors.As(err, &r) || r.Words != tc.refusal {
t.Errorf("%s: %v, want a refusal %q", tc.name, err, tc.refusal)
}
case tc.failedOn != "":
if err == nil || !strings.Contains(err.Error(), tc.failedOn) {
t.Errorf("%s: %v, want an error saying %q", tc.name, err, tc.failedOn)
}
default:
if err != nil || got.Module != tc.want.Module || got.Version != tc.want.Version || got.Released != tc.want.Released ||
len(got.Artifacts) != 1 || got.Artifacts[0] != tc.want.Artifacts[0] {
t.Errorf("%s: %+v %v, want %+v", tc.name, got, err, tc.want)
}
}
}
}
@@ -0,0 +1,5 @@
module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshbuildsclient
go 1.26.0
require git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds v0.0.0-00010101000000-000000000000
+7 -3
View File
@@ -117,11 +117,15 @@ type ReservedAnswerArtifactsItem struct {
// ReservedAnswer is what reserved answers.
type ReservedAnswer struct {
Artifacts []ReservedAnswerArtifactsItem `json:"artifacts"`
Artifacts []ReservedAnswerArtifactsItem `json:"artifacts,omitempty"`
// a name no entry carries, or a version neither reserved nor released, naming which; set alone (design 47 G 582, 679)
Error string `json:"error,omitempty"`
// the module
Module string `json:"module"`
Module string `json:"module,omitempty"`
// true when the version is released: the catalog holds it, and artifacts are the ones its release recorded with their digests; false when it is reserved (design 47 G 582, 679)
Released bool `json:"released,omitempty"`
// the reserved version, MAJOR.MINOR.PATCH
Version string `json:"version"`
Version string `json:"version,omitempty"`
}
// PackagePublishedEvent is the body of the event package-published.
@@ -27,6 +27,10 @@ func TestFixtures(t *testing.T) {
decode: func(b []byte) (any, error) { var v ReservedInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "reserved answers the version and each artifact's digest", subject: "", given: "{\"artifacts\":[{\"artifact\":\"mesh-sdk:go\",\"digest\":\"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"},{\"artifact\":\"mesh-sdk:ts\"}],\"module\":\"mesh-sdk\",\"version\":\"0.1.15\"}", wire: "{\"artifacts\":[{\"artifact\":\"mesh-sdk:go\",\"digest\":\"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"},{\"artifact\":\"mesh-sdk:ts\"}],\"module\":\"mesh-sdk\",\"version\":\"0.1.15\"}",
decode: func(b []byte) (any, error) { var v ReservedAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "reserved answers a released version with the digests the release recorded", subject: "", given: "{\"artifacts\":[{\"artifact\":\"mesh-tools:go-meshtools\",\"digest\":\"sha256:aa\"}],\"module\":\"mesh-tools\",\"released\":true,\"version\":\"0.1.4\"}", wire: "{\"artifacts\":[{\"artifact\":\"mesh-tools:go-meshtools\",\"digest\":\"sha256:aa\"}],\"module\":\"mesh-tools\",\"released\":true,\"version\":\"0.1.4\"}",
decode: func(b []byte) (any, error) { var v ReservedAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "reserved answers an error alone, naming the name or the version", subject: "", given: "{\"error\":\"no entry carries the name example.org/none\"}", wire: "{\"error\":\"no entry carries the name example.org/none\"}",
decode: func(b []byte) (any, error) { var v ReservedAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
}
for _, c := range cases {
v, err := c.decode([]byte(c.given))
+9
View File
@@ -315,6 +315,15 @@
],
"series": "0.1"
},
{
"name": "go-meshbuildsclient",
"kind": "package",
"language": "go",
"paths": [
"go/clients/meshbuildsclient"
],
"series": "0.1"
},
{
"name": "go-meshbuilds",
"kind": "package",
@@ -30,5 +30,27 @@
}
]
}
},
{
"name": "reserved answers a released version with the digests the release recorded",
"of": "verb:reserved.answer",
"given": {
"module": "mesh-tools",
"version": "0.1.4",
"artifacts": [
{
"artifact": "mesh-tools:go-meshtools",
"digest": "sha256:aa"
}
],
"released": true
}
},
{
"name": "reserved answers an error alone, naming the name or the version",
"of": "verb:reserved.answer",
"given": {
"error": "no entry carries the name example.org/none"
}
}
]
+9 -5
View File
@@ -64,13 +64,17 @@
"artifact"
]
}
},
"released": {
"type": "boolean",
"description": "true when the version is released: the catalog holds it, and artifacts are the ones its release recorded with their digests; false when it is reserved (design 47 G 582, 679)"
},
"error": {
"type": "string",
"description": "a name no entry carries, or a version neither reserved nor released, naming which; set alone (design 47 G 582, 679)"
}
},
"required": [
"module",
"version",
"artifacts"
]
"description": "the reservation or release, or the error; module, version and artifacts are present whenever error is not (design 47 G 679)"
}
}
],
+7 -3
View File
@@ -93,11 +93,15 @@ export interface ReservedAnswerArtifactsItem {
/** is what reserved answers. */
export interface ReservedAnswer {
"artifacts": ReservedAnswerArtifactsItem[];
"artifacts"?: ReservedAnswerArtifactsItem[];
/** a name no entry carries, or a version neither reserved nor released, naming which; set alone (design 47 G 582, 679) */
"error"?: string;
/** the module */
"module": string;
"module"?: string;
/** true when the version is released: the catalog holds it, and artifacts are the ones its release recorded with their digests; false when it is reserved (design 47 G 582, 679) */
"released"?: boolean;
/** the reserved version, MAJOR.MINOR.PATCH */
"version": string;
"version"?: string;
}
/** is the body of the event package-published. */