From 02a4ef349764f90f671868d005fdafe8017b3bab Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 20:33:08 +0100 Subject: [PATCH] CT13 (reserved's answers and its client): reserved answers released or error, and meshbuildsclient asks it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../go/clients/meshbuildsclient/client.go | 53 +++++++++++++++++ .../clients/meshbuildsclient/client_test.go | 57 +++++++++++++++++++ contracts/go/clients/meshbuildsclient/go.mod | 5 ++ contracts/go/seats/meshbuilds/contract.go | 10 +++- .../go/seats/meshbuilds/contract_test.go | 4 ++ contracts/module.json | 9 +++ .../seats/mesh-builds/fixtures/verbs.json | 22 +++++++ contracts/seats/mesh-builds/seat.json | 14 +++-- contracts/ts/seats/meshbuilds/index.d.ts | 10 +++- 9 files changed, 173 insertions(+), 11 deletions(-) create mode 100644 contracts/go/clients/meshbuildsclient/client.go create mode 100644 contracts/go/clients/meshbuildsclient/client_test.go create mode 100644 contracts/go/clients/meshbuildsclient/go.mod diff --git a/contracts/go/clients/meshbuildsclient/client.go b/contracts/go/clients/meshbuildsclient/client.go new file mode 100644 index 00000000..e1f7c778 --- /dev/null +++ b/contracts/go/clients/meshbuildsclient/client.go @@ -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:.`, 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 +} diff --git a/contracts/go/clients/meshbuildsclient/client_test.go b/contracts/go/clients/meshbuildsclient/client_test.go new file mode 100644 index 00000000..86e4b10c --- /dev/null +++ b/contracts/go/clients/meshbuildsclient/client_test.go @@ -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) + } + } + } +} diff --git a/contracts/go/clients/meshbuildsclient/go.mod b/contracts/go/clients/meshbuildsclient/go.mod new file mode 100644 index 00000000..f28889ab --- /dev/null +++ b/contracts/go/clients/meshbuildsclient/go.mod @@ -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 diff --git a/contracts/go/seats/meshbuilds/contract.go b/contracts/go/seats/meshbuilds/contract.go index f95ae661..298fe378 100644 --- a/contracts/go/seats/meshbuilds/contract.go +++ b/contracts/go/seats/meshbuilds/contract.go @@ -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. diff --git a/contracts/go/seats/meshbuilds/contract_test.go b/contracts/go/seats/meshbuilds/contract_test.go index 65fb7dfc..16da9fcc 100644 --- a/contracts/go/seats/meshbuilds/contract_test.go +++ b/contracts/go/seats/meshbuilds/contract_test.go @@ -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)) diff --git a/contracts/module.json b/contracts/module.json index bedc8454..b024f0d0 100644 --- a/contracts/module.json +++ b/contracts/module.json @@ -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", diff --git a/contracts/seats/mesh-builds/fixtures/verbs.json b/contracts/seats/mesh-builds/fixtures/verbs.json index b8c50afb..2ebcc767 100644 --- a/contracts/seats/mesh-builds/fixtures/verbs.json +++ b/contracts/seats/mesh-builds/fixtures/verbs.json @@ -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" + } } ] diff --git a/contracts/seats/mesh-builds/seat.json b/contracts/seats/mesh-builds/seat.json index 29e6a5c3..fca0bacf 100644 --- a/contracts/seats/mesh-builds/seat.json +++ b/contracts/seats/mesh-builds/seat.json @@ -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)" } } ], diff --git a/contracts/ts/seats/meshbuilds/index.d.ts b/contracts/ts/seats/meshbuilds/index.d.ts index 9732e7cd..f02549ab 100644 --- a/contracts/ts/seats/meshbuilds/index.d.ts +++ b/contracts/ts/seats/meshbuilds/index.d.ts @@ -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. */