J4 (J3 item 7): merge A3's wip/registry-a3-sdk (c974a11): a holder's receiving side of a file in parts joins its sender

Brings A3's parts receiver and bus adapters, the shared caller lists and builder versions question, a transfer read
back from bus state (0424e87) and catalogue.SeatReference (c10ef07). c974a11 adds nothing: its version_numbered.go
and BuildAnswer.OutOfStep are the join's own. publish stays out (ADR 0627, 1a149d4): packages_list_test keeps the
join's form. TestAHolderTakesAFileInParts is a subtest of TestAFileTravelsInTheParts, so unit tests stay at 476.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 15:12:03 +01:00
11 changed files with 891 additions and 4 deletions
+17
View File
@@ -1,7 +1,9 @@
package delivery
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
"testing"
@@ -174,6 +176,21 @@ func TestBuilderBuildVersion(t *testing.T) {
t.Errorf("DecodeBuilderBuild(%s) = %v", in, err)
}
}
// A holder's question before it stores (ADR 0605 §4): asked at the builder's address with name, version and digest;
// an answer without recorded is an error, never a yes.
for answer, want := range map[string]string{`{"recorded":true}`: "true ", `{"recorded":false,"why":"other digest"}`: "false other digest",
`{"name":"x"}`: "error", `not json`: "error"} {
b := BuilderVersions{Ask: func(addr string, body any) (json.RawMessage, error) {
if a := body.(map[string]any); addr != BuilderVersionsAddress || a["name"] != "n" || a["version"] != "0.1.2" || a["digest"] != "sha256:d" {
t.Errorf("asked %s %v", addr, body)
}
return json.RawMessage(answer), nil
}}
ok, why, err := b.Recorded("n", "0.1.2", "sha256:d")
if got := fmt.Sprintf("%v %s", ok, why); err != nil && want != "error" || err == nil && got != want {
t.Errorf("answer %s: %s, %v; want %s", answer, got, err, want)
}
}
}
// 1358/AC6: the builder's bucket and module do not collide with the controller's build layout (buildrecord.go), which
+38
View File
@@ -0,0 +1,38 @@
package delivery
import (
"encoding/json"
"fmt"
)
// The holder's question before it stores a publish (novox/hq ADR 0605 §4, design 51 *Publishing*): did the builder
// number this version with this digest? Every registry seat's holder asks it the same way, so the asking lives here
// once (ADR 0627 §2), beside the builder's layouts; no holder writes it again.
//
// The builder answers it today only as its module's tool builder_versions: no seat in the seat set names the builder's
// role yet. Once one does, BuilderVersionsAddress becomes that seat's verb and nothing else changes for a holder.
const BuilderVersionsAddress = BuilderModule + ".builder_versions"
// AskFunc is the runtime's request and reply over the bus (mesh-sdk stdio.Ask), given by the holder so this package does
// not depend on the runtime.
type AskFunc func(address string, body any) (json.RawMessage, error)
// BuilderVersions asks the builder whether it numbered a version with a digest. It answers the holder's Check:
// recorded, and why not when it is not.
type BuilderVersions struct{ Ask AskFunc }
// Recorded asks the builder about name@version with digest. An answer without `recorded` is an error, never a yes.
func (b BuilderVersions) Recorded(name, version, digest string) (bool, string, error) {
raw, err := b.Ask(BuilderVersionsAddress, map[string]any{"name": name, "version": version, "digest": digest})
if err != nil {
return false, "", err
}
var a struct {
Recorded *bool `json:"recorded"`
Why string `json:"why"`
}
if err := json.Unmarshal(raw, &a); err != nil || a.Recorded == nil {
return false, "", fmt.Errorf("builder_versions answered no recorded: %s", raw)
}
return *a.Recorded, a.Why, nil
}