fix9b2-2: the builder's process answers the seat mesh-builds' verb reserved; the tools bundle no longer serves it

serves beside the process in the manifest (no bus), the catalog half's carriers and seat mesh-catalog.versions used in the process; builder_build stays the bundle's.

Builds: design 47 G A-115, A-127, A-87
This commit is contained in:
2026-11-12 01:30:37 +01:00
parent 6034559dc2
commit a5bb2ba5b5
9 changed files with 153 additions and 56 deletions
@@ -17,7 +17,6 @@ import (
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
@@ -404,18 +403,6 @@ func main() {
}
return nil
}, versions.HolderOf(branches))
carriers := versions.CarriersFrom(versions.EntriesOf(branches))
released := func(_ context.Context, module string) ([]meshcatalog.VersionEntry, error) {
raw, err := stdio.Ask(catalogVersionsVerb, meshcatalog.VersionsInput{Module: module})
if err != nil {
return nil, err
}
var a meshcatalog.VersionsAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("the seat mesh-catalog's versions answered what does not read: %w", err)
}
return a.Versions, nil
}
reserve := func(name, version string) (meshbuilds.ReservedAnswer, error) {
raw, err := stdio.Ask(reservedVerb, meshbuilds.ReservedInput{Name: name, Version: version})
if err != nil {
@@ -427,7 +414,7 @@ func main() {
tools := Tools(r, put, askQueue(stdio.State(build.QueuesState), 25*time.Second), buildAgain, stdio.Log(builds.BuilderLog).Since, time.Now, vr, fr, ofr, reserve)
tools = append(tools, buildTool(&build.BuildReader{Builds: read(stdio.State("builds")), Entry: entryOf(stdio.State("builds")), Asked: read(stdio.State("asked")),
Newest: newestVia(stdio.Ask), Steps: r}))
tools = append(tools, reservedTools(rr, buildAgain, time.Now, meshhandactsclient.Recorder{Ask: stdio.Ask}, carriers, released)...)
tools = append(tools, reservedTools(rr, buildAgain, time.Now, meshhandactsclient.Recorder{Ask: stdio.Ask})...)
qs, as := stdio.State(versions.PublishQueueState), stdio.State(versions.ActedState)
tools = append(tools, republishTools(readerOf(qs), readerOf(as), buildAgain, time.Now, meshhandactsclient.Recorder{Ask: stdio.Ask})...)
ws := stdio.State(build.WaitsState)
@@ -1,7 +1,6 @@
package main
import (
"context"
"errors"
"fmt"
"strings"
@@ -14,31 +13,17 @@ import (
"mesh-delivery-builder/internal/versions"
)
// reservedTools are builder_reserved, builder_reserved_drop and the seat mesh-builds' verb reserved (design 47 G section
// 369), served as tools of the bundle. The drop is a hand act: it is recorded in the hand-act log first, then written to
// reservedTools are builder_reserved and builder_reserved_drop (design 47 G section 369), served as tools of the bundle. The drop is a hand act: it is recorded in the hand-act log first, then written to
// reasks for the builder, which owns the reservations, to answer.
//
// The verb reserved given a name finds the module from the catalog half's view the bundle follows (carriers) and, for a version
// no reservation holds, asks the seat mesh-catalog's verb versions (released); the builder's process serves no call (A-70).
func reservedTools(rr versions.Reader, ask queueAsker, now func() time.Time, acts meshhandactsclient.Recorder, carriers func(context.Context, string) ([]versions.Built, error), released versions.CatalogVersions) []stdio.Tool {
// The seat's verb reserved is the builder's process's (design 47 G A-115, A-127), not the bundle's.
func reservedTools(rr versions.Reader, ask queueAsker, now func() time.Time, acts meshhandactsclient.Recorder) []stdio.Tool {
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) }
return []stdio.Tool{
{Name: versions.ReservedTool, Description: versions.ReservedDescription,
Input: map[string]any{"type": "object", "properties": map[string]any{
"module": map[string]any{"type": "string", "description": "only this module's reservations"}}},
Run: func(args map[string]any) (any, error) { return versions.AnswerReserved(rr, str(args, "module")) }},
{Name: versions.ReservedVerb, Description: "The seat mesh-builds' verb reserved: a module's reserved version with each artifact's digest once built " +
"(design 47 G section 369), for a registry holder deciding whether to publish. Read-only.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"module": map[string]any{"type": "string", "description": "the module; give this or name"},
"name": map[string]any{"type": "string", "description": "a package's or an image's name in its ecosystem, in place of module"},
"version": map[string]any{"type": "string", "description": "the version, when asked for a particular one"}}},
Run: func(args map[string]any) (any, error) {
if name := str(args, "name"); name != "" {
return versions.AnswerReservedByName(context.Background(), rr, carriers, released, name, str(args, "version"))
}
return versions.AnswerReservedVerb(rr, str(args, "module"), str(args, "version"))
}},
{Name: versions.ReservedDropTool, Description: versions.ReservedDropDescription,
Input: map[string]any{"type": "object", "required": []string{"module", "series", "why"}, "properties": map[string]any{
"module": map[string]any{"type": "string", "description": "the module"},
@@ -1,41 +1,27 @@
package main
import (
"context"
"encoding/json"
"testing"
"time"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"mesh-delivery-builder/internal/build"
"mesh-delivery-builder/internal/versions"
)
// Design 47 G §572, §582, A-70: the bundle's verb reserved takes a name and answers from the view it follows and the catalog's
// versions, and builder_versions, asked by a name, calls reserved and answers what it answers.
func TestReservedByNameIsTheBundlesAndBuilderVersionsCallsIt(t *testing.T) {
// Design 47 G §572, §582, A-70: builder_versions, asked by a name, calls reserved and answers what it answers.
func TestBuilderVersionsByNameCallsReserved(t *testing.T) {
none := versions.Reader{Keys: func() ([]string, error) { return nil, nil }, Get: func(string) (json.RawMessage, bool, error) { return nil, false, nil }}
carriers := func(_ context.Context, name string) ([]versions.Built, error) {
if name == "@novox/mesh-sdk" {
return []versions.Built{{Module: "mesh-sdk", Artifact: "ts"}}, nil
}
return nil, nil
}
released := func(_ context.Context, module string) ([]meshcatalog.VersionEntry, error) {
return []meshcatalog.VersionEntry{{Module: module, Version: "0.1.2", Artifacts: []meshcatalog.VersionArtifact{{Artifact: "mesh-sdk:ts", Digest: "sha256:dd"}}}}, nil
}
acts := meshhandactsclient.Recorder{Ask: func(string, any) (json.RawMessage, error) { return json.RawMessage(`{}`), nil }}
byName := map[string]func(map[string]any) (any, error){}
for _, tl := range reservedTools(none, nil, time.Now, acts, carriers, released) {
byName[tl.Name] = tl.Run
}
got, err := byName[versions.ReservedVerb](map[string]any{"name": "@novox/mesh-sdk", "version": "0.1.2"})
want := meshbuilds.ReservedAnswer{Module: "mesh-sdk", Version: "0.1.2", Released: true, Artifacts: []meshbuilds.ReservedAnswerArtifactsItem{{Artifact: "mesh-sdk:ts", Digest: "sha256:dd"}}}
if err != nil || got.(meshbuilds.ReservedAnswer).Released != want.Released || got.(meshbuilds.ReservedAnswer).Artifacts[0] != want.Artifacts[0] {
t.Fatalf("reserved by name = %+v (%v), want %+v", got, err, want)
// The bundle serves no verb reserved any more: the builder's process does (design 47 G A-115, A-127).
acts := meshhandactsclient.Recorder{Ask: func(string, any) (json.RawMessage, error) { return json.RawMessage(`{}`), nil }}
for _, tl := range reservedTools(none, nil, time.Now, acts) {
if tl.Name == "reserved" {
t.Error("the bundle serves the verb reserved, which the builder's process answers")
}
}
var called []string
reserve := func(name, version string) (meshbuilds.ReservedAnswer, error) {
@@ -28,6 +28,7 @@ import (
runner "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
@@ -556,6 +557,16 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7
if v.Toolchains["go"].Version != "go1.27.1" || v.Toolchains["typescript"].Version != "tsc5" {
t.Errorf("toolchains = %+v", v.Toolchains)
}
// A-115: the builder's process answers the seat mesh-builds' verb reserved over the bus; the reservation of a module it never
// built is answered in words (here that the bed's state reserved is not on the bus), never left unanswered.
if conn, err := process.Connect(process.Account{URL: b.srvURL, Module: "mesh-delivery-view", User: "ctl", Password: "pw"}); err != nil {
t.Fatal(err)
} else {
defer conn.Close()
if _, err := conn.Call("seat:mesh-builds.reserved", meshbuilds.ReservedInput{Module: "never-built"}); err == nil || !strings.Contains(err.Error(), "_reserved") {
t.Errorf("reserved of a module never built = %v, want the process's answer naming its state reserved", err)
}
}
// ADR 0618 §1: the done key names the reference forms it is written in.
if len(v.Needs) != 1 || v.Needs[0] != "form:artifact-store" {
t.Errorf("needs = %v, want the first form", v.Needs)
@@ -403,6 +403,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// modules again (design 47 G A-82, §204).
// The runner's build step ask, the builder's one entry point for a candidate (design 47 G A-97).
b.StepEntry = newEntryWaits(branches, half.manifests).entry
// The seat mesh-builds' verb reserved, answered here (A-115, A-127): the reservations, the catalog half's carriers and the seat
// mesh-catalog's versions.
stopReserved, err := calls.AnswerSeat(meshbuilds.Seat, map[string]process.SeatHandler{
meshbuilds.VerbReserved: reservedVerb(ctx, reservedReader(ctx, reserved.r.Store), versions.CarriersFrom(versions.EntriesOf(branches)), catalogReleased(calls.Call))})
if err != nil {
return fmt.Errorf("answering the verb reserved: %w", err)
}
defer stopReserved()
if err := serveSteps(ctx, conn, js, cred, b, logger.Printf); err != nil {
return err
}
@@ -0,0 +1,60 @@
package main
import (
"context"
"encoding/json"
"fmt"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/process"
"mesh-delivery-builder/internal/versions"
)
// The seat mesh-builds' verb reserved is answered by the builder's own process (design 47 G A-115, A-127; A-87: builder_build stays
// its tools bundle's): a module's reserved version with each artifact's digest once built, for a registry holder deciding whether to
// publish. Given a name it finds the module from the catalog half the process follows (carriers) and, for a version no reservation
// holds, asks the seat mesh-catalog's verb versions. The process's manifest declares the verb in `serves` beside its process.
// reservedReader is the state reserved read through its store, as the verb's answers read it.
func reservedReader(ctx context.Context, store versions.Store) versions.Reader {
return versions.Reader{
Keys: func() ([]string, error) { return store.KeysOf(ctx, versions.AllFilter) },
Get: func(key string) (json.RawMessage, bool, error) {
v, _, found, err := store.Get(ctx, key)
return v, found, err
}}
}
// reservedVerb is the handler of the verb reserved.
func reservedVerb(ctx context.Context, rr versions.Reader, carriers func(context.Context, string) ([]versions.Built, error), released versions.CatalogVersions) process.SeatHandler {
return func(_ string, body json.RawMessage) (any, error) {
var in meshbuilds.ReservedInput
if len(body) > 0 {
if err := json.Unmarshal(body, &in); err != nil {
return nil, fmt.Errorf("the arguments do not fit the verb reserved: %w", err)
}
}
if name := strings.TrimSpace(in.Name); name != "" {
return versions.AnswerReservedByName(ctx, rr, carriers, released, name, in.Version)
}
return versions.AnswerReservedVerb(rr, in.Module, in.Version)
}
}
// catalogReleased are a module's released versions, asked of the seat mesh-catalog's verb versions through call.
func catalogReleased(call toolCaller) versions.CatalogVersions {
return func(_ context.Context, module string) ([]meshcatalog.VersionEntry, error) {
raw, err := call("seat:"+meshcatalog.Seat+".versions", meshcatalog.VersionsInput{Module: module})
if err != nil {
return nil, err
}
var a meshcatalog.VersionsAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("the seat mesh-catalog's versions answered what does not read: %w", err)
}
return a.Versions, nil
}
}
@@ -0,0 +1,45 @@
package main
import (
"context"
"encoding/json"
"testing"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"mesh-delivery-builder/internal/versions"
)
// Design 47 G A-115, A-127: the builder's process answers the verb reserved, given a name, from the catalog half's carriers and the
// seat mesh-catalog's versions, and given a module from its reservations.
func TestTheProcessAnswersReserved(t *testing.T) {
none := versions.Reader{Keys: func() ([]string, error) { return nil, nil }, Get: func(string) (json.RawMessage, bool, error) { return nil, false, nil }}
carriers := func(_ context.Context, name string) ([]versions.Built, error) {
if name == "@novox/mesh-sdk" {
return []versions.Built{{Module: "mesh-sdk", Artifact: "ts"}}, nil
}
return nil, nil
}
var asked []string
released := catalogReleased(func(key string, _ any) (json.RawMessage, error) {
asked = append(asked, key)
return json.Marshal(meshcatalog.VersionsAnswer{Versions: []meshcatalog.VersionEntry{{Module: "mesh-sdk", Version: "0.1.2",
Artifacts: []meshcatalog.VersionArtifact{{Artifact: "mesh-sdk:ts", Digest: "sha256:dd"}}}}})
})
handle := reservedVerb(context.Background(), none, carriers, released)
got, err := handle("", json.RawMessage(`{"name":"@novox/mesh-sdk","version":"0.1.2"}`))
a, ok := got.(meshbuilds.ReservedAnswer)
if err != nil || !ok || !a.Released || a.Module != "mesh-sdk" || len(a.Artifacts) != 1 || a.Artifacts[0].Digest != "sha256:dd" {
t.Fatalf("reserved by name = %+v (%v)", got, err)
}
if len(asked) != 1 || asked[0] != "seat:mesh-catalog.versions" {
t.Errorf("the released versions were asked of %v, want the seat mesh-catalog's versions", asked)
}
if _, err := handle("", json.RawMessage(`{"module":"mesh-sdk"}`)); err == nil {
t.Error("a module with no reservation was answered")
}
if _, err := handle("", json.RawMessage(`{"module":`)); err == nil {
t.Error("arguments that are no JSON were answered")
}
}
+14 -1
View File
@@ -170,7 +170,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
}
// 1945: the builder's tools (builder_seat_queue: note 62), served by its tools bundle, and the event it says when it asks an ask again.
if want := []string{"builder_announce_again", "builder_ask_again", "builder_bounds", "builder_build", "builder_build_again", "builder_builds_since", "builder_engine_reads", "builder_first_view_again", "builder_first_views",
"builder_floor_again", "builder_floor_waits", "builder_old_form", "builder_publish_again", "builder_publish_queue", "builder_release", "builder_reserved", "builder_reserved_drop", "builder_seat_list", "builder_seat_list_again", "builder_seat_queue", "builder_stuck", "builder_toolchains", "builder_toolchains_drop", "builder_versions", "builder_waits", "reserved"}; !reflect.DeepEqual(sorted(m.Tools), want) {
"builder_floor_again", "builder_floor_waits", "builder_old_form", "builder_publish_again", "builder_publish_queue", "builder_release", "builder_reserved", "builder_reserved_drop", "builder_seat_list", "builder_seat_list_again", "builder_seat_queue", "builder_stuck", "builder_toolchains", "builder_toolchains_drop", "builder_versions", "builder_waits"}; !reflect.DeepEqual(sorted(m.Tools), want) {
t.Errorf("tools = %v, want %v", m.Tools, want)
}
// Design 47 G §672: the toolchain waits' stream, kept 7 days within 16 MiB, and the bound's setting, 60 minutes.
@@ -195,6 +195,19 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
if len(m.Claims) != 1 || m.Claims[0].Name != "mesh-builds" || m.Claims[0].Scope != "mesh" || !reflect.DeepEqual(m.Claims[0].Serves, []string{"reserved"}) {
t.Errorf("claims = %+v, want mesh-builds, scope mesh, serving reserved", m.Claims)
}
// A-115: the process answers the verb itself and says so in `serves` beside its process, with no `bus`; the bundle serves no tool named so.
var resources []map[string]any
if err := json.Unmarshal(top["resources"], &resources); err != nil {
t.Fatal(err)
}
for _, r := range resources {
if r["type"] == "process" && !reflect.DeepEqual(r["serves"], []any{"seat:mesh-builds.reserved"}) {
t.Errorf("the process serves %v, want seat:mesh-builds.reserved (A-115)", r["serves"])
}
}
if slices.Contains(m.Tools, "reserved") {
t.Error("the tools bundle serves reserved, which the process answers")
}
for _, k := range []string{"provides", "requires", "defines-seats", "seat", "seats", "bus"} {
if _, ok := top[k]; ok {
t.Errorf("the manifest declares %q: the builder has none", k)
+3 -1
View File
@@ -35,7 +35,6 @@
"builder_toolchains_drop",
"builder_waits",
"builder_versions",
"reserved",
"builder_first_views",
"builder_first_view_again"
],
@@ -208,6 +207,9 @@
"type": "process",
"name": "mesh-delivery-builder",
"artifact": "program",
"serves": [
"seat:mesh-builds.reserved"
],
"run": [
"./mesh-delivery-builder"
],