From acd8e985d196487884baa90b38ec42633431021c Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:41:02 +0100 Subject: [PATCH 1/5] =?UTF-8?q?mesh-delivery-sdk=20kept:=20the=20version?= =?UTF-8?q?=20check,=20CheckName=20and=20IsHex64=20are=20mesh-sdk=20keys'?= =?UTF-8?q?=20now;=20kept=20keeps=20Decode=20and=20CheckDigest=20(ruling?= =?UTF-8?q?=202,=20ADR=200634=20=C2=A71;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../meshdeliverycheckerclient/client.go | 6 +-- .../mesh-delivery-builder/builds/builds.go | 8 +-- .../builds/builds_test.go | 5 +- modules/mesh-delivery-builder/builds/log.go | 3 +- .../takein/taken_test.go | 3 +- modules/mesh-delivery-view/viewstate/reach.go | 2 +- sdk/kept/kept.go | 49 ++----------------- sdk/kept/kept_test.go | 11 +---- 8 files changed, 19 insertions(+), 68 deletions(-) diff --git a/contracts/go/clients/meshdeliverycheckerclient/client.go b/contracts/go/clients/meshdeliverycheckerclient/client.go index c97e6ac3..80915d61 100644 --- a/contracts/go/clients/meshdeliverycheckerclient/client.go +++ b/contracts/go/clients/meshdeliverycheckerclient/client.go @@ -9,7 +9,7 @@ import ( "strings" checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" - "git.novox.be/novox/mesh-delivery/sdk/kept" + "git.novox.be/novox/mesh-sdk/go/keys" ) // The states of a verdict, the levels of a check and the compositions' states. @@ -44,10 +44,10 @@ func CheckRepository(repository string) error { if !ok || strings.Contains(name, "/") { return fmt.Errorf("meshdeliverycheckerclient: the repository %q is not /, with exactly one slash", repository) } - if err := kept.CheckName("repository owner", owner); err != nil { + if err := keys.CheckName("repository owner", owner); err != nil { return fmt.Errorf("meshdeliverycheckerclient: the repository %q: %w", repository, err) } - if err := kept.CheckName("repository name", name); err != nil { + if err := keys.CheckName("repository name", name); err != nil { return fmt.Errorf("meshdeliverycheckerclient: the repository %q: %w", repository, err) } return nil diff --git a/modules/mesh-delivery-builder/builds/builds.go b/modules/mesh-delivery-builder/builds/builds.go index 04bd7751..26e0973c 100644 --- a/modules/mesh-delivery-builder/builds/builds.go +++ b/modules/mesh-delivery-builder/builds/builds.go @@ -65,7 +65,7 @@ const ( // being the fingerprint's key part (fingerprint.KeyPart), since a bus key may not hold ':'. A module holding a dot or a // character a key refuses, and a fingerprint not of the new form, are refused, naming them. func BuilderBuildKey(module, fp string) (string, error) { - if err := kept.CheckName("module", module); err != nil { + if err := keys.CheckName("module", module); err != nil { return "", err } part, err := fingerprint.KeyPart(fp) @@ -81,7 +81,7 @@ func ParseBuilderBuildKey(key string) (module, fp string, err error) { if !ok { return "", "", fmt.Errorf("builds: %q is not a builder build key, .", key) } - if err := kept.CheckName("module", module); err != nil { + if err := keys.CheckName("module", module); err != nil { return "", "", fmt.Errorf("builds: %q is not a builder build key: %w", key, err) } if fp, err = fingerprint.ParseKeyPart(part); err != nil { @@ -265,7 +265,7 @@ func (b BuilderBuild) check() error { return fmt.Errorf("builds: the build state %q is not queued, building, done or failed", b.State) } if b.Manifest != "" { - if !kept.IsHex64(b.Manifest) { + if !keys.IsHex64(b.Manifest) { return fmt.Errorf("builds: %q is not a manifest digest, 64 lowercase hex digits", b.Manifest) } } @@ -355,7 +355,7 @@ func EncodeBuilderBuild(b BuilderBuild) ([]byte, error) { return json.Marshal(b) } -// DecodeBuilderBuild reads a builder build value; it refuses an unknown version (kept.ErrUnknownVersion) and what +// DecodeBuilderBuild reads a builder build value; it refuses an unknown version (keys.ErrUnknownVersion) and what // EncodeBuilderBuild refuses. func DecodeBuilderBuild(data []byte) (BuilderBuild, error) { var b BuilderBuild diff --git a/modules/mesh-delivery-builder/builds/builds_test.go b/modules/mesh-delivery-builder/builds/builds_test.go index e49c966e..8e272b67 100644 --- a/modules/mesh-delivery-builder/builds/builds_test.go +++ b/modules/mesh-delivery-builder/builds/builds_test.go @@ -9,7 +9,6 @@ import ( "testing" "git.novox.be/novox/mesh-delivery/sdk/buildrecord" - "git.novox.be/novox/mesh-delivery/sdk/kept" "git.novox.be/novox/mesh-sdk/go/keys" ) @@ -177,10 +176,10 @@ func TestBuilderBuildNamesItsToolchains(t *testing.T) { } } -// 1358/AC5: a value of another version, or none, is refused with kept.ErrUnknownVersion. +// 1358/AC5: a value of another version, or none, is refused with keys.ErrUnknownVersion. func TestBuilderBuildVersion(t *testing.T) { for _, in := range []string{`{"v":2,"state":"queued"}`, `{"state":"queued"}`} { - if _, err := DecodeBuilderBuild([]byte(in)); !errors.Is(err, kept.ErrUnknownVersion) { + if _, err := DecodeBuilderBuild([]byte(in)); !errors.Is(err, keys.ErrUnknownVersion) { t.Errorf("DecodeBuilderBuild(%s) = %v", in, err) } } diff --git a/modules/mesh-delivery-builder/builds/log.go b/modules/mesh-delivery-builder/builds/log.go index d7344eee..8bdc55e8 100644 --- a/modules/mesh-delivery-builder/builds/log.go +++ b/modules/mesh-delivery-builder/builds/log.go @@ -6,7 +6,6 @@ import ( "sort" "time" - "git.novox.be/novox/mesh-delivery/sdk/kept" "git.novox.be/novox/mesh-delivery/sdk/trunk" "git.novox.be/novox/mesh-sdk/go/keys" ) @@ -36,7 +35,7 @@ const ( // BuildLogKey is the key of a module's entries in the builder's log: the module's name, refused when a log key could not // hold it. func BuildLogKey(module string) (string, error) { - if err := kept.CheckName("module", module); err != nil { + if err := keys.CheckName("module", module); err != nil { return "", err } return module, nil diff --git a/modules/mesh-delivery-indexer/takein/taken_test.go b/modules/mesh-delivery-indexer/takein/taken_test.go index 7a0aca61..9e30603e 100644 --- a/modules/mesh-delivery-indexer/takein/taken_test.go +++ b/modules/mesh-delivery-indexer/takein/taken_test.go @@ -6,7 +6,6 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-delivery/sdk/kept" "git.novox.be/novox/mesh-sdk/go/keys" ) @@ -26,7 +25,7 @@ func TestATakenValueRoundTripsAndRefusesWhatLacksARepositoryOrAMerge(t *testing. t.Errorf("taken without merge: %v", err) } for _, in := range []string{`{"v":2,"repository":"a/b","merge":"c"}`, `{"repository":"a/b","merge":"c"}`} { - if _, err := DecodeTaken([]byte(in)); !errors.Is(err, kept.ErrUnknownVersion) { + if _, err := DecodeTaken([]byte(in)); !errors.Is(err, keys.ErrUnknownVersion) { t.Errorf("decode of %s = %v, want ErrUnknownVersion", in, err) } } diff --git a/modules/mesh-delivery-view/viewstate/reach.go b/modules/mesh-delivery-view/viewstate/reach.go index 4cad0487..51574736 100644 --- a/modules/mesh-delivery-view/viewstate/reach.go +++ b/modules/mesh-delivery-view/viewstate/reach.go @@ -168,7 +168,7 @@ func EncodeViewAnswer(a ViewAnswer) ([]byte, error) { return json.Marshal(a) } -// DecodeViewAnswer reads an answer; it refuses an unknown version (kept.ErrUnknownVersion) and what EncodeViewAnswer refuses. +// DecodeViewAnswer reads an answer; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeViewAnswer refuses. func DecodeViewAnswer(data []byte) (ViewAnswer, error) { var a ViewAnswer if err := kept.Decode(data, &a); err != nil { diff --git a/sdk/kept/kept.go b/sdk/kept/kept.go index 1d907361..35fe7e48 100644 --- a/sdk/kept/kept.go +++ b/sdk/kept/kept.go @@ -1,59 +1,20 @@ -// Package kept holds what the layouts of the Delivery domain's kept values share: the version check on reading one, the -// rule a name must keep to be a key part, and the form of a digest. It names no seat and no module (ADR 0634 §2). The -// version itself (keys.Version) and the escaping of a name (keys.EscapeName) are mesh-sdk's, since more than one domain uses -// them. +// Package kept holds what the layouts of the Delivery domain's kept values share: the reading of one at its version and the form +// of a digest. It names no seat and no module (ADR 0634 §2). The version (keys.Version, keys.CheckVersion), the rule a name +// must keep to be a key part (keys.CheckName), keys.IsHex64 and the escaping of a name (keys.EscapeName) are mesh-sdk's, +// since more than one domain uses them. package kept import ( "encoding/json" - "errors" "fmt" "regexp" "git.novox.be/novox/mesh-sdk/go/keys" ) -// ErrUnknownVersion is what Decode returns for a value whose version is missing or not keys.Version. Test it with errors.Is. -var ErrUnknownVersion = errors.New("kept: unknown value version") - -var nameOK = regexp.MustCompile(`^[-/_=a-zA-Z0-9]+$`) - -// CheckName refuses a name that is empty or holds a dot, a wildcard, whitespace or a character a bus key refuses. -func CheckName(kind, name string) error { - if name == "" { - return fmt.Errorf("kept: the %s name is empty", kind) - } - if !nameOK.MatchString(name) { - return fmt.Errorf("kept: the %s name %q holds a dot, a wildcard, whitespace or a character a key refuses", kind, name) - } - return nil -} - -var hex64 = regexp.MustCompile(`^[0-9a-f]{64}$`) - -// IsHex64 says whether s is 64 lowercase hex digits. -func IsHex64(s string) bool { return hex64.MatchString(s) } - -// CheckVersion refuses a value with no version or one this reader does not read. -func CheckVersion(data []byte) error { - var probe struct { - V *int `json:"v"` - } - if err := json.Unmarshal(data, &probe); err != nil { - return fmt.Errorf("kept: value is not JSON: %w", err) - } - if probe.V == nil { - return fmt.Errorf("%w: no version in the value, this reader reads %d", ErrUnknownVersion, keys.Version) - } - if *probe.V != keys.Version { - return fmt.Errorf("%w: the value is version %d, this reader reads %d", ErrUnknownVersion, *probe.V, keys.Version) - } - return nil -} - // Decode reads data into into after CheckVersion. func Decode(data []byte, into any) error { - if err := CheckVersion(data); err != nil { + if err := keys.CheckVersion(data); err != nil { return err } if err := json.Unmarshal(data, into); err != nil { diff --git a/sdk/kept/kept_test.go b/sdk/kept/kept_test.go index aeff9ec0..870c6cf4 100644 --- a/sdk/kept/kept_test.go +++ b/sdk/kept/kept_test.go @@ -2,6 +2,7 @@ package kept import ( "errors" + "git.novox.be/novox/mesh-sdk/go/keys" "strings" "testing" ) @@ -10,21 +11,13 @@ func TestAKeptValueIsReadOnlyAtItsVersionAndAKeyPartOnlyOfKeyCharacters(t *testi var v struct{ V int } for data, ok := range map[string]bool{`{"v":1}`: true, `{"v":2}`: false, `{}`: false} { err := Decode([]byte(data), &v) - if (err == nil) != ok || (!ok && !errors.Is(err, ErrUnknownVersion)) { + if (err == nil) != ok || (!ok && !errors.Is(err, keys.ErrUnknownVersion)) { t.Errorf("Decode(%s) = %v", data, err) } } if Decode([]byte(`nope`), &v) == nil { t.Error("a value that is no JSON is read") } - for name, ok := range map[string]bool{"a-b_c/d=": true, "": false, "a.b": false, "a b": false, "a*": false} { - if (CheckName("module", name) == nil) != ok { - t.Errorf("CheckName(%q) = %v", name, CheckName("module", name)) - } - } - if !IsHex64(strings.Repeat("a", 64)) || IsHex64(strings.Repeat("A", 64)) || IsHex64("a") { - t.Error("IsHex64 reads a wrong form") - } if CheckDigest("sha256:"+strings.Repeat("0", 64), "x") != nil || CheckDigest("sha256:0", "x") == nil { t.Error("CheckDigest reads a wrong form") } From dcd1f3c22b6f0c1700419155f4adf6212af07e3e Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:44:23 +0100 Subject: [PATCH 2/5] =?UTF-8?q?mesh-delivery-contracts:=20the=20artifact?= =?UTF-8?q?=20reference=20forms=20move=20to=20the=20registry=20seats'=20cl?= =?UTF-8?q?ient=20and=20the=20fingerprint=20key=20part=20to=20a=20new=20cl?= =?UTF-8?q?ient=20of=20mesh-catalog;=20mesh-delivery-sdk=20serves=20Delive?= =?UTF-8?q?ry=20modules=20only=20(ruling=203,=20ADR=200634=20=C2=A71,=20?= =?UTF-8?q?=C2=A72;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../reference/reference.go | 4 ++-- .../reference/reference_test.go | 0 .../clients/meshcatalogclient}/fingerprint.go | 19 +++++++++---------- .../meshcatalogclient}/fingerprint_test.go | 2 +- contracts/go/clients/meshcatalogclient/go.mod | 14 ++++++++++++++ contracts/go/clients/meshcatalogclient/go.sum | 18 ++++++++++++++++++ contracts/module.json | 9 +++++++++ .../mesh-delivery-builder/builds/builds.go | 10 +++++----- modules/mesh-delivery-builder/builds/go.mod | 2 ++ 9 files changed, 60 insertions(+), 18 deletions(-) rename {sdk => contracts/go/clients/containerimageregistryclient}/reference/reference.go (95%) rename {sdk => contracts/go/clients/containerimageregistryclient}/reference/reference_test.go (100%) rename {sdk/fingerprint => contracts/go/clients/meshcatalogclient}/fingerprint.go (66%) rename {sdk/fingerprint => contracts/go/clients/meshcatalogclient}/fingerprint_test.go (97%) create mode 100644 contracts/go/clients/meshcatalogclient/go.mod create mode 100644 contracts/go/clients/meshcatalogclient/go.sum diff --git a/sdk/reference/reference.go b/contracts/go/clients/containerimageregistryclient/reference/reference.go similarity index 95% rename from sdk/reference/reference.go rename to contracts/go/clients/containerimageregistryclient/reference/reference.go index d451689b..80cdaf46 100644 --- a/sdk/reference/reference.go +++ b/contracts/go/clients/containerimageregistryclient/reference/reference.go @@ -1,6 +1,6 @@ // Package reference holds the form of an artifact's reference once a registry seat published it: `//@` -// (novox/hq ADR 0605 §9), and the older `artifact-store://` form read beside it. It is the Delivery domain's rule over -// the names of the two registry seats that front the artifact store; it names no seat itself (ADR 0634 §4, ADR 0635 §4): the +// (novox/hq ADR 0605 §9), and the older `artifact-store://` form read beside it. It is the rule the holders and callers of the two +// registry seats that front the artifact store share (the seats' client, novox/hq ADR 0625 §4, ADR 0634 §2); it names no seat itself (ADR 0634 §4, ADR 0635 §4): the // caller gives the seats' names from their contracts (Seats), so a package here cannot drift from a contract. package reference diff --git a/sdk/reference/reference_test.go b/contracts/go/clients/containerimageregistryclient/reference/reference_test.go similarity index 100% rename from sdk/reference/reference_test.go rename to contracts/go/clients/containerimageregistryclient/reference/reference_test.go diff --git a/sdk/fingerprint/fingerprint.go b/contracts/go/clients/meshcatalogclient/fingerprint.go similarity index 66% rename from sdk/fingerprint/fingerprint.go rename to contracts/go/clients/meshcatalogclient/fingerprint.go index 34b60bec..0565fdb5 100644 --- a/sdk/fingerprint/fingerprint.go +++ b/contracts/go/clients/meshcatalogclient/fingerprint.go @@ -1,11 +1,14 @@ -// Package fingerprint holds the form of a module's source fingerprint, `src2:` and 64 lowercase hex digits (novox/hq ADR 0593 -// §2): the indexer computes it alone, in its own package; every part that keys a state by it reads this form. It is the -// Delivery domain's rule, over the `fp2` field of mesh-catalog's `modules` value (ADR 0634 §1). -package fingerprint +// Package meshcatalogclient holds the logic the holders and callers of the seat mesh-catalog share over its contract (novox/hq +// ADR 0625 §4, ADR 0634 §2): the form of a module's source fingerprint, `src2:` and 64 lowercase hex digits (ADR 0593 §2), +// over the `fp2` field of the seat's `modules` value. The indexer computes a fingerprint alone, in its own package; every part +// that keys a state by it, the node-engine among them, reads this form here. +package meshcatalogclient import ( "fmt" "strings" + + "git.novox.be/novox/mesh-sdk/go/keys" ) // SourcePrefix2 opens every source fingerprint of the form. @@ -23,7 +26,7 @@ type Toolchain2 struct { // refuses a value that is not `src2:` and 64 lowercase hex digits, a src1 fingerprint included, naming it. func KeyPart(fp string) (string, error) { part, ok := strings.CutPrefix(fp, SourcePrefix2) - if !ok || !isHex64(part) { + if !ok || !keys.IsHex64(part) { return "", fmt.Errorf("fingerprint: %q is not a fingerprint of the form, %s and 64 lowercase hex digits", fp, SourcePrefix2) } return part, nil @@ -31,12 +34,8 @@ func KeyPart(fp string) (string, error) { // ParseKeyPart is the inverse of KeyPart. It refuses a part that is not 64 lowercase hex digits, naming it. func ParseKeyPart(part string) (string, error) { - if !isHex64(part) { + if !keys.IsHex64(part) { return "", fmt.Errorf("fingerprint: %q is not the key part of a fingerprint, 64 lowercase hex digits", part) } return SourcePrefix2 + part, nil } - -func isHex64(s string) bool { - return len(s) == 64 && strings.Trim(s, "0123456789abcdef") == "" -} diff --git a/sdk/fingerprint/fingerprint_test.go b/contracts/go/clients/meshcatalogclient/fingerprint_test.go similarity index 97% rename from sdk/fingerprint/fingerprint_test.go rename to contracts/go/clients/meshcatalogclient/fingerprint_test.go index 11efe273..ec575e0a 100644 --- a/sdk/fingerprint/fingerprint_test.go +++ b/contracts/go/clients/meshcatalogclient/fingerprint_test.go @@ -1,4 +1,4 @@ -package fingerprint +package meshcatalogclient import ( "strings" diff --git a/contracts/go/clients/meshcatalogclient/go.mod b/contracts/go/clients/meshcatalogclient/go.mod new file mode 100644 index 00000000..12ac8e36 --- /dev/null +++ b/contracts/go/clients/meshcatalogclient/go.mod @@ -0,0 +1,14 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient + +go 1.26.0 + +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef + +require ( + github.com/klauspost/compress v1.20.1 // indirect + github.com/nats-io/nats.go v1.54.0 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/sys v0.48.0 // indirect +) diff --git a/contracts/go/clients/meshcatalogclient/go.sum b/contracts/go/clients/meshcatalogclient/go.sum new file mode 100644 index 00000000..c811fda7 --- /dev/null +++ b/contracts/go/clients/meshcatalogclient/go.sum @@ -0,0 +1,18 @@ +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a h1:E9ERPqR4uZXypudHGxv2/wNIOYcder9torVqJwJq/DI= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-20261105000000-000000000000 h1:BTqpa7dCCTQH9m08Jle8KVFwyIHiHPSUbwWxuqSg2yA= +git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-20261105000000-000000000000/go.mod h1:pPxwhTMb3hIk7UUrBicJnGQCqfTEoBGJvNFspEK2U18= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef h1:/Jic5ho0xz32Z9DH2NSUqVxCF1KyfIOWuRTrX1aLjus= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef/go.mod h1:2wHG7ntLCG8Tpm/SCfgp524a5emUJm1gAC4KrRYsU9s= +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/contracts/module.json b/contracts/module.json index 4e537386..a239ea00 100644 --- a/contracts/module.json +++ b/contracts/module.json @@ -112,6 +112,15 @@ ], "series": "0.1" }, + { + "name": "go-meshcatalogclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/meshcatalogclient" + ], + "series": "0.1" + }, { "name": "go-meshdeliverycheckerclient", "kind": "package", diff --git a/modules/mesh-delivery-builder/builds/builds.go b/modules/mesh-delivery-builder/builds/builds.go index 26e0973c..b52b676d 100644 --- a/modules/mesh-delivery-builder/builds/builds.go +++ b/modules/mesh-delivery-builder/builds/builds.go @@ -11,12 +11,12 @@ import ( "slices" "strings" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient/reference" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry" "git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry" "git.novox.be/novox/mesh-delivery/sdk/buildstep" - "git.novox.be/novox/mesh-delivery/sdk/fingerprint" "git.novox.be/novox/mesh-delivery/sdk/kept" - "git.novox.be/novox/mesh-delivery/sdk/reference" "git.novox.be/novox/mesh-delivery/sdk/trunk" "git.novox.be/novox/mesh-sdk/go/keys" ) @@ -62,13 +62,13 @@ const ( ) // BuilderBuildKey is the key, in BuilderBuildsBucket, of a module's build at a fingerprint: `.`, the hex -// being the fingerprint's key part (fingerprint.KeyPart), since a bus key may not hold ':'. A module holding a dot or a +// being the fingerprint's key part (meshcatalogclient.KeyPart), since a bus key may not hold ':'. A module holding a dot or a // character a key refuses, and a fingerprint not of the new form, are refused, naming them. func BuilderBuildKey(module, fp string) (string, error) { if err := keys.CheckName("module", module); err != nil { return "", err } - part, err := fingerprint.KeyPart(fp) + part, err := meshcatalogclient.KeyPart(fp) if err != nil { return "", err } @@ -84,7 +84,7 @@ func ParseBuilderBuildKey(key string) (module, fp string, err error) { if err := keys.CheckName("module", module); err != nil { return "", "", fmt.Errorf("builds: %q is not a builder build key: %w", key, err) } - if fp, err = fingerprint.ParseKeyPart(part); err != nil { + if fp, err = meshcatalogclient.ParseKeyPart(part); err != nil { return "", "", fmt.Errorf("builds: %q is not a builder build key: %w", key, err) } return module, fp, nil diff --git a/modules/mesh-delivery-builder/builds/go.mod b/modules/mesh-delivery-builder/builds/go.mod index 8fc38072..4eb84be8 100644 --- a/modules/mesh-delivery-builder/builds/go.mod +++ b/modules/mesh-delivery-builder/builds/go.mod @@ -3,6 +3,8 @@ module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds go 1.26.0 require ( + git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0 + git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0 git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0 git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0 git.novox.be/novox/mesh-delivery/sdk v0.0.0 From ccef33ef317c988bac75a4cea6ca1690e7590426 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:47:16 +0100 Subject: [PATCH 3/5] =?UTF-8?q?mesh-delivery-contracts:=20the=20client=20o?= =?UTF-8?q?f=20mesh-delivery=20(heads=20hash,=20merge=20ask=20key,=20ask?= =?UTF-8?q?=20id,=20owed=20acts=20and=20keys,=20owed=20values)=20and=20the?= =?UTF-8?q?=20checker=20client's=20preview=20rules=20(D-27,=20ADR=200625?= =?UTF-8?q?=20=C2=A74,=20ADR=200635=20=C2=A75;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../meshdeliverycheckerclient/client.go | 58 ++++ .../meshdeliverycheckerclient/client_test.go | 46 +++ .../go/clients/meshdeliveryclient/client.go | 323 ++++++++++++++++++ .../clients/meshdeliveryclient/client_test.go | 91 +++++ .../go/clients/meshdeliveryclient/go.mod | 17 + .../go/clients/meshdeliveryclient/go.sum | 18 + contracts/module.json | 9 + 7 files changed, 562 insertions(+) create mode 100644 contracts/go/clients/meshdeliveryclient/client.go create mode 100644 contracts/go/clients/meshdeliveryclient/client_test.go create mode 100644 contracts/go/clients/meshdeliveryclient/go.mod create mode 100644 contracts/go/clients/meshdeliveryclient/go.sum diff --git a/contracts/go/clients/meshdeliverycheckerclient/client.go b/contracts/go/clients/meshdeliverycheckerclient/client.go index 80915d61..c7d75a2e 100644 --- a/contracts/go/clients/meshdeliverycheckerclient/client.go +++ b/contracts/go/clients/meshdeliverycheckerclient/client.go @@ -6,6 +6,7 @@ package meshdeliverycheckerclient import ( "fmt" + pathpkg "path" "strings" checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" @@ -220,3 +221,60 @@ func CheckKept(k checker.KeptByLevelDigestValue) error { } return nil } + +// CheckPreview refuses a preview a reader could not place (the checker writes it, the indexer reads it for the candidates' +// fingerprints): a repository, head or trunk commit that does not key, a head in conflict that names no file or has a merge +// commit or modules, a head that merged and names no merge commit, and a module with no directory, no tree, a manifest +// digest that is not 64 lowercase hex digits, a lock without path or hash, or a named path that is not clean and inside the +// module's directory or names no git object. +func CheckPreview(p checker.PreviewsByRepositoryHeadTrunkValue) error { + const who = "meshdeliverycheckerclient: " + if err := CheckRepository(p.Repository); err != nil { + return err + } + if err := keys.CheckName("head", p.Head); err != nil { + return fmt.Errorf(who+"the preview: %w", err) + } + if err := keys.CheckName("trunk commit", p.Trunk); err != nil { + return fmt.Errorf(who+"the preview: %w", err) + } + for _, f := range p.Conflicts { + if f == "" { + return fmt.Errorf(who + "a preview names a conflicting file with an empty path") + } + } + if len(p.Conflicts) > 0 { + if p.Merge != "" || len(p.Modules) > 0 { + return fmt.Errorf(who + "a preview of a head in conflict has no merge commit and no modules") + } + return nil + } + if p.Merge == "" { + return fmt.Errorf(who + "a preview names no merge commit") + } + for dir, m := range p.Modules { + if dir == "" { + return fmt.Errorf(who + "a preview names a module with an empty directory") + } + if m.Tree == "" { + return fmt.Errorf(who+"the preview's module %q has no tree", dir) + } + if !keys.IsHex64(m.Manifest) { + return fmt.Errorf(who+"the preview's module %q: %q is not a manifest digest, 64 lowercase hex digits", dir, m.Manifest) + } + for path, hash := range m.Locks { + if path == "" || hash == "" { + return fmt.Errorf(who+"the lock %q of the preview's module %q has an empty path or hash", path, dir) + } + } + for p, object := range m.Paths { + if p == "" || p != pathpkg.Clean(p) || strings.HasPrefix(p, "/") || p == ".." || strings.HasPrefix(p, "../") { + return fmt.Errorf(who+"the preview's module %q: the named path %q is not a clean path inside the module's directory", dir, p) + } + if object == "" { + return fmt.Errorf(who+"the preview's module %q: the named path %q names no git object", dir, p) + } + } + } + return nil +} diff --git a/contracts/go/clients/meshdeliverycheckerclient/client_test.go b/contracts/go/clients/meshdeliverycheckerclient/client_test.go index 062efc25..04191ae5 100644 --- a/contracts/go/clients/meshdeliverycheckerclient/client_test.go +++ b/contracts/go/clients/meshdeliverycheckerclient/client_test.go @@ -59,6 +59,7 @@ func TestAVerdictIsRefusedForWhatAReaderCouldNotPlace(t *testing.T) { } func TestAKeptVerdictIsAPassOfAPackageAtALevel(t *testing.T) { + t.Run("a preview is refused for what a reader of candidates could not place", previewIsRefused) ok := checker.KeptByLevelDigestValue{State: VerdictPass, Repository: "novox/mesh-sdk", Package: "./go/keys", Level: LevelUnit, Toolchain: "1.26.0", At: "2026-11-05T10:00:00Z"} if err := CheckKept(ok); err != nil { t.Fatalf("a kept pass is refused: %v", err) @@ -79,3 +80,48 @@ func TestAKeptVerdictIsAPassOfAPackageAtALevel(t *testing.T) { } } } + +// A preview is refused for what a reader of candidates could not place. +func previewIsRefused(t *testing.T) { + good := func() checker.PreviewsByRepositoryHeadTrunkValue { + return checker.PreviewsByRepositoryHeadTrunkValue{Repository: "novox/a", Head: "h1", Trunk: "t1", Merge: "m1", + Modules: map[string]checker.PreviewsByRepositoryHeadTrunkValueModulesValue{"mod": {Tree: "tr", Manifest: strings.Repeat("a", 64), + Locks: map[string]string{"go.sum": "x"}, Paths: map[string]string{"src": "o1"}}}} + } + if err := CheckPreview(good()); err != nil { + t.Fatalf("a good preview is refused: %v", err) + } + for name, c := range map[string]struct { + edit func(*checker.PreviewsByRepositoryHeadTrunkValue) + says string + }{ + "a repository with no owner": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { p.Repository = "a" }, "/"}, + "a head with a dot": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { p.Head = "h.1" }, "head"}, + "a conflict with a merge": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { p.Conflicts = []string{"f"} }, "in conflict"}, + "no merge commit": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { p.Merge = "" }, "no merge commit"}, + "a module with no tree": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { + m := p.Modules["mod"] + m.Tree = "" + p.Modules["mod"] = m + }, "no tree"}, + "a short manifest digest": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { + m := p.Modules["mod"] + m.Manifest = "abc" + p.Modules["mod"] = m + }, "manifest digest"}, + "a path outside the module": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { + m := p.Modules["mod"] + m.Paths = map[string]string{"../x": "o"} + p.Modules["mod"] = m + }, "clean path"}, + } { + p := good() + c.edit(&p) + if err := CheckPreview(p); err == nil || !strings.Contains(err.Error(), c.says) { + t.Errorf("%s: CheckPreview = %v, want it to say %q", name, err, c.says) + } + } + if err := CheckPreview(checker.PreviewsByRepositoryHeadTrunkValue{Repository: "novox/a", Head: "h", Trunk: "t", Conflicts: []string{"f"}}); err != nil { + t.Errorf("a head in conflict is refused: %v", err) + } +} diff --git a/contracts/go/clients/meshdeliveryclient/client.go b/contracts/go/clients/meshdeliveryclient/client.go new file mode 100644 index 00000000..5754e183 --- /dev/null +++ b/contracts/go/clients/meshdeliveryclient/client.go @@ -0,0 +1,323 @@ +// Package meshdeliveryclient holds the logic the holders and callers of the seat mesh-delivery share over its contract +// (novox/hq ADR 0625 §4, ADR 0634 §2, ADR 0635 §5): the keys of its states `merges`, `asks` and `owed` and the rules their +// values keep, the id the teller gives the router for an ask, and the act an ask's options bind. The gate writes them, the +// forge bridge, the teller and the assigner read them; the contract's decoding is strict by schema only, so these rules are +// written once here and no caller copies them. +package meshdeliveryclient + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "regexp" + "sort" + "strconv" + "strings" + + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// AskKindMerge is the kind of the gate's ask for a group's merge, the first part of its key. +const AskKindMerge = "merge" + +// VerbMerge is the act an ask for a group's merge binds in its options, `mesh-delivery.merge` (Approve). +const VerbMerge = "merge" + +// Head is one pull request at one commit: the repository as `/` and the full sha. +type Head struct { + Repository string `json:"repository"` + Number int `json:"number"` + Head string `json:"head"` +} + +// HeadsHash is the first 12 hex digits of the sha256 over the lines `#=`, sorted and joined by a +// newline: the same for every order of heads, different when any head changes. An empty list is refused. +func HeadsHash(heads []Head) (string, error) { + if len(heads) == 0 { + return "", fmt.Errorf("meshdeliveryclient: a heads hash needs at least one pull request") + } + lines := make([]string, len(heads)) + for i, h := range heads { + lines[i] = fmt.Sprintf("%s#%d=%s", h.Repository, h.Number, h.Head) + } + sort.Strings(lines) + sum := sha256.Sum256([]byte(strings.Join(lines, "\n"))) + return hex.EncodeToString(sum[:])[:12], nil +} + +var hashOK = regexp.MustCompile(`^[0-9a-f]{12}$`) + +func groupKey(group string) (string, error) { + if group == "" { + return "", fmt.Errorf("meshdeliveryclient: the group name is empty") + } + return keys.EscapeName(group), nil +} + +// MergesKey is the key, in the state merges, of a group: ``, escaped so a branch such as `fix/1.2` keys. +func MergesKey(group string) (string, error) { return groupKey(group) } + +// MergeAskKey is the key, in the state asks, of the ask for merging a group at these heads: `merge..`. +// The teller's answer has the same key. +func MergeAskKey(group string, heads []Head) (string, error) { + part, err := groupKey(group) + if err != nil { + return "", err + } + hash, err := HeadsHash(heads) + if err != nil { + return "", err + } + return AskKindMerge + "." + part + "." + hash, nil +} + +// ParseAskKey is the inverse of MergeAskKey: the kind, the group and the heads hash. +func ParseAskKey(key string) (kind, group, hash string, err error) { + parts := strings.Split(key, ".") + if len(parts) != 3 || parts[0] != AskKindMerge { + return "", "", "", fmt.Errorf("meshdeliveryclient: %q is not an ask key, merge..", key) + } + if parts[1] == "" { + return "", "", "", fmt.Errorf("meshdeliveryclient: %q is not an ask key: the group name is empty", key) + } + if group, err = keys.UnescapeName(parts[1]); err != nil { + return "", "", "", fmt.Errorf("meshdeliveryclient: %q is not an ask key: %w", key, err) + } + if !hashOK.MatchString(parts[2]) { + return "", "", "", fmt.Errorf("meshdeliveryclient: %q is not an ask key: %q is not 12 hex digits", key, parts[2]) + } + return parts[0], group, parts[2], nil +} + +// SettleGroupPrefix begins the group of a settle ask (novox/hq ADR 0556 clause 5): a person's ask to settle deliveries held +// because their merge reached the trunk without a passing check. It is kept in the asks under MergeAskKey like a merge ask, +// so the teller carries it unchanged. Git forbids a colon in a branch name, so no pull request's group ever begins with it. +const SettleGroupPrefix = "settle:" + +// SettleGroup is the group of the settle ask of these delivery ids: the prefix and twelve hex digits of the sha256 over the +// ids, sorted and joined by a newline, so the same deliveries asked again keep one key. +func SettleGroup(ids []string) string { + sorted := append([]string(nil), ids...) + sort.Strings(sorted) + sum := sha256.Sum256([]byte(strings.Join(sorted, "\n"))) + return SettleGroupPrefix + hex.EncodeToString(sum[:])[:12] +} + +// AskVerb is the act the options of an ask bind, by its group: a settle ask's `mesh-delivery.settle`, any other +// `mesh-delivery.merge`. The gate composes the act with it and the teller checks a warrant against it. +func AskVerb(group string) string { + if strings.HasPrefix(group, SettleGroupPrefix) { + return seat.Seat + "." + seat.VerbSettle + } + return seat.Seat + "." + VerbMerge +} + +// AskID is the id the teller gives the router for the ask at askKey. The router's ids refuse dots, so in the readable part a +// dot becomes `-` and any other byte outside letters, digits, `_` and `-` becomes `_`; that part is cut to fit, and every id +// ends in `-` and the first 12 hex digits of the sha256 of the whole key, so two keys never give one id. It cannot be read +// back: the teller keeps the key beside it. +func AskID(askKey string) string { + const most = 64 // the router's bound on an id + const tail = 13 // "-" and 12 hex digits + readable := []byte(askKey) + for i, c := range readable { + switch { + case c >= 'A' && c <= 'Z', c >= 'a' && c <= 'z', c >= '0' && c <= '9': + case c == '.': + readable[i] = '-' + default: // a slash, an equals sign or any other byte the router's id refuses + readable[i] = '_' + } + } + if len(readable) > 0 && (readable[0] == '-' || readable[0] == '_') { + readable[0] = 'k' // an id starts with a letter or digit + } + if len(readable) > most-tail { + readable = readable[:most-tail] + } + sum := sha256.Sum256([]byte(askKey)) + return string(readable) + "-" + hex.EncodeToString(sum[:])[:12] +} + +// AskingID is the id of the asking-th writing of the ask at askKey. Asking 0 and 1 give AskID(askKey), so the first asking's +// id is unchanged; asking n of 2 or more gives the AskID form of askKey + "." + n, so each asking has an id of its own and the +// router, which drops an ask under an id it keeps as a redelivery, takes each as new and an answer to an earlier asking is +// never taken for a later one. +func AskingID(askKey string, asking int) string { + if asking < 2 { + return AskID(askKey) + } + return AskID(askKey + "." + strconv.Itoa(asking)) +} + +// The acts the gate owes the forge, the last part of an owed key. +const ( + OwedStatus = "status" + OwedView = "view" + OwedNote = "note" + OwedComment = "comment" +) + +// The status contexts the mesh owes the forge. +const ( + StatusContextMergeGate = "mesh/merge-gate" + StatusContextDelivery = "mesh/delivery" + StatusContextDeliveryGroup = "mesh/delivery-group" + StatusContextRepoCheck = "mesh/repo-check" + StatusContextCheck = "mesh/check" +) + +var statusContexts = []string{StatusContextMergeGate, StatusContextDelivery, StatusContextDeliveryGroup, StatusContextRepoCheck, StatusContextCheck} + +// StatusContexts is the contexts, in a new slice. +func StatusContexts() []string { return append([]string(nil), statusContexts...) } + +// The states a status the gate owes the forge may carry. +var statusStates = []string{"pending", "success", "failure", "error", "warning"} + +func checkStatusContext(context string) error { + for _, c := range statusContexts { + if context == c { + return nil + } + } + return fmt.Errorf("meshdeliveryclient: the status context %q is not one of %s", context, strings.Join(statusContexts, ", ")) +} + +// ParseOwedAct splits an owed act into its kind and, for a status, its context, for a comment its sequence as digits. The act +// is `status=`, `view`, `note` or `comment.`. +func ParseOwedAct(act string) (kind, context string, err error) { + kind, context, hasContext := strings.Cut(act, "=") + switch kind { + case OwedStatus: + if !hasContext { + return "", "", fmt.Errorf("meshdeliveryclient: the owed act %q is a status without its context, status=", act) + } + if err := checkStatusContext(context); err != nil { + return "", "", err + } + return kind, context, nil + case OwedView, OwedNote: + if hasContext { + return "", "", fmt.Errorf("meshdeliveryclient: the owed act %q takes no context", act) + } + return kind, "", nil + } + if kind, seq, ok := strings.Cut(act, "."); ok && kind == OwedComment { + if n, err := strconv.Atoi(seq); err != nil || n < 1 || strconv.Itoa(n) != seq { + return "", "", fmt.Errorf("meshdeliveryclient: the owed act %q is a comment without its sequence, comment. with seq of 1 or more", act) + } + return OwedComment, seq, nil + } + return "", "", fmt.Errorf("meshdeliveryclient: the owed act %q is not status=, view, note or comment.", act) +} + +// OwedCommentAct is the act of the seq-th comment on a head: `comment.`, seq of 1 or more, so a second comment has a key +// of its own and does not overwrite the first. +func OwedCommentAct(seq int) (string, error) { + if seq < 1 { + return "", fmt.Errorf("meshdeliveryclient: a comment's sequence is 1 or more, not %d", seq) + } + return OwedComment + "." + strconv.Itoa(seq), nil +} + +func escapeRepository(repository string) (string, error) { + owner, name, ok := strings.Cut(repository, "/") + if !ok || owner == "" || name == "" || strings.Contains(name, "/") { + return "", fmt.Errorf("meshdeliveryclient: the repository %q is not /, with exactly one slash", repository) + } + return keys.EscapeName(owner) + "/" + keys.EscapeName(name), nil +} + +func unescapeRepository(part string) (string, error) { + owner, name, ok := strings.Cut(part, "/") + if !ok || owner == "" || name == "" || strings.Contains(name, "/") { + return "", fmt.Errorf("meshdeliveryclient: the repository %q is not /, with exactly one slash", part) + } + o, err := keys.UnescapeName(owner) + if err != nil { + return "", err + } + n, err := keys.UnescapeName(name) + if err != nil { + return "", err + } + return o + "/" + n, nil +} + +// OwedKey is the key, in the state owed, of an act the gate owes the forge on a pull request at a head: +// `...`. The number is 0 for a commit no pull request names, and never negative. +func OwedKey(repository string, number int, head, act string) (string, error) { + repo, err := escapeRepository(repository) + if err != nil { + return "", err + } + if number < 0 { + return "", fmt.Errorf("meshdeliveryclient: an owed key's pull request number is 0 (none) or more, not %d", number) + } + if err := keys.CheckName("head", head); err != nil { + return "", err + } + if _, _, err := ParseOwedAct(act); err != nil { + return "", err + } + return repo + "." + strconv.Itoa(number) + "." + head + "." + act, nil +} + +// ParseOwedKey is the inverse of OwedKey; act is whole, as `status=` for a status. +func ParseOwedKey(key string) (repository string, number int, head, act string, err error) { + parts := strings.SplitN(key, ".", 4) // a comment's act holds a dot + if len(parts) != 4 { + return "", 0, "", "", fmt.Errorf("meshdeliveryclient: %q is not an owed key, ...", key) + } + number, convErr := strconv.Atoi(parts[1]) + if convErr != nil { + return "", 0, "", "", fmt.Errorf("meshdeliveryclient: %q is not an owed key: %q is not a pull request number", key, parts[1]) + } + if repository, err = unescapeRepository(parts[0]); err != nil { + return "", 0, "", "", fmt.Errorf("meshdeliveryclient: %q is not an owed key: %w", key, err) + } + if _, err := OwedKey(repository, number, parts[2], parts[3]); err != nil { + return "", 0, "", "", fmt.Errorf("meshdeliveryclient: %q is not an owed key: %w", key, err) + } + return repository, number, parts[2], parts[3], nil +} + +// CheckOwed refuses an owed value the forge bridge could not perform: a status needs its context (one the mesh owes), a +// state of the list and a target; a view and a comment need text; a note needs text and a ref. +func CheckOwed(o seat.OwedByRepositoryNumberHeadActValue) error { + const who = "meshdeliveryclient: " + switch o.Act { + case OwedStatus: + if o.Context == "" { + return fmt.Errorf(who + "an owed status has no context") + } + if err := checkStatusContext(o.Context); err != nil { + return err + } + if o.Target == "" { + return fmt.Errorf(who + "an owed status has no target") + } + for _, s := range statusStates { + if o.State == s { + return nil + } + } + return fmt.Errorf(who+"the owed status state %q is not %s", o.State, strings.Join(statusStates, ", ")) + case OwedView, OwedComment: + if o.Text == "" { + return fmt.Errorf(who+"an owed %s has no text", o.Act) + } + case OwedNote: + if o.Text == "" { + return fmt.Errorf(who + "an owed note has no text") + } + if o.Ref == "" { + return fmt.Errorf(who + "an owed note has no ref") + } + default: + return fmt.Errorf(who+"the owed act %q is not status, view, note or comment", o.Act) + } + return nil +} diff --git a/contracts/go/clients/meshdeliveryclient/client_test.go b/contracts/go/clients/meshdeliveryclient/client_test.go new file mode 100644 index 00000000..71cf9ad1 --- /dev/null +++ b/contracts/go/clients/meshdeliveryclient/client_test.go @@ -0,0 +1,91 @@ +package meshdeliveryclient + +import ( + "strings" + "testing" + + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" +) + +// The keys, ids and acts equal what the gate and the teller have always written (the expected strings are the trunk's own +// output for the same input), so a value written before is read after. +func TestTheAsksAndOwedActsKeepTheirFormsAndRules(t *testing.T) { + heads := []Head{{Repository: "novox/a", Number: 3, Head: "abc"}, {Repository: "novox/b", Number: 1, Head: "def"}} + key, err := MergeAskKey("fix/1.2", heads) + if err != nil || key != "merge.fix/1=2e2.d106c7950d28" { + t.Fatalf("MergeAskKey = %q, %v", key, err) + } + if rev, _ := MergeAskKey("fix/1.2", []Head{heads[1], heads[0]}); rev != key { + t.Errorf("the heads' order changes the key: %q", rev) + } + if kind, group, hash, err := ParseAskKey(key); err != nil || kind != "merge" || group != "fix/1.2" || hash != "d106c7950d28" { + t.Errorf("ParseAskKey = %q %q %q %v", kind, group, hash, err) + } + for _, bad := range []string{"merge.g", "settle.g.d106c7950d28", "merge..d106c7950d28", "merge.g.D106C7950D28", "merge.g.d106"} { + if _, _, _, err := ParseAskKey(bad); err == nil { + t.Errorf("ParseAskKey(%q) reads a wrong key", bad) + } + } + if _, err := MergeAskKey("", heads); err == nil { + t.Error("an empty group keys") + } + if _, err := MergeAskKey("g", nil); err == nil { + t.Error("no heads keys") + } + if id := AskID(key); id != "merge-fix_1_2e2-d106c7950d28-400030402c88" || AskingID(key, 1) != id || AskingID(key, 0) != id { + t.Errorf("AskID = %q", id) + } + if id := AskingID(key, 3); id != "merge-fix_1_2e2-d106c7950d28-3-ea06879e48d8" || len(id) > 64 { + t.Errorf("AskingID(3) = %q", id) + } + if g := SettleGroup([]string{"b", "a"}); g != "settle:7e18f737311b" || g != SettleGroup([]string{"a", "b"}) { + t.Errorf("SettleGroup = %q", g) + } + if AskVerb(SettleGroup([]string{"a"})) != "mesh-delivery.settle" || AskVerb("fix/1") != "mesh-delivery.merge" { + t.Error("AskVerb binds the wrong act") + } + + for act, want := range map[string]string{"status=mesh/check": "novox/a.3.abc.status=mesh/check", "view": "novox/a.3.abc.view", "comment.2": "novox/a.3.abc.comment.2"} { + k, err := OwedKey("novox/a", 3, "abc", act) + if err != nil || k != want { + t.Errorf("OwedKey(%q) = %q, %v", act, k, err) + } + if repo, n, head, a, err := ParseOwedKey(k); err != nil || repo != "novox/a" || n != 3 || head != "abc" || a != act { + t.Errorf("ParseOwedKey(%q) = %q %d %q %q %v", k, repo, n, head, a, err) + } + } + for _, act := range []string{"status", "status=mesh/other", "view=x", "comment", "comment.0", "comment.01", "other"} { + if _, err := OwedKey("novox/a", 3, "abc", act); err == nil { + t.Errorf("OwedKey takes the act %q", act) + } + } + if _, err := OwedKey("novox", 0, "abc", "view"); err == nil { + t.Error("a repository with no slash keys") + } + if _, err := OwedKey("novox/a", -1, "abc", "view"); err == nil { + t.Error("a negative number keys") + } + if act, err := OwedCommentAct(2); err != nil || act != "comment.2" { + t.Errorf("OwedCommentAct = %q, %v", act, err) + } + + type owed = seat.OwedByRepositoryNumberHeadActValue + for name, c := range map[string]struct { + v owed + says string + }{ + "a good status": {owed{Act: "status", Context: StatusContextCheck, Target: "http://x", State: "success"}, ""}, + "a good note": {owed{Act: "note", Text: "t", Ref: "mesh-plan"}, ""}, + "a status no target": {owed{Act: "status", Context: StatusContextCheck, State: "success"}, "no target"}, + "a status wrong state": {owed{Act: "status", Context: StatusContextCheck, Target: "x", State: "done"}, "state"}, + "a status no context": {owed{Act: "status", Target: "x", State: "success"}, "no context"}, + "a view no text": {owed{Act: "view"}, "no text"}, + "a note no ref": {owed{Act: "note", Text: "t"}, "no ref"}, + "an unknown act": {owed{Act: "merge"}, "not status"}, + } { + err := CheckOwed(c.v) + if (c.says == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.says)) { + t.Errorf("%s: CheckOwed = %v, want %q", name, err, c.says) + } + } +} diff --git a/contracts/go/clients/meshdeliveryclient/go.mod b/contracts/go/clients/meshdeliveryclient/go.mod new file mode 100644 index 00000000..975c6294 --- /dev/null +++ b/contracts/go/clients/meshdeliveryclient/go.mod @@ -0,0 +1,17 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef +) + +require ( + github.com/klauspost/compress v1.20.1 // indirect + github.com/nats-io/nats.go v1.54.0 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/sys v0.48.0 // indirect +) diff --git a/contracts/go/clients/meshdeliveryclient/go.sum b/contracts/go/clients/meshdeliveryclient/go.sum new file mode 100644 index 00000000..c811fda7 --- /dev/null +++ b/contracts/go/clients/meshdeliveryclient/go.sum @@ -0,0 +1,18 @@ +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a h1:E9ERPqR4uZXypudHGxv2/wNIOYcder9torVqJwJq/DI= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-20261105000000-000000000000 h1:BTqpa7dCCTQH9m08Jle8KVFwyIHiHPSUbwWxuqSg2yA= +git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-20261105000000-000000000000/go.mod h1:pPxwhTMb3hIk7UUrBicJnGQCqfTEoBGJvNFspEK2U18= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef h1:/Jic5ho0xz32Z9DH2NSUqVxCF1KyfIOWuRTrX1aLjus= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef/go.mod h1:2wHG7ntLCG8Tpm/SCfgp524a5emUJm1gAC4KrRYsU9s= +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/contracts/module.json b/contracts/module.json index a239ea00..d80c383c 100644 --- a/contracts/module.json +++ b/contracts/module.json @@ -121,6 +121,15 @@ ], "series": "0.1" }, + { + "name": "go-meshdeliveryclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/meshdeliveryclient" + ], + "series": "0.1" + }, { "name": "go-meshdeliverycheckerclient", "kind": "package", From c183f170cc60a0cc73ee58ea45dfa24e4d9a1542 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:52:26 +0100 Subject: [PATCH 4/5] =?UTF-8?q?mesh-delivery-assigner:=20the=20asks=20pack?= =?UTF-8?q?age=20(the=20keys=20of=20its=20asks=20and=20their=20value)=20is?= =?UTF-8?q?=20the=20module's=20own,=20a=20package=20artifact;=20the=20view?= =?UTF-8?q?'s=20answer=20key=20and=20fit=20in=20viewstate=20(ADR=200634=20?= =?UTF-8?q?=C2=A71,=20=C2=A74,=20ADR=200635=20=C2=A75;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- modules/mesh-delivery-assigner/asks/asks.go | 257 ++++++++++++++++++ .../mesh-delivery-assigner/asks/asks_test.go | 85 ++++++ modules/mesh-delivery-assigner/asks/go.mod | 13 + modules/mesh-delivery-assigner/module.json | 9 + .../mesh-delivery-view/viewstate/answerkey.go | 48 ++++ modules/mesh-delivery-view/viewstate/go.mod | 2 + .../viewstate/viewstate_test.go | 28 ++ 7 files changed, 442 insertions(+) create mode 100644 modules/mesh-delivery-assigner/asks/asks.go create mode 100644 modules/mesh-delivery-assigner/asks/asks_test.go create mode 100644 modules/mesh-delivery-assigner/asks/go.mod create mode 100644 modules/mesh-delivery-view/viewstate/answerkey.go diff --git a/modules/mesh-delivery-assigner/asks/asks.go b/modules/mesh-delivery-assigner/asks/asks.go new file mode 100644 index 00000000..d255408b --- /dev/null +++ b/modules/mesh-delivery-assigner/asks/asks.go @@ -0,0 +1,257 @@ +// Package asks holds the layout of the assigner's state `asks`: the keys of the asks the assigner puts to a person and the +// value it keeps at each, which the assigner writes and the view and the teller read (novox/hq ADR 0634 §1, ADR 0635 §5). It +// is the assigner's own published package. The gate's merge ask (`merge..`) needs proof of who answered +// (ADR 0421 §1), so it is answered on the phone alone until issue 953 closes. The assigner's asks need no such proof, and the +// view may answer them: the hold of a fingerprint whose health check failed (`hold..`: release it or +// keep it held), the restore of a data module set back (`restore...`: restore and lose newer +// writes, or leave it stopped, ADR 0429 §6), a backup that failed before an apply, a move a person must allow for a module of +// policy record, and the authorising ask of a proposal of placement. +package asks + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "git.novox.be/novox/mesh-delivery/sdk/kept" + "git.novox.be/novox/mesh-sdk/go/keys" +) + +const ( + // AskKindHold is the kind of the assigner's ask about a held fingerprint. + AskKindHold = "hold" + // AskKindRestore is the kind of the assigner's ask about a data module set back. + AskKindRestore = "restore" + // AskKindBackup is the kind of the assigner's ask about a backup before an apply that failed: back up again, or go on + // without one (novox/hq ADR 0539 clause 16). + AskKindBackup = "backup" + // AskKindRecord is the kind of the assigner's ask about a move a person must allow, for a module of policy record: a + // composition judged to recreate a container or move data, or one that cannot be judged (ADR 0539 clause 14). + AskKindRecord = "record" + // AskKindPlacement is the kind of the authorising ask about a proposal of placement (novox/hq ADR 0567 §5): + // `placement.`. The kind needs no proof, as the bus's put does not: its yes is an Approve, which only a + // channel proving who answered takes and the view never does (ViewAnswerFits). + AskKindPlacement = "placement" +) + +// AskKey is an ask's key read whole: its Kind, and the parts its kind has: a merge's Group and heads Hash, a hold's Module +// and fingerprint FP, a restore's and a backup's Node, Module and FP, a record's Node, Module and the digest Composed of the +// composition it would allow, a placement's Proposal. +type AskKey struct { + Proposal string + Kind string + Group string + Hash string + Node string + Module string + FP string + Composed string +} + +// AskNeedsProof says whether an ask of this kind must prove who answered it, so the view may not answer it: the merge's +// Approve does (ADR 0421 §1, issue 953), the assigner's hold and restore do not. An unknown kind is refused. +func AskNeedsProof(kind string) (bool, error) { + switch kind { + case meshdeliveryclient.AskKindMerge: + return true, nil + case AskKindHold, AskKindRestore, AskKindBackup, AskKindRecord, AskKindPlacement: + return false, nil + } + return false, fmt.Errorf("asks: %q is not a kind of ask, %s, %s, %s, %s, %s or %s", kind, meshdeliveryclient.AskKindMerge, AskKindHold, + AskKindRestore, AskKindBackup, AskKindRecord, AskKindPlacement) +} + +// HoldAskKey is the key of the ask about a module's held fingerprint: `hold..`. +func HoldAskKey(module, fingerprint string) (string, error) { + key, err := builds.BuilderBuildKey(module, fingerprint) + if err != nil { + return "", err + } + return AskKindHold + "." + key, nil +} + +// RestoreAskKey is the key of the ask about a data module set back on a node from a fingerprint: +// `restore...`. +func RestoreAskKey(node, module, fingerprint string) (string, error) { + if err := keys.CheckName("node", node); err != nil { + return "", err + } + key, err := builds.BuilderBuildKey(module, fingerprint) + if err != nil { + return "", err + } + return AskKindRestore + "." + node + "." + key, nil +} + +// BackupAskKey is the key of the ask about a module's failed backup on a node before it applies a fingerprint: +// `backup...`. +func BackupAskKey(node, module, fingerprint string) (string, error) { + part, err := meshcatalogclient.KeyPart(fingerprint) + if err != nil { + return "", err + } + key, err := nodeengineclient.ComposedKey(node, module, part) + if err != nil { + return "", err + } + return AskKindBackup + "." + key, nil +} + +// PlacementAskKey is the key of the authorising ask about a proposal of placement: `placement.` (ADR 0567 §5). +func PlacementAskKey(proposal string) (string, error) { + if err := meshmoduleassignerclient.CheckProposalID(proposal); err != nil { + return "", err + } + return AskKindPlacement + "." + proposal, nil +} + +// RecordAskKey is the key of the ask about a move of a module on a node to the composition whose digest it names: +// `record...`, so a new revision at the same fingerprint is asked again (ADR 0539 clause 14). +func RecordAskKey(node, module, composed string) (string, error) { + have, err := nodeengineclient.HaveKey(node, module) + if err != nil { + return "", err + } + if err := kept.CheckDigest(composed, "a record ask's composition digest"); err != nil { + return "", err + } + return AskKindRecord + "." + have + "." + strings.TrimPrefix(composed, "sha256:"), nil +} + +// ParseAnyAskKey reads an ask key of any kind (a merge, hold, restore, backup, record or placement ask); it refuses an unknown +// kind and a key whose parts do not fit its kind, naming the key. +func ParseAnyAskKey(key string) (AskKey, error) { + kind, rest, _ := strings.Cut(key, ".") + refused := func(err error) (AskKey, error) { + return AskKey{}, fmt.Errorf("asks: %q is not an ask key: %w", key, err) + } + switch kind { + case meshdeliveryclient.AskKindMerge: + _, group, hash, err := meshdeliveryclient.ParseAskKey(key) + if err != nil { + return AskKey{}, err + } + return AskKey{Kind: kind, Group: group, Hash: hash}, nil + case AskKindHold: + module, fp, err := builds.ParseBuilderBuildKey(rest) + if err != nil { + return refused(err) + } + return AskKey{Kind: kind, Module: module, FP: fp}, nil + case AskKindRestore: + node, build, ok := strings.Cut(rest, ".") + if !ok { + return refused(fmt.Errorf("a restore ask is restore...")) + } + if err := keys.CheckName("node", node); err != nil { + return refused(err) + } + module, fp, err := builds.ParseBuilderBuildKey(build) + if err != nil { + return refused(err) + } + return AskKey{Kind: kind, Node: node, Module: module, FP: fp}, nil + case AskKindBackup: + node, module, part, isNode, err := nodeengineclient.ParseComposedKey(rest) + if err != nil || isNode { + return refused(fmt.Errorf("a backup ask is backup...")) + } + fp, err := meshcatalogclient.ParseKeyPart(part) + if err != nil { + return refused(err) + } + return AskKey{Kind: kind, Node: node, Module: module, FP: fp}, nil + case AskKindPlacement: + if err := meshmoduleassignerclient.CheckProposalID(rest); err != nil { + return refused(err) + } + return AskKey{Kind: kind, Proposal: rest}, nil + case AskKindRecord: + parts := strings.Split(rest, ".") + if len(parts) != 3 { + return refused(fmt.Errorf("a record ask is record...")) + } + node, module, isNode, err := nodeengineclient.ParseHaveKey(parts[0] + "." + parts[1]) + if err != nil || isNode { + return refused(fmt.Errorf("a record ask is record...")) + } + composed := "sha256:" + parts[2] + if err := kept.CheckDigest(composed, "a record ask's composition digest"); err != nil { + return refused(err) + } + return AskKey{Kind: kind, Node: node, Module: module, Composed: composed}, nil + } + _, err := AskNeedsProof(kind) + return refused(err) +} + +// Ask is the value of a key in the assigner's state asks: the router's ask whole, as the assigner composes it, so the teller +// publishes it unchanged. Asking counts the times the assigner wrote the ask at this key, as the gate's ask does: 0 or 1 for +// the first, n for the n-th, and the ask's id is meshdeliveryclient.AskingID(key, Asking), so each asking has its own id and +// an answer to an earlier asking is never taken for a later one. The ask is the operator-channel seat's as its JSON: its id, +// its options and its digest are checked by the assigner and the teller through operator-channel's client (novox/hq ADR +// 0627 §4). +type Ask struct { + V int `json:"v"` + Ask json.RawMessage `json:"ask"` + Asking int `json:"asking,omitempty"` +} + +func (a Ask) check(key string) error { + ask, err := ParseAnyAskKey(key) + if err != nil { + return err + } + if proof, _ := AskNeedsProof(ask.Kind); proof { + return fmt.Errorf("asks: the assigner asks no %s ask: it needs proof of who answered", ask.Kind) + } + if a.Asking < 0 { + return fmt.Errorf("asks: the ask's asking %d is negative", a.Asking) + } + var fields map[string]json.RawMessage + if len(bytes.TrimSpace(a.Ask)) == 0 || json.Unmarshal(a.Ask, &fields) != nil || fields == nil { + return fmt.Errorf("asks: the value carries no router's ask") + } + return nil +} + +// EncodeAsk is a, the ask at key, as JSON at keys.Version; it refuses a key of a kind that needs proof or of no kind, a +// negative asking, and a value carrying no router's ask. +func EncodeAsk(key string, a Ask) ([]byte, error) { + if err := a.check(key); err != nil { + return nil, err + } + a.V = keys.Version + return json.Marshal(a) +} + +// DecodeAsk reads the ask at key; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeAsk refuses. +func DecodeAsk(key string, data []byte) (Ask, error) { + var a Ask + if err := kept.Decode(data, &a); err != nil { + return Ask{}, err + } + if err := a.check(key); err != nil { + return Ask{}, err + } + return a, nil +} + +// SameAsk says whether two asks are the same value: the same version, asking and router's ask, its JSON compared compacted. +func SameAsk(a, b Ask) bool { + return a.V == b.V && a.Asking == b.Asking && bytes.Equal(compacted(a.Ask), compacted(b.Ask)) +} + +func compacted(raw json.RawMessage) []byte { + var b bytes.Buffer + if json.Compact(&b, raw) != nil { + return raw + } + return b.Bytes() +} diff --git a/modules/mesh-delivery-assigner/asks/asks_test.go b/modules/mesh-delivery-assigner/asks/asks_test.go new file mode 100644 index 00000000..bfa79231 --- /dev/null +++ b/modules/mesh-delivery-assigner/asks/asks_test.go @@ -0,0 +1,85 @@ +package asks + +import ( + "errors" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// The keys equal what the assigner has always written (the expected strings are the trunk's own output for the same input), +// and each reads back to its parts. +func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) { + fp := "src2:" + strings.Repeat("ab", 32) + hex := strings.Repeat("ab", 32) + digest := "sha256:" + strings.Repeat("cd", 32) + hold, _ := HoldAskKey("gitea", fp) + restore, _ := RestoreAskKey("n1", "gitea", fp) + backup, _ := BackupAskKey("n1", "novox.be", fp) + placement, _ := PlacementAskKey("p-20261028t183000-1a2b3c") + record, _ := RecordAskKey("n1", "gitea", digest) + for key, want := range map[string]struct { + got string + back AskKey + }{ + "hold": {hold, AskKey{Kind: "hold", Module: "gitea", FP: fp}}, + "restore": {restore, AskKey{Kind: "restore", Node: "n1", Module: "gitea", FP: fp}}, + "backup": {backup, AskKey{Kind: "backup", Node: "n1", Module: "novox.be", FP: fp}}, + "placement": {placement, AskKey{Kind: "placement", Proposal: "p-20261028t183000-1a2b3c"}}, + "record": {record, AskKey{Kind: "record", Node: "n1", Module: "gitea", Composed: digest}}, + } { + back, err := ParseAnyAskKey(want.got) + if err != nil || back != want.back { + t.Errorf("%s: ParseAnyAskKey(%q) = %+v, %v; want %+v", key, want.got, back, err, want.back) + } + } + for got, want := range map[string]string{hold: "hold.gitea." + hex, restore: "restore.n1.gitea." + hex, backup: "backup.n1.novox=2ebe." + hex, + placement: "placement.p-20261028t183000-1a2b3c", record: "record.n1.gitea." + strings.Repeat("cd", 32)} { + if got != want { + t.Errorf("key %q, want %q", got, want) + } + } + merge, err := ParseAnyAskKey("merge.fix/1=2e2.d106c7950d28") + if err != nil || merge.Kind != "merge" || merge.Group != "fix/1.2" || merge.Hash != "d106c7950d28" { + t.Errorf("a merge ask key: %+v, %v", merge, err) + } + for _, bad := range []string{"", "other.x", "hold.gitea", "hold.gitea.src2:" + hex, "restore.n1", "backup.n1.node", "backup.n1.gitea." + hex[:60], + "placement.AB", "record.n1.gitea." + hex[:10], "record.n1.node." + hex, "merge.g"} { + if _, err := ParseAnyAskKey(bad); err == nil { + t.Errorf("ParseAnyAskKey(%q) reads a wrong key", bad) + } + } + if proof, err := AskNeedsProof("merge"); err != nil || !proof { + t.Error("a merge ask needs proof") + } + if proof, err := AskNeedsProof("hold"); err != nil || proof { + t.Error("a hold ask needs no proof") + } + if _, err := AskNeedsProof("view"); err == nil { + t.Error("an unknown kind of ask is taken") + } + + // The value: the router's ask whole; an ask of a kind that needs proof, a negative asking and no ask are refused. + good := Ask{Ask: []byte(`{"id":"x"}`), Asking: 2} + data, err := EncodeAsk(hold, good) + if err != nil { + t.Fatalf("EncodeAsk: %v", err) + } + back, err := DecodeAsk(hold, data) + if err != nil || !SameAsk(back, Ask{V: keys.Version, Ask: []byte(` {"id": "x"} `), Asking: 2}) { + t.Errorf("DecodeAsk = %+v, %v", back, err) + } + for name, c := range map[string]struct { + key string + a Ask + }{"a merge ask": {"merge.g.d106c7950d28", good}, "a negative asking": {hold, Ask{Ask: good.Ask, Asking: -1}}, "no ask": {hold, Ask{}}, + "a null ask": {hold, Ask{Ask: []byte("null")}}, "an unknown kind": {"other.x", good}} { + if _, err := EncodeAsk(c.key, c.a); err == nil { + t.Errorf("EncodeAsk takes %s", name) + } + } + if _, err := DecodeAsk(hold, []byte(`{"v":2,"ask":{}}`)); !errors.Is(err, keys.ErrUnknownVersion) { + t.Errorf("DecodeAsk of another version = %v", err) + } +} diff --git a/modules/mesh-delivery-assigner/asks/go.mod b/modules/mesh-delivery-assigner/asks/go.mod new file mode 100644 index 00000000..fbda6c1d --- /dev/null +++ b/modules/mesh-delivery-assigner/asks/go.mod @@ -0,0 +1,13 @@ +module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0 + git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0 + git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0 + git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0 + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0 + git.novox.be/novox/mesh-delivery/sdk v0.0.0 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f +) diff --git a/modules/mesh-delivery-assigner/module.json b/modules/mesh-delivery-assigner/module.json index 423a565a..97a163ae 100644 --- a/modules/mesh-delivery-assigner/module.json +++ b/modules/mesh-delivery-assigner/module.json @@ -158,6 +158,15 @@ "MESH_VIEW_CALLERS_SEAT_HOLDERS": "node-tools", "MESH_VIEW_CALLERS_WANTS": "mesh-grants" } + }, + { + "name": "asks", + "kind": "package", + "language": "go", + "paths": [ + "asks" + ], + "series": "0.1" } ] } diff --git a/modules/mesh-delivery-view/viewstate/answerkey.go b/modules/mesh-delivery-view/viewstate/answerkey.go new file mode 100644 index 00000000..7d7d8a09 --- /dev/null +++ b/modules/mesh-delivery-view/viewstate/answerkey.go @@ -0,0 +1,48 @@ +package viewstate + +import ( + "fmt" + "strings" + + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" +) + +const viewWord = "view" + +// ViewAnswerKey is the key, in the state answers, of the view's answer to the ask at askKey: `.view`. An ask that needs +// proof of who answered is refused with its reason: it is answered on the phone (issue 953). +func ViewAnswerKey(askKey string) (string, error) { + ask, err := asks.ParseAnyAskKey(askKey) + if err != nil { + return "", err + } + if proof, _ := asks.AskNeedsProof(ask.Kind); proof { + return "", fmt.Errorf("viewstate: the ask %s needs proof of who answered: answer it on the phone (issue 953)", askKey) + } + return askKey + "." + viewWord, nil +} + +// ParseViewAnswerKey is the inverse of ViewAnswerKey: the ask's key. It refuses a key without `.view`, an ask key of a kind that +// needs proof, and a key of another state, naming it. +func ParseViewAnswerKey(key string) (askKey string, err error) { + askKey, ok := strings.CutSuffix(key, "."+viewWord) + if !ok { + return "", fmt.Errorf("viewstate: %q is not a view answer key, .view", key) + } + if _, err := ViewAnswerKey(askKey); err != nil { + return "", fmt.Errorf("viewstate: %q is not a view answer key: %w", key, err) + } + return askKey, nil +} + +// ViewAnswerFits says whether the view's answer a answers the ask at key as it stands now: it refuses an answer to another +// asking of the ask (a stale answer to an earlier writing of the same key), naming both. Whether the ask offers the option +// chosen, and at the level acknowledge only (an option that authorises is answered on a channel that proves who answered, +// never in the view, which proves nobody), the teller reads from the ask through operator-channel's client. +func ViewAnswerFits(a ViewAnswer, key string, ask asks.Ask) error { + if want := meshdeliveryclient.AskingID(key, ask.Asking); a.Asking != want { + return fmt.Errorf("viewstate: the view's answer is to the asking %s, and the ask stands at the asking %s", a.Asking, want) + } + return nil +} diff --git a/modules/mesh-delivery-view/viewstate/go.mod b/modules/mesh-delivery-view/viewstate/go.mod index 5a17cb64..9c9477e4 100644 --- a/modules/mesh-delivery-view/viewstate/go.mod +++ b/modules/mesh-delivery-view/viewstate/go.mod @@ -3,6 +3,8 @@ module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate go 1.26.0 require ( + git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0 + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks v0.0.0 git.novox.be/novox/mesh-delivery/sdk v0.0.0 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f ) diff --git a/modules/mesh-delivery-view/viewstate/viewstate_test.go b/modules/mesh-delivery-view/viewstate/viewstate_test.go index c35726bb..9863ec8d 100644 --- a/modules/mesh-delivery-view/viewstate/viewstate_test.go +++ b/modules/mesh-delivery-view/viewstate/viewstate_test.go @@ -4,6 +4,8 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" "git.novox.be/novox/mesh-sdk/go/keys" ) @@ -72,6 +74,32 @@ func TestReachValuesDifferingInAnyPartAreNotTheSame(t *testing.T) { } func TestViewAnswerRoundTrip(t *testing.T) { + t.Run("the key and the asking", func(t *testing.T) { + fp := "src2:" + strings.Repeat("ab", 32) + hold, _ := asks.HoldAskKey("gitea", fp) + key, err := ViewAnswerKey(hold) + if err != nil || key != hold+".view" { + t.Fatalf("ViewAnswerKey = %q, %v", key, err) + } + if back, err := ParseViewAnswerKey(key); err != nil || back != hold { + t.Errorf("ParseViewAnswerKey = %q, %v", back, err) + } + for _, bad := range []string{hold, "merge.g.d106c7950d28.view", "other.x.view"} { + if _, err := ParseViewAnswerKey(bad); err == nil { + t.Errorf("ParseViewAnswerKey(%q) reads a wrong key", bad) + } + } + if _, err := ViewAnswerKey("merge.g.d106c7950d28"); err == nil || !strings.Contains(err.Error(), "proof") { + t.Errorf("a merge ask is answered by the view: %v", err) + } + first := ViewAnswer{Asking: meshdeliveryclient.AskingID(hold, 0), Option: "release"} + if err := ViewAnswerFits(first, hold, asks.Ask{}); err != nil { + t.Errorf("an answer to the first asking is refused: %v", err) + } + if err := ViewAnswerFits(first, hold, asks.Ask{Asking: 2}); err == nil { + t.Error("an answer to an earlier asking is taken for the later one") + } + }) a := ViewAnswer{Asking: "hold-mesh-mail-aaaa-0123456789ab", Option: "release", Words: "fixed by #12"} data, err := EncodeViewAnswer(a) if err != nil { From f2c61815f8653d37cda43bb7a006998be4dff1a0 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:55:35 +0100 Subject: [PATCH 5/5] =?UTF-8?q?mesh-delivery-contracts:=20the=20build=20se?= =?UTF-8?q?ats'=20part=20types=20Fetch,=20the=20toolchain,=20the=20artifac?= =?UTF-8?q?ts,=20the=20check's=20levels=20and=20a=20step's=20result,=20and?= =?UTF-8?q?=20marks=20the=20times=20date-time;=20fixtures=20for=20a=20chec?= =?UTF-8?q?k=20ask=20and=20the=20three=20kinds=20of=20result=20(ADR=200634?= =?UTF-8?q?=20=C2=A76;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../reference/reference_test.go | 8 +- contracts/go/seats/nodebuildgo/contract.go | 226 ++++---- .../go/seats/nodebuildgo/contract_test.go | 8 + contracts/go/seats/nodebuildimage/contract.go | 226 ++++---- contracts/go/seats/nodebuildnode/contract.go | 226 ++++---- contracts/parts/build-step.json | 513 +++++++++++------- .../seats/node-build-go/fixtures/states.json | 140 +++++ contracts/ts/seats/nodebuildgo/src/index.ts | 22 +- .../seats/nodebuildgo/src/parts/build-step.ts | 190 ++++--- .../ts/seats/nodebuildimage/src/index.ts | 22 +- .../nodebuildimage/src/parts/build-step.ts | 190 ++++--- contracts/ts/seats/nodebuildnode/src/index.ts | 22 +- .../nodebuildnode/src/parts/build-step.ts | 190 ++++--- 13 files changed, 1243 insertions(+), 740 deletions(-) diff --git a/contracts/go/clients/containerimageregistryclient/reference/reference_test.go b/contracts/go/clients/containerimageregistryclient/reference/reference_test.go index d707c9e4..60823b7d 100644 --- a/contracts/go/clients/containerimageregistryclient/reference/reference_test.go +++ b/contracts/go/clients/containerimageregistryclient/reference/reference_test.go @@ -7,7 +7,7 @@ import ( var digest = "sha256:" + strings.Repeat("d", 64) -var seats = Seats{Image: "container-image-registry", Object: "oci-artifact-registry"} +var seats = Seats{Image: "container-image-registry", Object: "object-registry"} func TestAReferenceIsRecordedWithoutTheStoresAddress(t *testing.T) { cases := map[string]string{ @@ -21,7 +21,7 @@ func TestAReferenceIsRecordedWithoutTheStoresAddress(t *testing.T) { "https://registry.example/v2/gitea/config/blobs/" + digest: "https://registry.example/v2/gitea/config/blobs/" + digest, // The form a registry seat answers is recorded as it is (ADR 0605 §9). "container-image-registry/gitea/server@" + digest: "container-image-registry/gitea/server@" + digest, - "oci-artifact-registry/gitea/config@" + digest: "oci-artifact-registry/gitea/config@" + digest, + "object-registry/gitea/config@" + digest: "object-registry/gitea/config@" + digest, } for announced, want := range cases { if got := seats.Recorded(announced); got != want { @@ -33,7 +33,7 @@ func TestAReferenceIsRecordedWithoutTheStoresAddress(t *testing.T) { ArtifactStoreScheme + "gitea/server@" + digest: "10.0.0.1:5000/gitea/server@" + digest, "container-image-registry/gitea/server@" + digest: "10.0.0.1:5000/gitea/server@" + digest, ArtifactStoreScheme + "gitea/config/blobs/" + digest: "http://10.0.0.1:5000/v2/gitea/config/blobs/" + digest, - "oci-artifact-registry/gitea/config@" + digest: "http://10.0.0.1:5000/v2/gitea/config/blobs/" + digest, + "object-registry/gitea/config@" + digest: "http://10.0.0.1:5000/v2/gitea/config/blobs/" + digest, "registry.example/gitea/server@" + digest: "registry.example/gitea/server@" + digest, "mesh-artifact-store/gitea/server@" + digest: "mesh-artifact-store/gitea/server@" + digest, // An upstream copy under the holder's own naming, in both forms. @@ -54,7 +54,7 @@ func TestAReferenceIsRecordedWithoutTheStoresAddress(t *testing.T) { // The holder's naming of an upstream copy reads through the image seat only, and InUpstream reads its host and path. for reference, want := range map[string]bool{ "container-image-registry/upstream/quay.io:443/org/app@" + digest: true, - "oci-artifact-registry/upstream/quay.io:443/org/app@" + digest: false, + "object-registry/upstream/quay.io:443/org/app@" + digest: false, "container-image-registry/upstream/org/app@" + digest: false, "container-image-registry/upstream/quay.io/app": false, } { diff --git a/contracts/go/seats/nodebuildgo/contract.go b/contracts/go/seats/nodebuildgo/contract.go index 59b72a28..a734103f 100644 --- a/contracts/go/seats/nodebuildgo/contract.go +++ b/contracts/go/seats/nodebuildgo/contract.go @@ -187,26 +187,27 @@ func DecodeToolchainsByNodeLanguageValue(raw []byte) (ToolchainsByNodeLanguageVa return v, nil } -// BuildAnswerArtifactsItem is a type of the contract. -type BuildAnswerArtifactsItem struct { - Artifact string `json:"artifact"` - Digest string `json:"digest"` - Reference string `json:"reference,omitempty"` -} - -// BuildAnswerToolchain is a type of the contract. -type BuildAnswerToolchain struct { - Language string `json:"language"` - Recipe string `json:"recipe"` - Version string `json:"version"` +// AskArtifact is one artifact of a build ask: built from the fetched tree, or copied from an upstream pin. +type AskArtifact struct { + Artifact string `json:"artifact"` + From string `json:"from,omitempty"` + Seat string `json:"seat,omitempty"` + Version string `json:"version,omitempty"` } // BuildAnswer is a type of the contract. type BuildAnswer struct { - Artifacts []BuildAnswerArtifactsItem `json:"artifacts"` - Imports []string `json:"imports,omitempty"` - OutOfStep []string `json:"out-of-step,omitempty"` - Toolchain *BuildAnswerToolchain `json:"toolchain,omitempty"` + Artifacts []BuiltArtifact `json:"artifacts"` + Imports []string `json:"imports,omitempty"` + OutOfStep []string `json:"out-of-step,omitempty"` + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` +} + +// BuiltArtifact is one artifact a build made: its digest and the reference it was published under. +type BuiltArtifact struct { + Artifact string `json:"artifact"` + Digest string `json:"digest"` + Reference string `json:"reference,omitempty"` } // Capacity is how many steps the node runs at once. @@ -229,56 +230,10 @@ type Capacity struct { Why string `json:"why"` } -// CheckAnswerLevelsItemDigestNeedsItem is a type of the contract. -type CheckAnswerLevelsItemDigestNeedsItem struct { - Needs []string `json:"needs,omitempty"` - Package string `json:"package,omitempty"` -} - -// CheckAnswerLevelsItemDigestPackagesItem is a type of the contract. -type CheckAnswerLevelsItemDigestPackagesItem struct { - Digest string `json:"digest"` - Package string `json:"package"` -} - -// CheckAnswerLevelsItemDigest is a type of the contract. -type CheckAnswerLevelsItemDigest struct { - Beside []string `json:"beside,omitempty"` - JudgedBy string `json:"judged_by,omitempty"` - Level string `json:"level,omitempty"` - Needs []CheckAnswerLevelsItemDigestNeedsItem `json:"needs,omitempty"` - Packages []CheckAnswerLevelsItemDigestPackagesItem `json:"packages,omitempty"` - WithoutTests []string `json:"without_tests,omitempty"` -} - -// CheckAnswerLevelsItemKeptItem is a type of the contract. -type CheckAnswerLevelsItemKeptItem struct { - Digest string `json:"digest,omitempty"` - Package string `json:"package,omitempty"` - Tests int64 `json:"tests,omitempty"` -} - -// CheckAnswerLevelsItemUnmetItem is a type of the contract. -type CheckAnswerLevelsItemUnmetItem struct { - Needs []string `json:"needs,omitempty"` - Package string `json:"package,omitempty"` -} - -// CheckAnswerLevelsItem is a type of the contract. -type CheckAnswerLevelsItem struct { - Digest *CheckAnswerLevelsItemDigest `json:"digest,omitempty"` - Judged string `json:"judged,omitempty"` - Kept []CheckAnswerLevelsItemKeptItem `json:"kept,omitempty"` - Level string `json:"level"` - Tests []any `json:"tests,omitempty"` - Unmet []CheckAnswerLevelsItemUnmetItem `json:"unmet,omitempty"` -} - // CheckAnswer is a type of the contract. type CheckAnswer struct { - // what the build module fetches: the repository, commit and directory, as the indexer states it - Fetch any `json:"fetch"` - Levels []CheckAnswerLevelsItem `json:"levels,omitempty"` + Fetch Fetch `json:"fetch"` + Levels []LevelAnswer `json:"levels,omitempty"` } // ComposeAnswer is a type of the contract. @@ -289,6 +244,77 @@ type ComposeAnswer struct { Verdict any `json:"verdict,omitempty"` } +// FetchBesideItem is a type of the contract. +type FetchBesideItem struct { + Commit string `json:"commit"` + Repository string `json:"repository"` +} + +// Fetch is what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside. +type Fetch struct { + // the trunk commit a merge preview merges onto + Base string `json:"base,omitempty"` + // the sibling repositories placed beside the fetched tree + Beside []FetchBesideItem `json:"beside,omitempty"` + // the full commit + Commit string `json:"commit"` + // the module's directory in the repository ("." for its root), with paths + Dir string `json:"dir,omitempty"` + // the version (a full commit) of each mesh package to build against, by its module path + Packages map[string]string `json:"packages,omitempty"` + // the paths the module names, relative to dir + Paths []string `json:"paths,omitempty"` + // the pull request's ref, when only its head carries the commit + Ref string `json:"ref,omitempty"` + // / + Repository string `json:"repository"` +} + +// LevelAnswerDigestNeedsItem is a type of the contract. +type LevelAnswerDigestNeedsItem struct { + Needs []string `json:"needs,omitempty"` + Package string `json:"package,omitempty"` +} + +// LevelAnswerDigestPackagesItem is a type of the contract. +type LevelAnswerDigestPackagesItem struct { + Digest string `json:"digest"` + Package string `json:"package"` +} + +// LevelAnswerDigest is a type of the contract. +type LevelAnswerDigest struct { + Beside []string `json:"beside,omitempty"` + JudgedBy string `json:"judged_by,omitempty"` + Level string `json:"level,omitempty"` + Needs []LevelAnswerDigestNeedsItem `json:"needs,omitempty"` + Packages []LevelAnswerDigestPackagesItem `json:"packages,omitempty"` + WithoutTests []string `json:"without_tests,omitempty"` +} + +// LevelAnswerKeptItem is a type of the contract. +type LevelAnswerKeptItem struct { + Digest string `json:"digest,omitempty"` + Package string `json:"package,omitempty"` + Tests int64 `json:"tests,omitempty"` +} + +// LevelAnswerUnmetItem is a type of the contract. +type LevelAnswerUnmetItem struct { + Needs []string `json:"needs,omitempty"` + Package string `json:"package,omitempty"` +} + +// LevelAnswer is one level of a check ask: the digest's answer, the kept verdicts taken, one test result per test step run, and the packages the node could not test. +type LevelAnswer struct { + Digest *LevelAnswerDigest `json:"digest,omitempty"` + Judged string `json:"judged,omitempty"` + Kept []LevelAnswerKeptItem `json:"kept,omitempty"` + Level string `json:"level"` + Tests []any `json:"tests,omitempty"` + Unmet []LevelAnswerUnmetItem `json:"unmet,omitempty"` +} + // Pause is whether the node takes no new ask. type Pause struct { // when (RFC 3339 time) @@ -308,10 +334,9 @@ type Step struct { Owner string `json:"owner,omitempty"` Phase string `json:"phase,omitempty"` // when (RFC 3339 time) - PhaseAt string `json:"phase-at,omitempty"` - Reason string `json:"reason,omitempty"` - // what the step answered - Result any `json:"result,omitempty"` + PhaseAt string `json:"phase-at,omitempty"` + Reason string `json:"reason,omitempty"` + Result *StepResult `json:"result,omitempty"` // when (RFC 3339 time) Started string `json:"started"` // running, passed, failed or refused @@ -322,37 +347,20 @@ type Step struct { What string `json:"what,omitempty"` } -// StepAskBuildArtifactsItem is a type of the contract. -type StepAskBuildArtifactsItem struct { - Artifact string `json:"artifact"` - From string `json:"from,omitempty"` - Seat string `json:"seat,omitempty"` - Version string `json:"version,omitempty"` -} - -// StepAskBuildToolchain is a type of the contract. -type StepAskBuildToolchain struct { - Language string `json:"language"` - Recipe string `json:"recipe"` - Version string `json:"version"` -} - // StepAskBuild is a type of the contract. type StepAskBuild struct { - Artifacts []StepAskBuildArtifactsItem `json:"artifacts"` - // what the build module fetches: the repository, commit and directory, as the indexer states it - Fetch any `json:"fetch,omitempty"` - Fp string `json:"fp,omitempty"` - Module string `json:"module"` - Path string `json:"path,omitempty"` - Publish bool `json:"publish,omitempty"` - Toolchain *StepAskBuildToolchain `json:"toolchain,omitempty"` + Artifacts []AskArtifact `json:"artifacts"` + Fetch *Fetch `json:"fetch,omitempty"` + Fp string `json:"fp,omitempty"` + Module string `json:"module"` + Path string `json:"path,omitempty"` + Publish bool `json:"publish,omitempty"` + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` } // StepAskCheck is a type of the contract. type StepAskCheck struct { - // what the build module fetches: the repository, commit and directory, as the indexer states it - Fetch any `json:"fetch"` + Fetch Fetch `json:"fetch"` Kept bool `json:"kept,omitempty"` Levels []string `json:"levels"` Packages []string `json:"packages,omitempty"` @@ -363,9 +371,8 @@ type StepAskCheck struct { // StepAskCompose is a type of the contract. type StepAskCompose struct { - Dir string `json:"dir"` - // what the build module fetches: the repository, commit and directory, as the indexer states it - Fetch any `json:"fetch"` + Dir string `json:"dir"` + Fetch Fetch `json:"fetch"` Package string `json:"package"` Reader string `json:"reader,omitempty"` // the compose-candidate request @@ -392,6 +399,26 @@ type StepAsk struct { V int64 `json:"v,omitempty"` } +// StepResult is what a step answered, by the kind of ask: a build's artifacts, toolchain, imports and out-of-step modules; a check's fetch and levels; a compose's built, key and verdict; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the four. +type StepResult struct { + Artifacts []BuiltArtifact `json:"artifacts,omitempty"` + Built bool `json:"built,omitempty"` + // the holder declined the ask: its node lacks what the ask needs + Declined bool `json:"declined,omitempty"` + Fetch *Fetch `json:"fetch,omitempty"` + Imports []string `json:"imports,omitempty"` + Key string `json:"key,omitempty"` + // what the holder's node lacks of it + Lacks []string `json:"lacks,omitempty"` + Levels []LevelAnswer `json:"levels,omitempty"` + // what the declined ask needs + Needs []string `json:"needs,omitempty"` + OutOfStep []string `json:"out-of-step,omitempty"` + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` + // the compose-candidate verdict (the composer's own layout) + Verdict any `json:"verdict,omitempty"` +} + // Toolchain is the toolchain a node reports. type Toolchain struct { // when (RFC 3339 time) @@ -404,6 +431,13 @@ type Toolchain struct { Version string `json:"version"` } +// ToolchainUsed is the toolchain a build was made with: the language, the installed version and the build module's recipe. +type ToolchainUsed struct { + Language string `json:"language"` + Recipe string `json:"recipe"` + Version string `json:"version"` +} + // CurrentInput is what current takes. type CurrentInput struct { } diff --git a/contracts/go/seats/nodebuildgo/contract_test.go b/contracts/go/seats/nodebuildgo/contract_test.go index e7dd00cc..4aabcc73 100644 --- a/contracts/go/seats/nodebuildgo/contract_test.go +++ b/contracts/go/seats/nodebuildgo/contract_test.go @@ -39,6 +39,14 @@ func TestFixtures(t *testing.T) { }, subjectIs: nil}, {name: "a build ask", subject: "mesh.seat.node-build-go.accept.delivery", given: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}", wire: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}", decode: func(b []byte) (any, error) { var v DeliveryAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.accept\\.delivery$")}, + {name: "a check ask with its fetch, siblings and levels", subject: "mesh.seat.node-build-go.accept.check", given: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"beside\":[{\"commit\":\"fedcba9876543210fedcba9876543210fedcba98\",\"repository\":\"novox/mesh-sdk\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"fedcba9876543210fedcba9876543210fedcba98\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-catalog\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"]},\"id\":\"c3d4\",\"kind\":\"check\",\"language\":\"go\",\"v\":1}", wire: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"beside\":[{\"commit\":\"fedcba9876543210fedcba9876543210fedcba98\",\"repository\":\"novox/mesh-sdk\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"fedcba9876543210fedcba9876543210fedcba98\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-catalog\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"]},\"id\":\"c3d4\",\"kind\":\"check\",\"language\":\"go\",\"v\":1}", + decode: func(b []byte) (any, error) { var v CheckAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.accept\\.check$")}, + {name: "a finished build step with its artifacts", subject: "", given: "{\"ended\":\"2026-11-05T10:03:00Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"result\":{\"artifacts\":[{\"artifact\":\"go\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"reference\":\"container-image-registry/nats/go@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}],\"imports\":[\"git.novox.be/novox/mesh-sdk/go\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"build\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:03:00Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"result\":{\"artifacts\":[{\"artifact\":\"go\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"reference\":\"container-image-registry/nats/go@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}],\"imports\":[\"git.novox.be/novox/mesh-sdk/go\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"build\",\"v\":1}", + decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a finished check step with its levels", subject: "", given: "{\"ended\":\"2026-11-05T10:06:00Z\",\"id\":\"c3d4\",\"node\":\"ace\",\"result\":{\"fetch\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"repository\":\"novox/mesh-catalog\"},\"levels\":[{\"digest\":{\"level\":\"unit\",\"packages\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\"}]},\"kept\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\",\"tests\":12}],\"level\":\"unit\",\"tests\":[{\"package\":\"./modules/b\",\"pass\":true}],\"unmet\":[{\"needs\":[\"docker\"],\"package\":\"./modules/c\"}]}]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:06:00Z\",\"id\":\"c3d4\",\"node\":\"ace\",\"result\":{\"fetch\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"repository\":\"novox/mesh-catalog\"},\"levels\":[{\"digest\":{\"level\":\"unit\",\"packages\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\"}]},\"kept\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\",\"tests\":12}],\"level\":\"unit\",\"tests\":[{\"package\":\"./modules/b\",\"pass\":true}],\"unmet\":[{\"needs\":[\"docker\"],\"package\":\"./modules/c\"}]}]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", + decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a declined step", subject: "", given: "{\"id\":\"c3d5\",\"node\":\"ace\",\"result\":{\"declined\":true,\"lacks\":[\"docker\"],\"needs\":[\"docker\"]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"refused\",\"tool\":\"check\",\"v\":1}", wire: "{\"id\":\"c3d5\",\"node\":\"ace\",\"result\":{\"declined\":true,\"lacks\":[\"docker\"],\"needs\":[\"docker\"]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"refused\",\"tool\":\"check\",\"v\":1}", + decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; 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/go/seats/nodebuildimage/contract.go b/contracts/go/seats/nodebuildimage/contract.go index 843c04ae..db640ee3 100644 --- a/contracts/go/seats/nodebuildimage/contract.go +++ b/contracts/go/seats/nodebuildimage/contract.go @@ -187,26 +187,27 @@ func DecodeToolchainsByNodeLanguageValue(raw []byte) (ToolchainsByNodeLanguageVa return v, nil } -// BuildAnswerArtifactsItem is a type of the contract. -type BuildAnswerArtifactsItem struct { - Artifact string `json:"artifact"` - Digest string `json:"digest"` - Reference string `json:"reference,omitempty"` -} - -// BuildAnswerToolchain is a type of the contract. -type BuildAnswerToolchain struct { - Language string `json:"language"` - Recipe string `json:"recipe"` - Version string `json:"version"` +// AskArtifact is one artifact of a build ask: built from the fetched tree, or copied from an upstream pin. +type AskArtifact struct { + Artifact string `json:"artifact"` + From string `json:"from,omitempty"` + Seat string `json:"seat,omitempty"` + Version string `json:"version,omitempty"` } // BuildAnswer is a type of the contract. type BuildAnswer struct { - Artifacts []BuildAnswerArtifactsItem `json:"artifacts"` - Imports []string `json:"imports,omitempty"` - OutOfStep []string `json:"out-of-step,omitempty"` - Toolchain *BuildAnswerToolchain `json:"toolchain,omitempty"` + Artifacts []BuiltArtifact `json:"artifacts"` + Imports []string `json:"imports,omitempty"` + OutOfStep []string `json:"out-of-step,omitempty"` + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` +} + +// BuiltArtifact is one artifact a build made: its digest and the reference it was published under. +type BuiltArtifact struct { + Artifact string `json:"artifact"` + Digest string `json:"digest"` + Reference string `json:"reference,omitempty"` } // Capacity is how many steps the node runs at once. @@ -229,56 +230,10 @@ type Capacity struct { Why string `json:"why"` } -// CheckAnswerLevelsItemDigestNeedsItem is a type of the contract. -type CheckAnswerLevelsItemDigestNeedsItem struct { - Needs []string `json:"needs,omitempty"` - Package string `json:"package,omitempty"` -} - -// CheckAnswerLevelsItemDigestPackagesItem is a type of the contract. -type CheckAnswerLevelsItemDigestPackagesItem struct { - Digest string `json:"digest"` - Package string `json:"package"` -} - -// CheckAnswerLevelsItemDigest is a type of the contract. -type CheckAnswerLevelsItemDigest struct { - Beside []string `json:"beside,omitempty"` - JudgedBy string `json:"judged_by,omitempty"` - Level string `json:"level,omitempty"` - Needs []CheckAnswerLevelsItemDigestNeedsItem `json:"needs,omitempty"` - Packages []CheckAnswerLevelsItemDigestPackagesItem `json:"packages,omitempty"` - WithoutTests []string `json:"without_tests,omitempty"` -} - -// CheckAnswerLevelsItemKeptItem is a type of the contract. -type CheckAnswerLevelsItemKeptItem struct { - Digest string `json:"digest,omitempty"` - Package string `json:"package,omitempty"` - Tests int64 `json:"tests,omitempty"` -} - -// CheckAnswerLevelsItemUnmetItem is a type of the contract. -type CheckAnswerLevelsItemUnmetItem struct { - Needs []string `json:"needs,omitempty"` - Package string `json:"package,omitempty"` -} - -// CheckAnswerLevelsItem is a type of the contract. -type CheckAnswerLevelsItem struct { - Digest *CheckAnswerLevelsItemDigest `json:"digest,omitempty"` - Judged string `json:"judged,omitempty"` - Kept []CheckAnswerLevelsItemKeptItem `json:"kept,omitempty"` - Level string `json:"level"` - Tests []any `json:"tests,omitempty"` - Unmet []CheckAnswerLevelsItemUnmetItem `json:"unmet,omitempty"` -} - // CheckAnswer is a type of the contract. type CheckAnswer struct { - // what the build module fetches: the repository, commit and directory, as the indexer states it - Fetch any `json:"fetch"` - Levels []CheckAnswerLevelsItem `json:"levels,omitempty"` + Fetch Fetch `json:"fetch"` + Levels []LevelAnswer `json:"levels,omitempty"` } // ComposeAnswer is a type of the contract. @@ -289,6 +244,77 @@ type ComposeAnswer struct { Verdict any `json:"verdict,omitempty"` } +// FetchBesideItem is a type of the contract. +type FetchBesideItem struct { + Commit string `json:"commit"` + Repository string `json:"repository"` +} + +// Fetch is what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside. +type Fetch struct { + // the trunk commit a merge preview merges onto + Base string `json:"base,omitempty"` + // the sibling repositories placed beside the fetched tree + Beside []FetchBesideItem `json:"beside,omitempty"` + // the full commit + Commit string `json:"commit"` + // the module's directory in the repository ("." for its root), with paths + Dir string `json:"dir,omitempty"` + // the version (a full commit) of each mesh package to build against, by its module path + Packages map[string]string `json:"packages,omitempty"` + // the paths the module names, relative to dir + Paths []string `json:"paths,omitempty"` + // the pull request's ref, when only its head carries the commit + Ref string `json:"ref,omitempty"` + // / + Repository string `json:"repository"` +} + +// LevelAnswerDigestNeedsItem is a type of the contract. +type LevelAnswerDigestNeedsItem struct { + Needs []string `json:"needs,omitempty"` + Package string `json:"package,omitempty"` +} + +// LevelAnswerDigestPackagesItem is a type of the contract. +type LevelAnswerDigestPackagesItem struct { + Digest string `json:"digest"` + Package string `json:"package"` +} + +// LevelAnswerDigest is a type of the contract. +type LevelAnswerDigest struct { + Beside []string `json:"beside,omitempty"` + JudgedBy string `json:"judged_by,omitempty"` + Level string `json:"level,omitempty"` + Needs []LevelAnswerDigestNeedsItem `json:"needs,omitempty"` + Packages []LevelAnswerDigestPackagesItem `json:"packages,omitempty"` + WithoutTests []string `json:"without_tests,omitempty"` +} + +// LevelAnswerKeptItem is a type of the contract. +type LevelAnswerKeptItem struct { + Digest string `json:"digest,omitempty"` + Package string `json:"package,omitempty"` + Tests int64 `json:"tests,omitempty"` +} + +// LevelAnswerUnmetItem is a type of the contract. +type LevelAnswerUnmetItem struct { + Needs []string `json:"needs,omitempty"` + Package string `json:"package,omitempty"` +} + +// LevelAnswer is one level of a check ask: the digest's answer, the kept verdicts taken, one test result per test step run, and the packages the node could not test. +type LevelAnswer struct { + Digest *LevelAnswerDigest `json:"digest,omitempty"` + Judged string `json:"judged,omitempty"` + Kept []LevelAnswerKeptItem `json:"kept,omitempty"` + Level string `json:"level"` + Tests []any `json:"tests,omitempty"` + Unmet []LevelAnswerUnmetItem `json:"unmet,omitempty"` +} + // Pause is whether the node takes no new ask. type Pause struct { // when (RFC 3339 time) @@ -308,10 +334,9 @@ type Step struct { Owner string `json:"owner,omitempty"` Phase string `json:"phase,omitempty"` // when (RFC 3339 time) - PhaseAt string `json:"phase-at,omitempty"` - Reason string `json:"reason,omitempty"` - // what the step answered - Result any `json:"result,omitempty"` + PhaseAt string `json:"phase-at,omitempty"` + Reason string `json:"reason,omitempty"` + Result *StepResult `json:"result,omitempty"` // when (RFC 3339 time) Started string `json:"started"` // running, passed, failed or refused @@ -322,37 +347,20 @@ type Step struct { What string `json:"what,omitempty"` } -// StepAskBuildArtifactsItem is a type of the contract. -type StepAskBuildArtifactsItem struct { - Artifact string `json:"artifact"` - From string `json:"from,omitempty"` - Seat string `json:"seat,omitempty"` - Version string `json:"version,omitempty"` -} - -// StepAskBuildToolchain is a type of the contract. -type StepAskBuildToolchain struct { - Language string `json:"language"` - Recipe string `json:"recipe"` - Version string `json:"version"` -} - // StepAskBuild is a type of the contract. type StepAskBuild struct { - Artifacts []StepAskBuildArtifactsItem `json:"artifacts"` - // what the build module fetches: the repository, commit and directory, as the indexer states it - Fetch any `json:"fetch,omitempty"` - Fp string `json:"fp,omitempty"` - Module string `json:"module"` - Path string `json:"path,omitempty"` - Publish bool `json:"publish,omitempty"` - Toolchain *StepAskBuildToolchain `json:"toolchain,omitempty"` + Artifacts []AskArtifact `json:"artifacts"` + Fetch *Fetch `json:"fetch,omitempty"` + Fp string `json:"fp,omitempty"` + Module string `json:"module"` + Path string `json:"path,omitempty"` + Publish bool `json:"publish,omitempty"` + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` } // StepAskCheck is a type of the contract. type StepAskCheck struct { - // what the build module fetches: the repository, commit and directory, as the indexer states it - Fetch any `json:"fetch"` + Fetch Fetch `json:"fetch"` Kept bool `json:"kept,omitempty"` Levels []string `json:"levels"` Packages []string `json:"packages,omitempty"` @@ -363,9 +371,8 @@ type StepAskCheck struct { // StepAskCompose is a type of the contract. type StepAskCompose struct { - Dir string `json:"dir"` - // what the build module fetches: the repository, commit and directory, as the indexer states it - Fetch any `json:"fetch"` + Dir string `json:"dir"` + Fetch Fetch `json:"fetch"` Package string `json:"package"` Reader string `json:"reader,omitempty"` // the compose-candidate request @@ -392,6 +399,26 @@ type StepAsk struct { V int64 `json:"v,omitempty"` } +// StepResult is what a step answered, by the kind of ask: a build's artifacts, toolchain, imports and out-of-step modules; a check's fetch and levels; a compose's built, key and verdict; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the four. +type StepResult struct { + Artifacts []BuiltArtifact `json:"artifacts,omitempty"` + Built bool `json:"built,omitempty"` + // the holder declined the ask: its node lacks what the ask needs + Declined bool `json:"declined,omitempty"` + Fetch *Fetch `json:"fetch,omitempty"` + Imports []string `json:"imports,omitempty"` + Key string `json:"key,omitempty"` + // what the holder's node lacks of it + Lacks []string `json:"lacks,omitempty"` + Levels []LevelAnswer `json:"levels,omitempty"` + // what the declined ask needs + Needs []string `json:"needs,omitempty"` + OutOfStep []string `json:"out-of-step,omitempty"` + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` + // the compose-candidate verdict (the composer's own layout) + Verdict any `json:"verdict,omitempty"` +} + // Toolchain is the toolchain a node reports. type Toolchain struct { // when (RFC 3339 time) @@ -404,6 +431,13 @@ type Toolchain struct { Version string `json:"version"` } +// ToolchainUsed is the toolchain a build was made with: the language, the installed version and the build module's recipe. +type ToolchainUsed struct { + Language string `json:"language"` + Recipe string `json:"recipe"` + Version string `json:"version"` +} + // CurrentInput is what current takes. type CurrentInput struct { } diff --git a/contracts/go/seats/nodebuildnode/contract.go b/contracts/go/seats/nodebuildnode/contract.go index 8e3e0448..9b0c7ff8 100644 --- a/contracts/go/seats/nodebuildnode/contract.go +++ b/contracts/go/seats/nodebuildnode/contract.go @@ -187,26 +187,27 @@ func DecodeToolchainsByNodeLanguageValue(raw []byte) (ToolchainsByNodeLanguageVa return v, nil } -// BuildAnswerArtifactsItem is a type of the contract. -type BuildAnswerArtifactsItem struct { - Artifact string `json:"artifact"` - Digest string `json:"digest"` - Reference string `json:"reference,omitempty"` -} - -// BuildAnswerToolchain is a type of the contract. -type BuildAnswerToolchain struct { - Language string `json:"language"` - Recipe string `json:"recipe"` - Version string `json:"version"` +// AskArtifact is one artifact of a build ask: built from the fetched tree, or copied from an upstream pin. +type AskArtifact struct { + Artifact string `json:"artifact"` + From string `json:"from,omitempty"` + Seat string `json:"seat,omitempty"` + Version string `json:"version,omitempty"` } // BuildAnswer is a type of the contract. type BuildAnswer struct { - Artifacts []BuildAnswerArtifactsItem `json:"artifacts"` - Imports []string `json:"imports,omitempty"` - OutOfStep []string `json:"out-of-step,omitempty"` - Toolchain *BuildAnswerToolchain `json:"toolchain,omitempty"` + Artifacts []BuiltArtifact `json:"artifacts"` + Imports []string `json:"imports,omitempty"` + OutOfStep []string `json:"out-of-step,omitempty"` + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` +} + +// BuiltArtifact is one artifact a build made: its digest and the reference it was published under. +type BuiltArtifact struct { + Artifact string `json:"artifact"` + Digest string `json:"digest"` + Reference string `json:"reference,omitempty"` } // Capacity is how many steps the node runs at once. @@ -229,56 +230,10 @@ type Capacity struct { Why string `json:"why"` } -// CheckAnswerLevelsItemDigestNeedsItem is a type of the contract. -type CheckAnswerLevelsItemDigestNeedsItem struct { - Needs []string `json:"needs,omitempty"` - Package string `json:"package,omitempty"` -} - -// CheckAnswerLevelsItemDigestPackagesItem is a type of the contract. -type CheckAnswerLevelsItemDigestPackagesItem struct { - Digest string `json:"digest"` - Package string `json:"package"` -} - -// CheckAnswerLevelsItemDigest is a type of the contract. -type CheckAnswerLevelsItemDigest struct { - Beside []string `json:"beside,omitempty"` - JudgedBy string `json:"judged_by,omitempty"` - Level string `json:"level,omitempty"` - Needs []CheckAnswerLevelsItemDigestNeedsItem `json:"needs,omitempty"` - Packages []CheckAnswerLevelsItemDigestPackagesItem `json:"packages,omitempty"` - WithoutTests []string `json:"without_tests,omitempty"` -} - -// CheckAnswerLevelsItemKeptItem is a type of the contract. -type CheckAnswerLevelsItemKeptItem struct { - Digest string `json:"digest,omitempty"` - Package string `json:"package,omitempty"` - Tests int64 `json:"tests,omitempty"` -} - -// CheckAnswerLevelsItemUnmetItem is a type of the contract. -type CheckAnswerLevelsItemUnmetItem struct { - Needs []string `json:"needs,omitempty"` - Package string `json:"package,omitempty"` -} - -// CheckAnswerLevelsItem is a type of the contract. -type CheckAnswerLevelsItem struct { - Digest *CheckAnswerLevelsItemDigest `json:"digest,omitempty"` - Judged string `json:"judged,omitempty"` - Kept []CheckAnswerLevelsItemKeptItem `json:"kept,omitempty"` - Level string `json:"level"` - Tests []any `json:"tests,omitempty"` - Unmet []CheckAnswerLevelsItemUnmetItem `json:"unmet,omitempty"` -} - // CheckAnswer is a type of the contract. type CheckAnswer struct { - // what the build module fetches: the repository, commit and directory, as the indexer states it - Fetch any `json:"fetch"` - Levels []CheckAnswerLevelsItem `json:"levels,omitempty"` + Fetch Fetch `json:"fetch"` + Levels []LevelAnswer `json:"levels,omitempty"` } // ComposeAnswer is a type of the contract. @@ -289,6 +244,77 @@ type ComposeAnswer struct { Verdict any `json:"verdict,omitempty"` } +// FetchBesideItem is a type of the contract. +type FetchBesideItem struct { + Commit string `json:"commit"` + Repository string `json:"repository"` +} + +// Fetch is what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside. +type Fetch struct { + // the trunk commit a merge preview merges onto + Base string `json:"base,omitempty"` + // the sibling repositories placed beside the fetched tree + Beside []FetchBesideItem `json:"beside,omitempty"` + // the full commit + Commit string `json:"commit"` + // the module's directory in the repository ("." for its root), with paths + Dir string `json:"dir,omitempty"` + // the version (a full commit) of each mesh package to build against, by its module path + Packages map[string]string `json:"packages,omitempty"` + // the paths the module names, relative to dir + Paths []string `json:"paths,omitempty"` + // the pull request's ref, when only its head carries the commit + Ref string `json:"ref,omitempty"` + // / + Repository string `json:"repository"` +} + +// LevelAnswerDigestNeedsItem is a type of the contract. +type LevelAnswerDigestNeedsItem struct { + Needs []string `json:"needs,omitempty"` + Package string `json:"package,omitempty"` +} + +// LevelAnswerDigestPackagesItem is a type of the contract. +type LevelAnswerDigestPackagesItem struct { + Digest string `json:"digest"` + Package string `json:"package"` +} + +// LevelAnswerDigest is a type of the contract. +type LevelAnswerDigest struct { + Beside []string `json:"beside,omitempty"` + JudgedBy string `json:"judged_by,omitempty"` + Level string `json:"level,omitempty"` + Needs []LevelAnswerDigestNeedsItem `json:"needs,omitempty"` + Packages []LevelAnswerDigestPackagesItem `json:"packages,omitempty"` + WithoutTests []string `json:"without_tests,omitempty"` +} + +// LevelAnswerKeptItem is a type of the contract. +type LevelAnswerKeptItem struct { + Digest string `json:"digest,omitempty"` + Package string `json:"package,omitempty"` + Tests int64 `json:"tests,omitempty"` +} + +// LevelAnswerUnmetItem is a type of the contract. +type LevelAnswerUnmetItem struct { + Needs []string `json:"needs,omitempty"` + Package string `json:"package,omitempty"` +} + +// LevelAnswer is one level of a check ask: the digest's answer, the kept verdicts taken, one test result per test step run, and the packages the node could not test. +type LevelAnswer struct { + Digest *LevelAnswerDigest `json:"digest,omitempty"` + Judged string `json:"judged,omitempty"` + Kept []LevelAnswerKeptItem `json:"kept,omitempty"` + Level string `json:"level"` + Tests []any `json:"tests,omitempty"` + Unmet []LevelAnswerUnmetItem `json:"unmet,omitempty"` +} + // Pause is whether the node takes no new ask. type Pause struct { // when (RFC 3339 time) @@ -308,10 +334,9 @@ type Step struct { Owner string `json:"owner,omitempty"` Phase string `json:"phase,omitempty"` // when (RFC 3339 time) - PhaseAt string `json:"phase-at,omitempty"` - Reason string `json:"reason,omitempty"` - // what the step answered - Result any `json:"result,omitempty"` + PhaseAt string `json:"phase-at,omitempty"` + Reason string `json:"reason,omitempty"` + Result *StepResult `json:"result,omitempty"` // when (RFC 3339 time) Started string `json:"started"` // running, passed, failed or refused @@ -322,37 +347,20 @@ type Step struct { What string `json:"what,omitempty"` } -// StepAskBuildArtifactsItem is a type of the contract. -type StepAskBuildArtifactsItem struct { - Artifact string `json:"artifact"` - From string `json:"from,omitempty"` - Seat string `json:"seat,omitempty"` - Version string `json:"version,omitempty"` -} - -// StepAskBuildToolchain is a type of the contract. -type StepAskBuildToolchain struct { - Language string `json:"language"` - Recipe string `json:"recipe"` - Version string `json:"version"` -} - // StepAskBuild is a type of the contract. type StepAskBuild struct { - Artifacts []StepAskBuildArtifactsItem `json:"artifacts"` - // what the build module fetches: the repository, commit and directory, as the indexer states it - Fetch any `json:"fetch,omitempty"` - Fp string `json:"fp,omitempty"` - Module string `json:"module"` - Path string `json:"path,omitempty"` - Publish bool `json:"publish,omitempty"` - Toolchain *StepAskBuildToolchain `json:"toolchain,omitempty"` + Artifacts []AskArtifact `json:"artifacts"` + Fetch *Fetch `json:"fetch,omitempty"` + Fp string `json:"fp,omitempty"` + Module string `json:"module"` + Path string `json:"path,omitempty"` + Publish bool `json:"publish,omitempty"` + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` } // StepAskCheck is a type of the contract. type StepAskCheck struct { - // what the build module fetches: the repository, commit and directory, as the indexer states it - Fetch any `json:"fetch"` + Fetch Fetch `json:"fetch"` Kept bool `json:"kept,omitempty"` Levels []string `json:"levels"` Packages []string `json:"packages,omitempty"` @@ -363,9 +371,8 @@ type StepAskCheck struct { // StepAskCompose is a type of the contract. type StepAskCompose struct { - Dir string `json:"dir"` - // what the build module fetches: the repository, commit and directory, as the indexer states it - Fetch any `json:"fetch"` + Dir string `json:"dir"` + Fetch Fetch `json:"fetch"` Package string `json:"package"` Reader string `json:"reader,omitempty"` // the compose-candidate request @@ -392,6 +399,26 @@ type StepAsk struct { V int64 `json:"v,omitempty"` } +// StepResult is what a step answered, by the kind of ask: a build's artifacts, toolchain, imports and out-of-step modules; a check's fetch and levels; a compose's built, key and verdict; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the four. +type StepResult struct { + Artifacts []BuiltArtifact `json:"artifacts,omitempty"` + Built bool `json:"built,omitempty"` + // the holder declined the ask: its node lacks what the ask needs + Declined bool `json:"declined,omitempty"` + Fetch *Fetch `json:"fetch,omitempty"` + Imports []string `json:"imports,omitempty"` + Key string `json:"key,omitempty"` + // what the holder's node lacks of it + Lacks []string `json:"lacks,omitempty"` + Levels []LevelAnswer `json:"levels,omitempty"` + // what the declined ask needs + Needs []string `json:"needs,omitempty"` + OutOfStep []string `json:"out-of-step,omitempty"` + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` + // the compose-candidate verdict (the composer's own layout) + Verdict any `json:"verdict,omitempty"` +} + // Toolchain is the toolchain a node reports. type Toolchain struct { // when (RFC 3339 time) @@ -404,6 +431,13 @@ type Toolchain struct { Version string `json:"version"` } +// ToolchainUsed is the toolchain a build was made with: the language, the installed version and the build module's recipe. +type ToolchainUsed struct { + Language string `json:"language"` + Recipe string `json:"recipe"` + Version string `json:"version"` +} + // CurrentInput is what current takes. type CurrentInput struct { } diff --git a/contracts/parts/build-step.json b/contracts/parts/build-step.json index dfc809f6..55920376 100644 --- a/contracts/parts/build-step.json +++ b/contracts/parts/build-step.json @@ -64,7 +64,8 @@ }, "at": { "type": "string", - "description": "when (RFC 3339 time)" + "description": "when (RFC 3339 time)", + "format": "date-time" }, "build": { "type": "object", @@ -79,52 +80,19 @@ "type": "string" }, "fetch": { - "description": "what the build module fetches: the repository, commit and directory, as the indexer states it" + "$ref": "#/defs/Fetch" }, "artifacts": { "type": "array", "items": { - "type": "object", - "properties": { - "artifact": { - "type": "string" - }, - "from": { - "type": "string" - }, - "seat": { - "type": "string" - }, - "version": { - "type": "string" - } - }, - "required": [ - "artifact" - ] + "$ref": "#/defs/AskArtifact" } }, "publish": { "type": "boolean" }, "toolchain": { - "type": "object", - "properties": { - "language": { - "type": "string" - }, - "version": { - "type": "string" - }, - "recipe": { - "type": "string" - } - }, - "required": [ - "language", - "version", - "recipe" - ] + "$ref": "#/defs/ToolchainUsed" } }, "required": [ @@ -136,7 +104,7 @@ "type": "object", "properties": { "fetch": { - "description": "what the build module fetches: the repository, commit and directory, as the indexer states it" + "$ref": "#/defs/Fetch" }, "levels": { "type": "array", @@ -173,7 +141,7 @@ "type": "object", "properties": { "fetch": { - "description": "what the build module fetches: the repository, commit and directory, as the indexer states it" + "$ref": "#/defs/Fetch" }, "dir": { "type": "string" @@ -223,42 +191,11 @@ "artifacts": { "type": "array", "items": { - "type": "object", - "properties": { - "artifact": { - "type": "string" - }, - "digest": { - "type": "string" - }, - "reference": { - "type": "string" - } - }, - "required": [ - "artifact", - "digest" - ] + "$ref": "#/defs/BuiltArtifact" } }, "toolchain": { - "type": "object", - "properties": { - "language": { - "type": "string" - }, - "version": { - "type": "string" - }, - "recipe": { - "type": "string" - } - }, - "required": [ - "language", - "version", - "recipe" - ] + "$ref": "#/defs/ToolchainUsed" }, "imports": { "type": "array", @@ -281,121 +218,12 @@ "type": "object", "properties": { "fetch": { - "description": "what the build module fetches: the repository, commit and directory, as the indexer states it" + "$ref": "#/defs/Fetch" }, "levels": { "type": "array", "items": { - "type": "object", - "properties": { - "level": { - "type": "string" - }, - "judged": { - "type": "string" - }, - "digest": { - "type": "object", - "properties": { - "level": { - "type": "string" - }, - "packages": { - "type": "array", - "items": { - "type": "object", - "properties": { - "package": { - "type": "string" - }, - "digest": { - "type": "string" - } - }, - "required": [ - "package", - "digest" - ] - } - }, - "beside": { - "type": "array", - "items": { - "type": "string" - } - }, - "without_tests": { - "type": "array", - "items": { - "type": "string" - } - }, - "judged_by": { - "type": "string" - }, - "needs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "package": { - "type": "string" - }, - "needs": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - } - }, - "kept": { - "type": "array", - "items": { - "type": "object", - "properties": { - "package": { - "type": "string" - }, - "digest": { - "type": "string" - }, - "tests": { - "type": "integer" - } - } - } - }, - "tests": { - "type": "array", - "items": { - "description": "one test's result" - } - }, - "unmet": { - "type": "array", - "items": { - "type": "object", - "properties": { - "package": { - "type": "string" - }, - "needs": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - }, - "required": [ - "level" - ] + "$ref": "#/defs/LevelAnswer" } } }, @@ -442,17 +270,19 @@ }, "started": { "type": "string", - "description": "when (RFC 3339 time)" + "description": "when (RFC 3339 time)", + "format": "date-time" }, "ended": { "type": "string", - "description": "when (RFC 3339 time)" + "description": "when (RFC 3339 time)", + "format": "date-time" }, "reason": { "type": "string" }, "result": { - "description": "what the step answered" + "$ref": "#/defs/StepResult" }, "what": { "type": "string" @@ -462,7 +292,8 @@ }, "phase-at": { "type": "string", - "description": "when (RFC 3339 time)" + "description": "when (RFC 3339 time)", + "format": "date-time" }, "owner": { "type": "string" @@ -496,7 +327,8 @@ }, "at": { "type": "string", - "description": "when (RFC 3339 time)" + "description": "when (RFC 3339 time)", + "format": "date-time" } }, "required": [ @@ -521,7 +353,8 @@ }, "at": { "type": "string", - "description": "when (RFC 3339 time)" + "description": "when (RFC 3339 time)", + "format": "date-time" } }, "required": [ @@ -574,7 +407,8 @@ }, "at": { "type": "string", - "description": "when (RFC 3339 time)" + "description": "when (RFC 3339 time)", + "format": "date-time" } }, "required": [ @@ -583,6 +417,305 @@ "why" ], "description": "how many steps the node runs at once" + }, + "Fetch": { + "type": "object", + "description": "what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside", + "properties": { + "repository": { + "type": "string", + "description": "/" + }, + "commit": { + "type": "string", + "description": "the full commit" + }, + "ref": { + "type": "string", + "description": "the pull request's ref, when only its head carries the commit" + }, + "base": { + "type": "string", + "description": "the trunk commit a merge preview merges onto" + }, + "dir": { + "type": "string", + "description": "the module's directory in the repository (\".\" for its root), with paths" + }, + "paths": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the paths the module names, relative to dir" + }, + "packages": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "the version (a full commit) of each mesh package to build against, by its module path" + }, + "beside": { + "type": "array", + "items": { + "type": "object", + "properties": { + "repository": { + "type": "string" + }, + "commit": { + "type": "string" + } + }, + "required": [ + "repository", + "commit" + ] + }, + "description": "the sibling repositories placed beside the fetched tree" + } + }, + "required": [ + "repository", + "commit" + ] + }, + "ToolchainUsed": { + "type": "object", + "properties": { + "language": { + "type": "string" + }, + "version": { + "type": "string" + }, + "recipe": { + "type": "string" + } + }, + "required": [ + "language", + "version", + "recipe" + ], + "description": "the toolchain a build was made with: the language, the installed version and the build module's recipe" + }, + "AskArtifact": { + "type": "object", + "properties": { + "artifact": { + "type": "string" + }, + "from": { + "type": "string" + }, + "seat": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "required": [ + "artifact" + ], + "description": "one artifact of a build ask: built from the fetched tree, or copied from an upstream pin" + }, + "BuiltArtifact": { + "type": "object", + "properties": { + "artifact": { + "type": "string" + }, + "digest": { + "type": "string" + }, + "reference": { + "type": "string" + } + }, + "required": [ + "artifact", + "digest" + ], + "description": "one artifact a build made: its digest and the reference it was published under" + }, + "LevelAnswer": { + "type": "object", + "properties": { + "level": { + "type": "string" + }, + "judged": { + "type": "string" + }, + "digest": { + "type": "object", + "properties": { + "level": { + "type": "string" + }, + "packages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "package": { + "type": "string" + }, + "digest": { + "type": "string" + } + }, + "required": [ + "package", + "digest" + ] + } + }, + "beside": { + "type": "array", + "items": { + "type": "string" + } + }, + "without_tests": { + "type": "array", + "items": { + "type": "string" + } + }, + "judged_by": { + "type": "string" + }, + "needs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "package": { + "type": "string" + }, + "needs": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "kept": { + "type": "array", + "items": { + "type": "object", + "properties": { + "package": { + "type": "string" + }, + "digest": { + "type": "string" + }, + "tests": { + "type": "integer" + } + } + } + }, + "tests": { + "type": "array", + "items": { + "description": "one test's result" + } + }, + "unmet": { + "type": "array", + "items": { + "type": "object", + "properties": { + "package": { + "type": "string" + }, + "needs": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "required": [ + "level" + ], + "description": "one level of a check ask: the digest's answer, the kept verdicts taken, one test result per test step run, and the packages the node could not test" + }, + "StepResult": { + "type": "object", + "description": "what a step answered, by the kind of ask: a build's artifacts, toolchain, imports and out-of-step modules; a check's fetch and levels; a compose's built, key and verdict; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the four", + "properties": { + "artifacts": { + "type": "array", + "items": { + "$ref": "#/defs/BuiltArtifact" + } + }, + "toolchain": { + "$ref": "#/defs/ToolchainUsed" + }, + "imports": { + "type": "array", + "items": { + "type": "string" + } + }, + "out-of-step": { + "type": "array", + "items": { + "type": "string" + } + }, + "fetch": { + "$ref": "#/defs/Fetch" + }, + "levels": { + "type": "array", + "items": { + "$ref": "#/defs/LevelAnswer" + } + }, + "built": { + "type": "boolean" + }, + "key": { + "type": "string" + }, + "verdict": { + "description": "the compose-candidate verdict (the composer's own layout)" + }, + "declined": { + "type": "boolean", + "description": "the holder declined the ask: its node lacks what the ask needs" + }, + "needs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "what the declined ask needs" + }, + "lacks": { + "type": "array", + "items": { + "type": "string" + }, + "description": "what the holder's node lacks of it" + } + } } } } diff --git a/contracts/seats/node-build-go/fixtures/states.json b/contracts/seats/node-build-go/fixtures/states.json index 1988c3d4..5193a6af 100644 --- a/contracts/seats/node-build-go/fixtures/states.json +++ b/contracts/seats/node-build-go/fixtures/states.json @@ -59,5 +59,145 @@ ] } } + }, + { + "name": "a check ask with its fetch, siblings and levels", + "of": "accept:check", + "subject": "mesh.seat.node-build-go.accept.check", + "given": { + "v": 1, + "id": "c3d4", + "kind": "check", + "language": "go", + "asker": "mesh-delivery-checker", + "at": "2026-11-05T10:00:00Z", + "check": { + "fetch": { + "repository": "novox/mesh-catalog", + "commit": "0123456789abcdef0123456789abcdef01234567", + "ref": "refs/pull/7/head", + "base": "89abcdef0123456789abcdef0123456789abcdef", + "beside": [ + { + "repository": "novox/mesh-sdk", + "commit": "fedcba9876543210fedcba9876543210fedcba98" + } + ], + "packages": { + "git.novox.be/novox/mesh-sdk/go": "fedcba9876543210fedcba9876543210fedcba98" + } + }, + "levels": [ + "unit", + "integration" + ], + "kept": true + } + } + }, + { + "name": "a finished build step with its artifacts", + "of": "state:steps/.", + "given": { + "v": 1, + "id": "a1b2", + "tool": "build", + "node": "ace", + "state": "passed", + "started": "2026-11-05T10:00:00Z", + "ended": "2026-11-05T10:03:00Z", + "result": { + "artifacts": [ + { + "artifact": "go", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "reference": "container-image-registry/nats/go@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + ], + "toolchain": { + "language": "go", + "version": "1.26.0", + "recipe": "1" + }, + "imports": [ + "git.novox.be/novox/mesh-sdk/go" + ] + } + } + }, + { + "name": "a finished check step with its levels", + "of": "state:steps/.", + "given": { + "v": 1, + "id": "c3d4", + "tool": "check", + "node": "ace", + "state": "passed", + "started": "2026-11-05T10:00:00Z", + "ended": "2026-11-05T10:06:00Z", + "result": { + "fetch": { + "repository": "novox/mesh-catalog", + "commit": "0123456789abcdef0123456789abcdef01234567" + }, + "levels": [ + { + "level": "unit", + "digest": { + "level": "unit", + "packages": [ + { + "package": "./modules/a", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + } + ] + }, + "kept": [ + { + "package": "./modules/a", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "tests": 12 + } + ], + "tests": [ + { + "package": "./modules/b", + "pass": true + } + ], + "unmet": [ + { + "package": "./modules/c", + "needs": [ + "docker" + ] + } + ] + } + ] + } + } + }, + { + "name": "a declined step", + "of": "state:steps/.", + "given": { + "v": 1, + "id": "c3d5", + "tool": "check", + "node": "ace", + "state": "refused", + "started": "2026-11-05T10:00:00Z", + "result": { + "declined": true, + "needs": [ + "docker" + ], + "lacks": [ + "docker" + ] + } + } } ] diff --git a/contracts/ts/seats/nodebuildgo/src/index.ts b/contracts/ts/seats/nodebuildgo/src/index.ts index 24fa95b4..48102965 100644 --- a/contracts/ts/seats/nodebuildgo/src/index.ts +++ b/contracts/ts/seats/nodebuildgo/src/index.ts @@ -102,20 +102,22 @@ export function parseToolchainsByNodeLanguageKey(key: string): { node: string; l return { node, language }; } +export type AskArtifact = buildStep.AskArtifact; export type BuildAnswer = buildStep.BuildAnswer; -export type BuildAnswerArtifactsItem = buildStep.BuildAnswerArtifactsItem; -export type BuildAnswerToolchain = buildStep.BuildAnswerToolchain; +export type BuiltArtifact = buildStep.BuiltArtifact; export type Capacity = buildStep.Capacity; export type CheckAnswer = buildStep.CheckAnswer; -export type CheckAnswerLevelsItem = buildStep.CheckAnswerLevelsItem; -export type CheckAnswerLevelsItemDigest = buildStep.CheckAnswerLevelsItemDigest; -export type CheckAnswerLevelsItemDigestNeedsItem = buildStep.CheckAnswerLevelsItemDigestNeedsItem; -export type CheckAnswerLevelsItemDigestPackagesItem = buildStep.CheckAnswerLevelsItemDigestPackagesItem; -export type CheckAnswerLevelsItemKeptItem = buildStep.CheckAnswerLevelsItemKeptItem; -export type CheckAnswerLevelsItemUnmetItem = buildStep.CheckAnswerLevelsItemUnmetItem; export type ComposeAnswer = buildStep.ComposeAnswer; export type CurrentAnswer = buildStep.CurrentAnswer; export type CurrentInput = buildStep.CurrentInput; +export type Fetch = buildStep.Fetch; +export type FetchBesideItem = buildStep.FetchBesideItem; +export type LevelAnswer = buildStep.LevelAnswer; +export type LevelAnswerDigest = buildStep.LevelAnswerDigest; +export type LevelAnswerDigestNeedsItem = buildStep.LevelAnswerDigestNeedsItem; +export type LevelAnswerDigestPackagesItem = buildStep.LevelAnswerDigestPackagesItem; +export type LevelAnswerKeptItem = buildStep.LevelAnswerKeptItem; +export type LevelAnswerUnmetItem = buildStep.LevelAnswerUnmetItem; export type Pause = buildStep.Pause; export type PauseAnswer = buildStep.PauseAnswer; export type PauseInput = buildStep.PauseInput; @@ -124,11 +126,11 @@ export type ResumeInput = buildStep.ResumeInput; export type Step = buildStep.Step; export type StepAsk = buildStep.StepAsk; export type StepAskBuild = buildStep.StepAskBuild; -export type StepAskBuildArtifactsItem = buildStep.StepAskBuildArtifactsItem; -export type StepAskBuildToolchain = buildStep.StepAskBuildToolchain; export type StepAskCheck = buildStep.StepAskCheck; export type StepAskCompose = buildStep.StepAskCompose; +export type StepResult = buildStep.StepResult; export type Toolchain = buildStep.Toolchain; +export type ToolchainUsed = buildStep.ToolchainUsed; /** is the body of delivery. */ export type DeliveryAccept = StepAsk; diff --git a/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts b/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts index fa5146dc..4ca1eb95 100644 --- a/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts +++ b/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts @@ -8,23 +8,26 @@ export const verbs = { resume: "resume", } as const; -export interface BuildAnswerArtifactsItem { +/** one artifact of a build ask: built from the fetched tree, or copied from an upstream pin */ +export interface AskArtifact { "artifact": string; - "digest": string; - "reference"?: string; -} - -export interface BuildAnswerToolchain { - "language": string; - "recipe": string; - "version": string; + "from"?: string; + "seat"?: string; + "version"?: string; } export interface BuildAnswer { - "artifacts": BuildAnswerArtifactsItem[]; + "artifacts": BuiltArtifact[]; "imports"?: string[]; "out-of-step"?: string[]; - "toolchain"?: BuildAnswerToolchain; + "toolchain"?: ToolchainUsed; +} + +/** one artifact a build made: its digest and the reference it was published under */ +export interface BuiltArtifact { + "artifact": string; + "digest": string; + "reference"?: string; } /** how many steps the node runs at once */ @@ -47,49 +50,9 @@ export interface Capacity { "why": string; } -export interface CheckAnswerLevelsItemDigestNeedsItem { - "needs"?: string[]; - "package"?: string; -} - -export interface CheckAnswerLevelsItemDigestPackagesItem { - "digest": string; - "package": string; -} - -export interface CheckAnswerLevelsItemDigest { - "beside"?: string[]; - "judged_by"?: string; - "level"?: string; - "needs"?: CheckAnswerLevelsItemDigestNeedsItem[]; - "packages"?: CheckAnswerLevelsItemDigestPackagesItem[]; - "without_tests"?: string[]; -} - -export interface CheckAnswerLevelsItemKeptItem { - "digest"?: string; - "package"?: string; - "tests"?: number; -} - -export interface CheckAnswerLevelsItemUnmetItem { - "needs"?: string[]; - "package"?: string; -} - -export interface CheckAnswerLevelsItem { - "digest"?: CheckAnswerLevelsItemDigest; - "judged"?: string; - "kept"?: CheckAnswerLevelsItemKeptItem[]; - "level": string; - "tests"?: unknown[]; - "unmet"?: CheckAnswerLevelsItemUnmetItem[]; -} - export interface CheckAnswer { - /** what the build module fetches: the repository, commit and directory, as the indexer states it */ - "fetch": unknown; - "levels"?: CheckAnswerLevelsItem[]; + "fetch": Fetch; + "levels"?: LevelAnswer[]; } export interface ComposeAnswer { @@ -99,6 +62,71 @@ export interface ComposeAnswer { "verdict"?: unknown; } +export interface FetchBesideItem { + "commit": string; + "repository": string; +} + +/** what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside */ +export interface Fetch { + /** the trunk commit a merge preview merges onto */ + "base"?: string; + /** the sibling repositories placed beside the fetched tree */ + "beside"?: FetchBesideItem[]; + /** the full commit */ + "commit": string; + /** the module's directory in the repository ("." for its root), with paths */ + "dir"?: string; + /** the version (a full commit) of each mesh package to build against, by its module path */ + "packages"?: Record; + /** the paths the module names, relative to dir */ + "paths"?: string[]; + /** the pull request's ref, when only its head carries the commit */ + "ref"?: string; + /** / */ + "repository": string; +} + +export interface LevelAnswerDigestNeedsItem { + "needs"?: string[]; + "package"?: string; +} + +export interface LevelAnswerDigestPackagesItem { + "digest": string; + "package": string; +} + +export interface LevelAnswerDigest { + "beside"?: string[]; + "judged_by"?: string; + "level"?: string; + "needs"?: LevelAnswerDigestNeedsItem[]; + "packages"?: LevelAnswerDigestPackagesItem[]; + "without_tests"?: string[]; +} + +export interface LevelAnswerKeptItem { + "digest"?: string; + "package"?: string; + "tests"?: number; +} + +export interface LevelAnswerUnmetItem { + "needs"?: string[]; + "package"?: string; +} + +/** one level of a check ask: the digest's answer, the kept verdicts taken, one test result per test step run, and the packages the node could not test */ +export interface LevelAnswer { + "digest"?: LevelAnswerDigest; + "judged"?: string; + "kept"?: LevelAnswerKeptItem[]; + "level": string; + "tests"?: unknown[]; + "unmet"?: LevelAnswerUnmetItem[]; +} + /** whether the node takes no new ask */ export interface Pause { /** when (RFC 3339 time) */ @@ -120,8 +148,7 @@ export interface Step { /** when (RFC 3339 time) */ "phase-at"?: string; "reason"?: string; - /** what the step answered */ - "result"?: unknown; + "result"?: StepResult; /** when (RFC 3339 time) */ "started": string; /** running, passed, failed or refused */ @@ -132,33 +159,18 @@ export interface Step { "what"?: string; } -export interface StepAskBuildArtifactsItem { - "artifact": string; - "from"?: string; - "seat"?: string; - "version"?: string; -} - -export interface StepAskBuildToolchain { - "language": string; - "recipe": string; - "version": string; -} - export interface StepAskBuild { - "artifacts": StepAskBuildArtifactsItem[]; - /** what the build module fetches: the repository, commit and directory, as the indexer states it */ - "fetch"?: unknown; + "artifacts": AskArtifact[]; + "fetch"?: Fetch; "fp"?: string; "module": string; "path"?: string; "publish"?: boolean; - "toolchain"?: StepAskBuildToolchain; + "toolchain"?: ToolchainUsed; } export interface StepAskCheck { - /** what the build module fetches: the repository, commit and directory, as the indexer states it */ - "fetch": unknown; + "fetch": Fetch; "kept"?: boolean; "levels": string[]; "packages"?: string[]; @@ -169,8 +181,7 @@ export interface StepAskCheck { export interface StepAskCompose { "dir": string; - /** what the build module fetches: the repository, commit and directory, as the indexer states it */ - "fetch": unknown; + "fetch": Fetch; "package": string; "reader"?: string; /** the compose-candidate request */ @@ -197,6 +208,26 @@ export interface StepAsk { "v"?: number; } +/** what a step answered, by the kind of ask: a build's artifacts, toolchain, imports and out-of-step modules; a check's fetch and levels; a compose's built, key and verdict; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the four */ +export interface StepResult { + "artifacts"?: BuiltArtifact[]; + "built"?: boolean; + /** the holder declined the ask: its node lacks what the ask needs */ + "declined"?: boolean; + "fetch"?: Fetch; + "imports"?: string[]; + "key"?: string; + /** what the holder's node lacks of it */ + "lacks"?: string[]; + "levels"?: LevelAnswer[]; + /** what the declined ask needs */ + "needs"?: string[]; + "out-of-step"?: string[]; + "toolchain"?: ToolchainUsed; + /** the compose-candidate verdict (the composer's own layout) */ + "verdict"?: unknown; +} + /** the toolchain a node reports */ export interface Toolchain { /** when (RFC 3339 time) */ @@ -209,6 +240,13 @@ export interface Toolchain { "version": string; } +/** the toolchain a build was made with: the language, the installed version and the build module's recipe */ +export interface ToolchainUsed { + "language": string; + "recipe": string; + "version": string; +} + /** is what current takes. */ export interface CurrentInput { } diff --git a/contracts/ts/seats/nodebuildimage/src/index.ts b/contracts/ts/seats/nodebuildimage/src/index.ts index b289e8f7..f0312a18 100644 --- a/contracts/ts/seats/nodebuildimage/src/index.ts +++ b/contracts/ts/seats/nodebuildimage/src/index.ts @@ -102,20 +102,22 @@ export function parseToolchainsByNodeLanguageKey(key: string): { node: string; l return { node, language }; } +export type AskArtifact = buildStep.AskArtifact; export type BuildAnswer = buildStep.BuildAnswer; -export type BuildAnswerArtifactsItem = buildStep.BuildAnswerArtifactsItem; -export type BuildAnswerToolchain = buildStep.BuildAnswerToolchain; +export type BuiltArtifact = buildStep.BuiltArtifact; export type Capacity = buildStep.Capacity; export type CheckAnswer = buildStep.CheckAnswer; -export type CheckAnswerLevelsItem = buildStep.CheckAnswerLevelsItem; -export type CheckAnswerLevelsItemDigest = buildStep.CheckAnswerLevelsItemDigest; -export type CheckAnswerLevelsItemDigestNeedsItem = buildStep.CheckAnswerLevelsItemDigestNeedsItem; -export type CheckAnswerLevelsItemDigestPackagesItem = buildStep.CheckAnswerLevelsItemDigestPackagesItem; -export type CheckAnswerLevelsItemKeptItem = buildStep.CheckAnswerLevelsItemKeptItem; -export type CheckAnswerLevelsItemUnmetItem = buildStep.CheckAnswerLevelsItemUnmetItem; export type ComposeAnswer = buildStep.ComposeAnswer; export type CurrentAnswer = buildStep.CurrentAnswer; export type CurrentInput = buildStep.CurrentInput; +export type Fetch = buildStep.Fetch; +export type FetchBesideItem = buildStep.FetchBesideItem; +export type LevelAnswer = buildStep.LevelAnswer; +export type LevelAnswerDigest = buildStep.LevelAnswerDigest; +export type LevelAnswerDigestNeedsItem = buildStep.LevelAnswerDigestNeedsItem; +export type LevelAnswerDigestPackagesItem = buildStep.LevelAnswerDigestPackagesItem; +export type LevelAnswerKeptItem = buildStep.LevelAnswerKeptItem; +export type LevelAnswerUnmetItem = buildStep.LevelAnswerUnmetItem; export type Pause = buildStep.Pause; export type PauseAnswer = buildStep.PauseAnswer; export type PauseInput = buildStep.PauseInput; @@ -124,11 +126,11 @@ export type ResumeInput = buildStep.ResumeInput; export type Step = buildStep.Step; export type StepAsk = buildStep.StepAsk; export type StepAskBuild = buildStep.StepAskBuild; -export type StepAskBuildArtifactsItem = buildStep.StepAskBuildArtifactsItem; -export type StepAskBuildToolchain = buildStep.StepAskBuildToolchain; export type StepAskCheck = buildStep.StepAskCheck; export type StepAskCompose = buildStep.StepAskCompose; +export type StepResult = buildStep.StepResult; export type Toolchain = buildStep.Toolchain; +export type ToolchainUsed = buildStep.ToolchainUsed; /** is the body of delivery. */ export type DeliveryAccept = StepAsk; diff --git a/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts b/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts index fa5146dc..4ca1eb95 100644 --- a/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts +++ b/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts @@ -8,23 +8,26 @@ export const verbs = { resume: "resume", } as const; -export interface BuildAnswerArtifactsItem { +/** one artifact of a build ask: built from the fetched tree, or copied from an upstream pin */ +export interface AskArtifact { "artifact": string; - "digest": string; - "reference"?: string; -} - -export interface BuildAnswerToolchain { - "language": string; - "recipe": string; - "version": string; + "from"?: string; + "seat"?: string; + "version"?: string; } export interface BuildAnswer { - "artifacts": BuildAnswerArtifactsItem[]; + "artifacts": BuiltArtifact[]; "imports"?: string[]; "out-of-step"?: string[]; - "toolchain"?: BuildAnswerToolchain; + "toolchain"?: ToolchainUsed; +} + +/** one artifact a build made: its digest and the reference it was published under */ +export interface BuiltArtifact { + "artifact": string; + "digest": string; + "reference"?: string; } /** how many steps the node runs at once */ @@ -47,49 +50,9 @@ export interface Capacity { "why": string; } -export interface CheckAnswerLevelsItemDigestNeedsItem { - "needs"?: string[]; - "package"?: string; -} - -export interface CheckAnswerLevelsItemDigestPackagesItem { - "digest": string; - "package": string; -} - -export interface CheckAnswerLevelsItemDigest { - "beside"?: string[]; - "judged_by"?: string; - "level"?: string; - "needs"?: CheckAnswerLevelsItemDigestNeedsItem[]; - "packages"?: CheckAnswerLevelsItemDigestPackagesItem[]; - "without_tests"?: string[]; -} - -export interface CheckAnswerLevelsItemKeptItem { - "digest"?: string; - "package"?: string; - "tests"?: number; -} - -export interface CheckAnswerLevelsItemUnmetItem { - "needs"?: string[]; - "package"?: string; -} - -export interface CheckAnswerLevelsItem { - "digest"?: CheckAnswerLevelsItemDigest; - "judged"?: string; - "kept"?: CheckAnswerLevelsItemKeptItem[]; - "level": string; - "tests"?: unknown[]; - "unmet"?: CheckAnswerLevelsItemUnmetItem[]; -} - export interface CheckAnswer { - /** what the build module fetches: the repository, commit and directory, as the indexer states it */ - "fetch": unknown; - "levels"?: CheckAnswerLevelsItem[]; + "fetch": Fetch; + "levels"?: LevelAnswer[]; } export interface ComposeAnswer { @@ -99,6 +62,71 @@ export interface ComposeAnswer { "verdict"?: unknown; } +export interface FetchBesideItem { + "commit": string; + "repository": string; +} + +/** what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside */ +export interface Fetch { + /** the trunk commit a merge preview merges onto */ + "base"?: string; + /** the sibling repositories placed beside the fetched tree */ + "beside"?: FetchBesideItem[]; + /** the full commit */ + "commit": string; + /** the module's directory in the repository ("." for its root), with paths */ + "dir"?: string; + /** the version (a full commit) of each mesh package to build against, by its module path */ + "packages"?: Record; + /** the paths the module names, relative to dir */ + "paths"?: string[]; + /** the pull request's ref, when only its head carries the commit */ + "ref"?: string; + /** / */ + "repository": string; +} + +export interface LevelAnswerDigestNeedsItem { + "needs"?: string[]; + "package"?: string; +} + +export interface LevelAnswerDigestPackagesItem { + "digest": string; + "package": string; +} + +export interface LevelAnswerDigest { + "beside"?: string[]; + "judged_by"?: string; + "level"?: string; + "needs"?: LevelAnswerDigestNeedsItem[]; + "packages"?: LevelAnswerDigestPackagesItem[]; + "without_tests"?: string[]; +} + +export interface LevelAnswerKeptItem { + "digest"?: string; + "package"?: string; + "tests"?: number; +} + +export interface LevelAnswerUnmetItem { + "needs"?: string[]; + "package"?: string; +} + +/** one level of a check ask: the digest's answer, the kept verdicts taken, one test result per test step run, and the packages the node could not test */ +export interface LevelAnswer { + "digest"?: LevelAnswerDigest; + "judged"?: string; + "kept"?: LevelAnswerKeptItem[]; + "level": string; + "tests"?: unknown[]; + "unmet"?: LevelAnswerUnmetItem[]; +} + /** whether the node takes no new ask */ export interface Pause { /** when (RFC 3339 time) */ @@ -120,8 +148,7 @@ export interface Step { /** when (RFC 3339 time) */ "phase-at"?: string; "reason"?: string; - /** what the step answered */ - "result"?: unknown; + "result"?: StepResult; /** when (RFC 3339 time) */ "started": string; /** running, passed, failed or refused */ @@ -132,33 +159,18 @@ export interface Step { "what"?: string; } -export interface StepAskBuildArtifactsItem { - "artifact": string; - "from"?: string; - "seat"?: string; - "version"?: string; -} - -export interface StepAskBuildToolchain { - "language": string; - "recipe": string; - "version": string; -} - export interface StepAskBuild { - "artifacts": StepAskBuildArtifactsItem[]; - /** what the build module fetches: the repository, commit and directory, as the indexer states it */ - "fetch"?: unknown; + "artifacts": AskArtifact[]; + "fetch"?: Fetch; "fp"?: string; "module": string; "path"?: string; "publish"?: boolean; - "toolchain"?: StepAskBuildToolchain; + "toolchain"?: ToolchainUsed; } export interface StepAskCheck { - /** what the build module fetches: the repository, commit and directory, as the indexer states it */ - "fetch": unknown; + "fetch": Fetch; "kept"?: boolean; "levels": string[]; "packages"?: string[]; @@ -169,8 +181,7 @@ export interface StepAskCheck { export interface StepAskCompose { "dir": string; - /** what the build module fetches: the repository, commit and directory, as the indexer states it */ - "fetch": unknown; + "fetch": Fetch; "package": string; "reader"?: string; /** the compose-candidate request */ @@ -197,6 +208,26 @@ export interface StepAsk { "v"?: number; } +/** what a step answered, by the kind of ask: a build's artifacts, toolchain, imports and out-of-step modules; a check's fetch and levels; a compose's built, key and verdict; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the four */ +export interface StepResult { + "artifacts"?: BuiltArtifact[]; + "built"?: boolean; + /** the holder declined the ask: its node lacks what the ask needs */ + "declined"?: boolean; + "fetch"?: Fetch; + "imports"?: string[]; + "key"?: string; + /** what the holder's node lacks of it */ + "lacks"?: string[]; + "levels"?: LevelAnswer[]; + /** what the declined ask needs */ + "needs"?: string[]; + "out-of-step"?: string[]; + "toolchain"?: ToolchainUsed; + /** the compose-candidate verdict (the composer's own layout) */ + "verdict"?: unknown; +} + /** the toolchain a node reports */ export interface Toolchain { /** when (RFC 3339 time) */ @@ -209,6 +240,13 @@ export interface Toolchain { "version": string; } +/** the toolchain a build was made with: the language, the installed version and the build module's recipe */ +export interface ToolchainUsed { + "language": string; + "recipe": string; + "version": string; +} + /** is what current takes. */ export interface CurrentInput { } diff --git a/contracts/ts/seats/nodebuildnode/src/index.ts b/contracts/ts/seats/nodebuildnode/src/index.ts index d84ca0ae..99f5963c 100644 --- a/contracts/ts/seats/nodebuildnode/src/index.ts +++ b/contracts/ts/seats/nodebuildnode/src/index.ts @@ -102,20 +102,22 @@ export function parseToolchainsByNodeLanguageKey(key: string): { node: string; l return { node, language }; } +export type AskArtifact = buildStep.AskArtifact; export type BuildAnswer = buildStep.BuildAnswer; -export type BuildAnswerArtifactsItem = buildStep.BuildAnswerArtifactsItem; -export type BuildAnswerToolchain = buildStep.BuildAnswerToolchain; +export type BuiltArtifact = buildStep.BuiltArtifact; export type Capacity = buildStep.Capacity; export type CheckAnswer = buildStep.CheckAnswer; -export type CheckAnswerLevelsItem = buildStep.CheckAnswerLevelsItem; -export type CheckAnswerLevelsItemDigest = buildStep.CheckAnswerLevelsItemDigest; -export type CheckAnswerLevelsItemDigestNeedsItem = buildStep.CheckAnswerLevelsItemDigestNeedsItem; -export type CheckAnswerLevelsItemDigestPackagesItem = buildStep.CheckAnswerLevelsItemDigestPackagesItem; -export type CheckAnswerLevelsItemKeptItem = buildStep.CheckAnswerLevelsItemKeptItem; -export type CheckAnswerLevelsItemUnmetItem = buildStep.CheckAnswerLevelsItemUnmetItem; export type ComposeAnswer = buildStep.ComposeAnswer; export type CurrentAnswer = buildStep.CurrentAnswer; export type CurrentInput = buildStep.CurrentInput; +export type Fetch = buildStep.Fetch; +export type FetchBesideItem = buildStep.FetchBesideItem; +export type LevelAnswer = buildStep.LevelAnswer; +export type LevelAnswerDigest = buildStep.LevelAnswerDigest; +export type LevelAnswerDigestNeedsItem = buildStep.LevelAnswerDigestNeedsItem; +export type LevelAnswerDigestPackagesItem = buildStep.LevelAnswerDigestPackagesItem; +export type LevelAnswerKeptItem = buildStep.LevelAnswerKeptItem; +export type LevelAnswerUnmetItem = buildStep.LevelAnswerUnmetItem; export type Pause = buildStep.Pause; export type PauseAnswer = buildStep.PauseAnswer; export type PauseInput = buildStep.PauseInput; @@ -124,11 +126,11 @@ export type ResumeInput = buildStep.ResumeInput; export type Step = buildStep.Step; export type StepAsk = buildStep.StepAsk; export type StepAskBuild = buildStep.StepAskBuild; -export type StepAskBuildArtifactsItem = buildStep.StepAskBuildArtifactsItem; -export type StepAskBuildToolchain = buildStep.StepAskBuildToolchain; export type StepAskCheck = buildStep.StepAskCheck; export type StepAskCompose = buildStep.StepAskCompose; +export type StepResult = buildStep.StepResult; export type Toolchain = buildStep.Toolchain; +export type ToolchainUsed = buildStep.ToolchainUsed; /** is the body of delivery. */ export type DeliveryAccept = StepAsk; diff --git a/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts b/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts index fa5146dc..4ca1eb95 100644 --- a/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts +++ b/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts @@ -8,23 +8,26 @@ export const verbs = { resume: "resume", } as const; -export interface BuildAnswerArtifactsItem { +/** one artifact of a build ask: built from the fetched tree, or copied from an upstream pin */ +export interface AskArtifact { "artifact": string; - "digest": string; - "reference"?: string; -} - -export interface BuildAnswerToolchain { - "language": string; - "recipe": string; - "version": string; + "from"?: string; + "seat"?: string; + "version"?: string; } export interface BuildAnswer { - "artifacts": BuildAnswerArtifactsItem[]; + "artifacts": BuiltArtifact[]; "imports"?: string[]; "out-of-step"?: string[]; - "toolchain"?: BuildAnswerToolchain; + "toolchain"?: ToolchainUsed; +} + +/** one artifact a build made: its digest and the reference it was published under */ +export interface BuiltArtifact { + "artifact": string; + "digest": string; + "reference"?: string; } /** how many steps the node runs at once */ @@ -47,49 +50,9 @@ export interface Capacity { "why": string; } -export interface CheckAnswerLevelsItemDigestNeedsItem { - "needs"?: string[]; - "package"?: string; -} - -export interface CheckAnswerLevelsItemDigestPackagesItem { - "digest": string; - "package": string; -} - -export interface CheckAnswerLevelsItemDigest { - "beside"?: string[]; - "judged_by"?: string; - "level"?: string; - "needs"?: CheckAnswerLevelsItemDigestNeedsItem[]; - "packages"?: CheckAnswerLevelsItemDigestPackagesItem[]; - "without_tests"?: string[]; -} - -export interface CheckAnswerLevelsItemKeptItem { - "digest"?: string; - "package"?: string; - "tests"?: number; -} - -export interface CheckAnswerLevelsItemUnmetItem { - "needs"?: string[]; - "package"?: string; -} - -export interface CheckAnswerLevelsItem { - "digest"?: CheckAnswerLevelsItemDigest; - "judged"?: string; - "kept"?: CheckAnswerLevelsItemKeptItem[]; - "level": string; - "tests"?: unknown[]; - "unmet"?: CheckAnswerLevelsItemUnmetItem[]; -} - export interface CheckAnswer { - /** what the build module fetches: the repository, commit and directory, as the indexer states it */ - "fetch": unknown; - "levels"?: CheckAnswerLevelsItem[]; + "fetch": Fetch; + "levels"?: LevelAnswer[]; } export interface ComposeAnswer { @@ -99,6 +62,71 @@ export interface ComposeAnswer { "verdict"?: unknown; } +export interface FetchBesideItem { + "commit": string; + "repository": string; +} + +/** what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside */ +export interface Fetch { + /** the trunk commit a merge preview merges onto */ + "base"?: string; + /** the sibling repositories placed beside the fetched tree */ + "beside"?: FetchBesideItem[]; + /** the full commit */ + "commit": string; + /** the module's directory in the repository ("." for its root), with paths */ + "dir"?: string; + /** the version (a full commit) of each mesh package to build against, by its module path */ + "packages"?: Record; + /** the paths the module names, relative to dir */ + "paths"?: string[]; + /** the pull request's ref, when only its head carries the commit */ + "ref"?: string; + /** / */ + "repository": string; +} + +export interface LevelAnswerDigestNeedsItem { + "needs"?: string[]; + "package"?: string; +} + +export interface LevelAnswerDigestPackagesItem { + "digest": string; + "package": string; +} + +export interface LevelAnswerDigest { + "beside"?: string[]; + "judged_by"?: string; + "level"?: string; + "needs"?: LevelAnswerDigestNeedsItem[]; + "packages"?: LevelAnswerDigestPackagesItem[]; + "without_tests"?: string[]; +} + +export interface LevelAnswerKeptItem { + "digest"?: string; + "package"?: string; + "tests"?: number; +} + +export interface LevelAnswerUnmetItem { + "needs"?: string[]; + "package"?: string; +} + +/** one level of a check ask: the digest's answer, the kept verdicts taken, one test result per test step run, and the packages the node could not test */ +export interface LevelAnswer { + "digest"?: LevelAnswerDigest; + "judged"?: string; + "kept"?: LevelAnswerKeptItem[]; + "level": string; + "tests"?: unknown[]; + "unmet"?: LevelAnswerUnmetItem[]; +} + /** whether the node takes no new ask */ export interface Pause { /** when (RFC 3339 time) */ @@ -120,8 +148,7 @@ export interface Step { /** when (RFC 3339 time) */ "phase-at"?: string; "reason"?: string; - /** what the step answered */ - "result"?: unknown; + "result"?: StepResult; /** when (RFC 3339 time) */ "started": string; /** running, passed, failed or refused */ @@ -132,33 +159,18 @@ export interface Step { "what"?: string; } -export interface StepAskBuildArtifactsItem { - "artifact": string; - "from"?: string; - "seat"?: string; - "version"?: string; -} - -export interface StepAskBuildToolchain { - "language": string; - "recipe": string; - "version": string; -} - export interface StepAskBuild { - "artifacts": StepAskBuildArtifactsItem[]; - /** what the build module fetches: the repository, commit and directory, as the indexer states it */ - "fetch"?: unknown; + "artifacts": AskArtifact[]; + "fetch"?: Fetch; "fp"?: string; "module": string; "path"?: string; "publish"?: boolean; - "toolchain"?: StepAskBuildToolchain; + "toolchain"?: ToolchainUsed; } export interface StepAskCheck { - /** what the build module fetches: the repository, commit and directory, as the indexer states it */ - "fetch": unknown; + "fetch": Fetch; "kept"?: boolean; "levels": string[]; "packages"?: string[]; @@ -169,8 +181,7 @@ export interface StepAskCheck { export interface StepAskCompose { "dir": string; - /** what the build module fetches: the repository, commit and directory, as the indexer states it */ - "fetch": unknown; + "fetch": Fetch; "package": string; "reader"?: string; /** the compose-candidate request */ @@ -197,6 +208,26 @@ export interface StepAsk { "v"?: number; } +/** what a step answered, by the kind of ask: a build's artifacts, toolchain, imports and out-of-step modules; a check's fetch and levels; a compose's built, key and verdict; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the four */ +export interface StepResult { + "artifacts"?: BuiltArtifact[]; + "built"?: boolean; + /** the holder declined the ask: its node lacks what the ask needs */ + "declined"?: boolean; + "fetch"?: Fetch; + "imports"?: string[]; + "key"?: string; + /** what the holder's node lacks of it */ + "lacks"?: string[]; + "levels"?: LevelAnswer[]; + /** what the declined ask needs */ + "needs"?: string[]; + "out-of-step"?: string[]; + "toolchain"?: ToolchainUsed; + /** the compose-candidate verdict (the composer's own layout) */ + "verdict"?: unknown; +} + /** the toolchain a node reports */ export interface Toolchain { /** when (RFC 3339 time) */ @@ -209,6 +240,13 @@ export interface Toolchain { "version": string; } +/** the toolchain a build was made with: the language, the installed version and the build module's recipe */ +export interface ToolchainUsed { + "language": string; + "recipe": string; + "version": string; +} + /** is what current takes. */ export interface CurrentInput { }