diff --git a/contracts/go/clients/containerimageregistryclient/client.go b/contracts/go/clients/containerimageregistryclient/client.go new file mode 100644 index 00000000..3616faf7 --- /dev/null +++ b/contracts/go/clients/containerimageregistryclient/client.go @@ -0,0 +1,63 @@ +// Package containerimageregistryclient is the container-image-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51 +// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part, +// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a +// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is +// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the +// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport. +package containerimageregistryclient + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry" + "git.novox.be/novox/mesh-delivery/sdk/parts" +) + +// Call asks one verb of the seat, `seat:.`, with a body, and answers its JSON. +type Call func(key string, body any) (json.RawMessage, error) + +// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were +// sent, and the chunks they took. +type Sent struct { + Transfer string + parts.Sent +} + +// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in +// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes +// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read. +// Its outcome, published or refused, is the holder's event, which the caller waits for. +func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) { + open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) { + in.Parts = make([]seat.Part, 0, len(ps)) + for _, p := range ps { + in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size}) + } + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in) + if err != nil { + return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err) + } + var a seat.PublishAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err) + } + return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil + } + chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error { + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part, + Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)}) + if err != nil { + return fmt.Errorf("%s's publish-part: %w", seat.Seat, err) + } + var a seat.PublishPartAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err) + } + return nil + } + sent, err := parts.Send(ctx, open, chunk, ps) + return Sent{Transfer: in.Transfer, Sent: sent}, err +} diff --git a/contracts/go/clients/containerimageregistryclient/client_test.go b/contracts/go/clients/containerimageregistryclient/client_test.go new file mode 100644 index 00000000..7b77d5f5 --- /dev/null +++ b/contracts/go/clients/containerimageregistryclient/client_test.go @@ -0,0 +1,57 @@ +package containerimageregistryclient + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "io" + "testing" + + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry" + "git.novox.be/novox/mesh-delivery/sdk/parts" +) + +// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the +// holder lacks travels by publish-part, its bytes whole, the last chunk marked. +func TestPublishTalksToItsOwnSeat(t *testing.T) { + held, lacked := []byte("held"), []byte("lacked") + part := func(b []byte) parts.Part { + sum := sha256.Sum256(b) + return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)), + Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }} + } + ps := []parts.Part{part(held), part(lacked)} + var asked []string + var got []byte + call := func(key string, body any) (json.RawMessage, error) { + asked = append(asked, key) + raw, _ := json.Marshal(body) + switch key { + case "seat:container-image-registry.publish": + var in seat.PublishInput + _ = json.Unmarshal(raw, &in) + if len(in.Parts) != 2 { + t.Errorf("publish names %d parts, want 2", len(in.Parts)) + } + return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}}) + case "seat:container-image-registry.publish-part": + var in seat.PublishPartInput + _ = json.Unmarshal(raw, &in) + data, _ := base64.StdEncoding.DecodeString(in.Data) + if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" { + t.Errorf("publish-part %+v", in) + } + got = append(got, data...) + return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part}) + } + t.Fatalf("asked %s", key) + return nil, nil + } + sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps) + if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 { + t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err) + } +} diff --git a/contracts/go/clients/containerimageregistryclient/go.mod b/contracts/go/clients/containerimageregistryclient/go.mod new file mode 100644 index 00000000..25546684 --- /dev/null +++ b/contracts/go/clients/containerimageregistryclient/go.mod @@ -0,0 +1,18 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0 + git.novox.be/novox/mesh-delivery/sdk 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/containerimageregistryclient/go.sum b/contracts/go/clients/containerimageregistryclient/go.sum new file mode 100644 index 00000000..c811fda7 --- /dev/null +++ b/contracts/go/clients/containerimageregistryclient/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/go/clients/gopackageregistryclient/client.go b/contracts/go/clients/gopackageregistryclient/client.go new file mode 100644 index 00000000..39cdb75f --- /dev/null +++ b/contracts/go/clients/gopackageregistryclient/client.go @@ -0,0 +1,63 @@ +// Package gopackageregistryclient is the go-package-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51 +// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part, +// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a +// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is +// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the +// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport. +package gopackageregistryclient + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry" + "git.novox.be/novox/mesh-delivery/sdk/parts" +) + +// Call asks one verb of the seat, `seat:.`, with a body, and answers its JSON. +type Call func(key string, body any) (json.RawMessage, error) + +// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were +// sent, and the chunks they took. +type Sent struct { + Transfer string + parts.Sent +} + +// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in +// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes +// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read. +// Its outcome, published or refused, is the holder's event, which the caller waits for. +func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) { + open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) { + in.Parts = make([]seat.Part, 0, len(ps)) + for _, p := range ps { + in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size}) + } + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in) + if err != nil { + return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err) + } + var a seat.PublishAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err) + } + return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil + } + chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error { + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part, + Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)}) + if err != nil { + return fmt.Errorf("%s's publish-part: %w", seat.Seat, err) + } + var a seat.PublishPartAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err) + } + return nil + } + sent, err := parts.Send(ctx, open, chunk, ps) + return Sent{Transfer: in.Transfer, Sent: sent}, err +} diff --git a/contracts/go/clients/gopackageregistryclient/client_test.go b/contracts/go/clients/gopackageregistryclient/client_test.go new file mode 100644 index 00000000..3405e705 --- /dev/null +++ b/contracts/go/clients/gopackageregistryclient/client_test.go @@ -0,0 +1,57 @@ +package gopackageregistryclient + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "io" + "testing" + + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry" + "git.novox.be/novox/mesh-delivery/sdk/parts" +) + +// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the +// holder lacks travels by publish-part, its bytes whole, the last chunk marked. +func TestPublishTalksToItsOwnSeat(t *testing.T) { + held, lacked := []byte("held"), []byte("lacked") + part := func(b []byte) parts.Part { + sum := sha256.Sum256(b) + return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)), + Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }} + } + ps := []parts.Part{part(held), part(lacked)} + var asked []string + var got []byte + call := func(key string, body any) (json.RawMessage, error) { + asked = append(asked, key) + raw, _ := json.Marshal(body) + switch key { + case "seat:go-package-registry.publish": + var in seat.PublishInput + _ = json.Unmarshal(raw, &in) + if len(in.Parts) != 2 { + t.Errorf("publish names %d parts, want 2", len(in.Parts)) + } + return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}}) + case "seat:go-package-registry.publish-part": + var in seat.PublishPartInput + _ = json.Unmarshal(raw, &in) + data, _ := base64.StdEncoding.DecodeString(in.Data) + if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" { + t.Errorf("publish-part %+v", in) + } + got = append(got, data...) + return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part}) + } + t.Fatalf("asked %s", key) + return nil, nil + } + sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps) + if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 { + t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err) + } +} diff --git a/contracts/go/clients/gopackageregistryclient/go.mod b/contracts/go/clients/gopackageregistryclient/go.mod new file mode 100644 index 00000000..d4b84471 --- /dev/null +++ b/contracts/go/clients/gopackageregistryclient/go.mod @@ -0,0 +1,18 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/clients/gopackageregistryclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry v0.0.0 + git.novox.be/novox/mesh-delivery/sdk 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/gopackageregistryclient/go.sum b/contracts/go/clients/gopackageregistryclient/go.sum new file mode 100644 index 00000000..1ab0e3c0 --- /dev/null +++ b/contracts/go/clients/gopackageregistryclient/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/gopackageregistry v0.0.0-20261105000000-000000000000 h1:Zl6CzMg9AZtk7RU/DSJxSGGoUg5fSW3t7Yf8zyfXbVY= +git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry v0.0.0-20261105000000-000000000000/go.mod h1:Y5kgBh0hBbL/hgc+abCF9Vbhov+vCsB0ckpRzUw1D+0= +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/go/clients/npmpackageregistryclient/client.go b/contracts/go/clients/npmpackageregistryclient/client.go new file mode 100644 index 00000000..82625d6e --- /dev/null +++ b/contracts/go/clients/npmpackageregistryclient/client.go @@ -0,0 +1,63 @@ +// Package npmpackageregistryclient is the npm-package-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51 +// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part, +// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a +// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is +// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the +// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport. +package npmpackageregistryclient + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry" + "git.novox.be/novox/mesh-delivery/sdk/parts" +) + +// Call asks one verb of the seat, `seat:.`, with a body, and answers its JSON. +type Call func(key string, body any) (json.RawMessage, error) + +// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were +// sent, and the chunks they took. +type Sent struct { + Transfer string + parts.Sent +} + +// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in +// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes +// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read. +// Its outcome, published or refused, is the holder's event, which the caller waits for. +func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) { + open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) { + in.Parts = make([]seat.Part, 0, len(ps)) + for _, p := range ps { + in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size}) + } + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in) + if err != nil { + return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err) + } + var a seat.PublishAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err) + } + return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil + } + chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error { + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part, + Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)}) + if err != nil { + return fmt.Errorf("%s's publish-part: %w", seat.Seat, err) + } + var a seat.PublishPartAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err) + } + return nil + } + sent, err := parts.Send(ctx, open, chunk, ps) + return Sent{Transfer: in.Transfer, Sent: sent}, err +} diff --git a/contracts/go/clients/npmpackageregistryclient/client_test.go b/contracts/go/clients/npmpackageregistryclient/client_test.go new file mode 100644 index 00000000..916ba073 --- /dev/null +++ b/contracts/go/clients/npmpackageregistryclient/client_test.go @@ -0,0 +1,57 @@ +package npmpackageregistryclient + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "io" + "testing" + + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry" + "git.novox.be/novox/mesh-delivery/sdk/parts" +) + +// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the +// holder lacks travels by publish-part, its bytes whole, the last chunk marked. +func TestPublishTalksToItsOwnSeat(t *testing.T) { + held, lacked := []byte("held"), []byte("lacked") + part := func(b []byte) parts.Part { + sum := sha256.Sum256(b) + return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)), + Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }} + } + ps := []parts.Part{part(held), part(lacked)} + var asked []string + var got []byte + call := func(key string, body any) (json.RawMessage, error) { + asked = append(asked, key) + raw, _ := json.Marshal(body) + switch key { + case "seat:npm-package-registry.publish": + var in seat.PublishInput + _ = json.Unmarshal(raw, &in) + if len(in.Parts) != 2 { + t.Errorf("publish names %d parts, want 2", len(in.Parts)) + } + return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}}) + case "seat:npm-package-registry.publish-part": + var in seat.PublishPartInput + _ = json.Unmarshal(raw, &in) + data, _ := base64.StdEncoding.DecodeString(in.Data) + if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" { + t.Errorf("publish-part %+v", in) + } + got = append(got, data...) + return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part}) + } + t.Fatalf("asked %s", key) + return nil, nil + } + sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps) + if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 { + t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err) + } +} diff --git a/contracts/go/clients/npmpackageregistryclient/go.mod b/contracts/go/clients/npmpackageregistryclient/go.mod new file mode 100644 index 00000000..22bdecb7 --- /dev/null +++ b/contracts/go/clients/npmpackageregistryclient/go.mod @@ -0,0 +1,18 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/clients/npmpackageregistryclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry v0.0.0 + git.novox.be/novox/mesh-delivery/sdk 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/npmpackageregistryclient/go.sum b/contracts/go/clients/npmpackageregistryclient/go.sum new file mode 100644 index 00000000..1858987c --- /dev/null +++ b/contracts/go/clients/npmpackageregistryclient/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/npmpackageregistry v0.0.0-20261105000000-000000000000 h1:JdEqNAv+s0ut6JEcCtXN5RIoGpuYg0jIVteFZSJJ36s= +git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry v0.0.0-20261105000000-000000000000/go.mod h1:JRtTjIjMTkzn59IN4vCYei9ZwJGFnN1Puwame4V/sD0= +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/go/clients/ociartifactregistryclient/client.go b/contracts/go/clients/ociartifactregistryclient/client.go new file mode 100644 index 00000000..2892757d --- /dev/null +++ b/contracts/go/clients/ociartifactregistryclient/client.go @@ -0,0 +1,63 @@ +// Package ociartifactregistryclient is the oci-artifact-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51 +// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part, +// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a +// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is +// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the +// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport. +package ociartifactregistryclient + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry" + "git.novox.be/novox/mesh-delivery/sdk/parts" +) + +// Call asks one verb of the seat, `seat:.`, with a body, and answers its JSON. +type Call func(key string, body any) (json.RawMessage, error) + +// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were +// sent, and the chunks they took. +type Sent struct { + Transfer string + parts.Sent +} + +// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in +// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes +// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read. +// Its outcome, published or refused, is the holder's event, which the caller waits for. +func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) { + open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) { + in.Parts = make([]seat.Part, 0, len(ps)) + for _, p := range ps { + in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size}) + } + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in) + if err != nil { + return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err) + } + var a seat.PublishAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err) + } + return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil + } + chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error { + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part, + Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)}) + if err != nil { + return fmt.Errorf("%s's publish-part: %w", seat.Seat, err) + } + var a seat.PublishPartAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err) + } + return nil + } + sent, err := parts.Send(ctx, open, chunk, ps) + return Sent{Transfer: in.Transfer, Sent: sent}, err +} diff --git a/contracts/go/clients/ociartifactregistryclient/client_test.go b/contracts/go/clients/ociartifactregistryclient/client_test.go new file mode 100644 index 00000000..6c340fd1 --- /dev/null +++ b/contracts/go/clients/ociartifactregistryclient/client_test.go @@ -0,0 +1,57 @@ +package ociartifactregistryclient + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "io" + "testing" + + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry" + "git.novox.be/novox/mesh-delivery/sdk/parts" +) + +// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the +// holder lacks travels by publish-part, its bytes whole, the last chunk marked. +func TestPublishTalksToItsOwnSeat(t *testing.T) { + held, lacked := []byte("held"), []byte("lacked") + part := func(b []byte) parts.Part { + sum := sha256.Sum256(b) + return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)), + Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }} + } + ps := []parts.Part{part(held), part(lacked)} + var asked []string + var got []byte + call := func(key string, body any) (json.RawMessage, error) { + asked = append(asked, key) + raw, _ := json.Marshal(body) + switch key { + case "seat:oci-artifact-registry.publish": + var in seat.PublishInput + _ = json.Unmarshal(raw, &in) + if len(in.Parts) != 2 { + t.Errorf("publish names %d parts, want 2", len(in.Parts)) + } + return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}}) + case "seat:oci-artifact-registry.publish-part": + var in seat.PublishPartInput + _ = json.Unmarshal(raw, &in) + data, _ := base64.StdEncoding.DecodeString(in.Data) + if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" { + t.Errorf("publish-part %+v", in) + } + got = append(got, data...) + return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part}) + } + t.Fatalf("asked %s", key) + return nil, nil + } + sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps) + if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 { + t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err) + } +} diff --git a/contracts/go/clients/ociartifactregistryclient/go.mod b/contracts/go/clients/ociartifactregistryclient/go.mod new file mode 100644 index 00000000..c23078e0 --- /dev/null +++ b/contracts/go/clients/ociartifactregistryclient/go.mod @@ -0,0 +1,18 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/clients/ociartifactregistryclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0 + git.novox.be/novox/mesh-delivery/sdk 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/ociartifactregistryclient/go.sum b/contracts/go/clients/ociartifactregistryclient/go.sum new file mode 100644 index 00000000..c9a0ca56 --- /dev/null +++ b/contracts/go/clients/ociartifactregistryclient/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/ociartifactregistry v0.0.0-20261105000000-000000000000 h1:fhQ+q40fnPwuBBU+7Bpv13ktzoPxHtQiO6mpHR9oVoo= +git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0-20261105000000-000000000000/go.mod h1:ooQa2CiP8JOvhP0bdQvwurV5wskRE7Cm68ecURBbCwU= +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/go/seats/containerimageregistry/contract.go b/contracts/go/seats/containerimageregistry/contract.go new file mode 100644 index 00000000..80739294 --- /dev/null +++ b/contracts/go/seats/containerimageregistry/contract.go @@ -0,0 +1,528 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package containerimageregistry is the contract of the seat "container-image-registry", defined whole (seats/container-image-registry/seat.json): The mesh's container images, in the one artifact store: the OCI distribution protocol's read side, an image's tags and mirroring an outside image. +package containerimageregistry + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +// Seat is the seat's name. +const Seat = "container-image-registry" + +const ( + VerbPublish = "publish" + VerbPublishPart = "publish-part" + VerbVersions = "versions" + VerbRemove = "remove" + VerbMirror = "mirror" + VerbTags = "tags" + VerbHold = "hold" + VerbRelease = "release" + VerbList = "list" +) + +const ( + EventPublished = "published" + EventRefused = "refused" + EventRemoved = "removed" + EventKept = "kept" + EventHeld = "held" + EventReleased = "released" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove, VerbMirror, VerbTags, VerbHold, VerbRelease, VerbList} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventPublished, EventRefused, EventRemoved, EventKept, EventHeld, EventReleased} +} + +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. +const ( + Family = "container-image-registry.events" + FamilyMaxAgeDays = 7 +) + +// SubjectPublished is where published is published. +const SubjectPublished = "mesh.seat.container-image-registry.event.published" + +// SubjectRefused is where refused is published. +const SubjectRefused = "mesh.seat.container-image-registry.event.refused" + +// SubjectRemoved is where removed is published. +const SubjectRemoved = "mesh.seat.container-image-registry.event.removed" + +// SubjectKept is where kept is published. +const SubjectKept = "mesh.seat.container-image-registry.event.kept" + +// SubjectHeld is where held is published. +const SubjectHeld = "mesh.seat.container-image-registry.event.held" + +// SubjectReleased is where released is published. +const SubjectReleased = "mesh.seat.container-image-registry.event.released" + +const ( + StateHolds = "holds" + StateWindows = "windows" +) + +// HoldsBySeatNameDigestKey is the key of one value of the state holds under the form "..". +func HoldsBySeatNameDigestKey(seat, name, digest string) string { + return seat + "." + name + "." + digest +} + +// ParseHoldsBySeatNameDigestKey reads the parts of a key of the form "..", in the order of the form; the last result is false for a key of another form. +func ParseHoldsBySeatNameDigestKey(key string) (string, string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", "", false + } + rest = after0 + v2, after2, found2 := strings.Cut(rest, ".") + if !found2 || v2 == "" { + return "", "", "", false + } + rest = after2 + if rest == "" { + return "", "", "", false + } + v4 := rest + rest = "" + if rest != "" { + return "", "", "", false + } + return v0, v2, v4, true +} + +// DecodeHoldsBySeatNameDigestValue reads one value of the state holds under the form "..", refusing a field HoldsBySeatNameDigestValue does not know and any text after the value. +func DecodeHoldsBySeatNameDigestValue(raw []byte) (HoldsBySeatNameDigestValue, error) { + var v HoldsBySeatNameDigestValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// WindowsByCallerKey is the key of one value of the state windows under the form "". +func WindowsByCallerKey(caller string) string { return caller } + +// ParseWindowsByCallerKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseWindowsByCallerKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeWindowsByCallerValue reads one value of the state windows under the form "", refusing a field WindowsByCallerValue does not know and any text after the value. +func DecodeWindowsByCallerValue(raw []byte) (WindowsByCallerValue, error) { + var v WindowsByCallerValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// Artifact is one published thing, as every registry event and answer names it. +type Artifact struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) + Name string `json:"name"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// The values of Artifact's kind. +const ( + ArtifactKindPackage = "package" + ArtifactKindImage = "image" + ArtifactKindBundle = "bundle" + ArtifactKindArchive = "archive" + ArtifactKindUpstream = "upstream" +) + +// Hold is the body of held and released. +type Hold struct { + // when, RFC 3339 + At string `json:"at,omitempty"` + // the caller the holder proved + By string `json:"by,omitempty"` + // the digest + Digest string `json:"digest"` + // the module holding the seat + Holder string `json:"holder"` + // the name + Name string `json:"name"` + // why + Reason string `json:"reason,omitempty"` +} + +// Listed is a type of the contract. +type Listed struct { + // for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by + Artifact string `json:"artifact,omitempty"` + // the manifest's digest, sha256: + Digest string `json:"digest"` + // a hold keeps it from retention + Held bool `json:"held"` + // it is an index, naming a manifest per platform + Index bool `json:"index"` + // for an index, the digests of the manifests it names + Platforms []string `json:"platforms,omitempty"` + // its bytes: its own content and every blob it names + Size int64 `json:"size"` + // the tags naming it, none for an untagged one + Tags []string `json:"tags,omitempty"` +} + +// Outcome is the body of published, refused and removed. +type Outcome struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // the module holding the seat that said it + Holder string `json:"holder"` + // what it is + Kind string `json:"kind"` + // the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact + Module string `json:"module,omitempty"` + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) + Name string `json:"name"` + // why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) + Reason string `json:"reason,omitempty"` + // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) + Reference string `json:"reference,omitempty"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // for kept: the other tags still naming its digest + Tags []string `json:"tags,omitempty"` + // the transfer it ends, when it ended one + Transfer string `json:"transfer,omitempty"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// The values of Outcome's kind. +const ( + OutcomeKindPackage = "package" + OutcomeKindImage = "image" + OutcomeKindBundle = "bundle" + OutcomeKindArchive = "archive" + OutcomeKindUpstream = "upstream" +) + +// Part is one part of a transfer. +type Part struct { + // the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob + Digest string `json:"digest"` + // its size in bytes + Size int64 `json:"size"` +} + +// Repository is a type of the contract. +type Repository struct { + // each manifest it holds, by digest + Manifests []Listed `json:"manifests"` + // the repository + Name string `json:"name"` +} + +// PublishInput is what publish takes. +type PublishInput struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) + Name string `json:"name"` + // every part's digest and size; none when the digest is already held + Parts []Part `json:"parts,omitempty"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the transfer's name, the caller's own; the same name opens the same transfer again + Transfer string `json:"transfer"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// The values of PublishInput's kind. +const ( + PublishInputKindPackage = "package" + PublishInputKindImage = "image" + PublishInputKindBundle = "bundle" + PublishInputKindArchive = "archive" + PublishInputKindUpstream = "upstream" +) + +// PublishAnswer is what publish answers. +type PublishAnswer struct { + // the digest is held already: nothing to send; the outcome follows as an event + Held bool `json:"held,omitempty"` + // the digests of the parts the holder lacks, to send by publish-part + Lacks []string `json:"lacks,omitempty"` + // the transfer + Transfer string `json:"transfer"` +} + +// PublishPartInput is what publish-part takes. +type PublishPartInput struct { + // the chunk's bytes, base64 (at most 760 KiB before encoding) + Data string `json:"data"` + // this chunk is the part's last + Last bool `json:"last"` + // the part's digest + Part string `json:"part"` + // the chunk's place in its part, from 0 + Sequence int64 `json:"sequence"` + // the transfer + Transfer string `json:"transfer"` +} + +// PublishPartAnswer is what publish-part answers. +type PublishPartAnswer struct { + // the part is whole and its bytes match its digest + Complete bool `json:"complete,omitempty"` + // the part + Part string `json:"part"` + // the chunks of the part held so far + Received int64 `json:"received,omitempty"` + // the transfer + Transfer string `json:"transfer"` +} + +// VersionsInput is what versions takes. +type VersionsInput struct { + // narrowed to one kind (optional) + Kind string `json:"kind,omitempty"` + // the name + Name string `json:"name"` +} + +// VersionsAnswer is what versions answers. +type VersionsAnswer struct { + // the name + Name string `json:"name"` + // newest first + Versions []Artifact `json:"versions"` +} + +// RemoveInput is what remove takes. +type RemoveInput struct { + // the digest (for a bundle, archive or copied base) + Digest string `json:"digest,omitempty"` + // the name + Name string `json:"name"` + // why it goes + Reason string `json:"reason"` + // the tag (for a tagged version) + Tag string `json:"tag,omitempty"` + // the version + Version string `json:"version,omitempty"` +} + +// The values of RemoveInput's reason. +const ( + RemoveInputReasonTagGone = "tag-gone" + RemoveInputReasonRetention = "retention" +) + +// RemoveAnswer is what remove answers. +type RemoveAnswer struct { + Artifact *Artifact `json:"artifact,omitempty"` + // it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) + Kept bool `json:"kept,omitempty"` + // why it was kept: shared-digest, another tag names its digest + Reason string `json:"reason,omitempty"` + // it is gone + Removed bool `json:"removed"` + // the other tags still naming its digest, when kept + Tags []string `json:"tags,omitempty"` +} + +// The values of RemoveAnswer's reason. +const ( + RemoveAnswerReasonSharedDigest = "shared-digest" +) + +// MirrorInput is what mirror takes. +type MirrorInput struct { + // its pinned digest, sha256: + Digest string `json:"digest"` + // the name it is kept under + Name string `json:"name"` + // the outside image's reference + Source string `json:"source"` +} + +// MirrorAnswer is what mirror answers. +type MirrorAnswer struct { + // the digest stored + Digest string `json:"digest"` + // the reference the seat answers + Reference string `json:"reference"` +} + +// TagsInput is what tags takes. +type TagsInput struct { + // the image + Name string `json:"name"` +} + +// TagsAnswerTagsItem is a type of the contract. +type TagsAnswerTagsItem struct { + // its digest + Digest string `json:"digest"` + // the tag + Tag string `json:"tag"` +} + +// TagsAnswer is what tags answers. +type TagsAnswer struct { + // the image + Name string `json:"name"` + // every tag + Tags []TagsAnswerTagsItem `json:"tags"` +} + +// HoldInput is what hold takes. +type HoldInput struct { + // the digest + Digest string `json:"digest"` + // the name + Name string `json:"name"` + // which of the sweep's rules keeps it + Reason string `json:"reason"` +} + +// HoldAnswer is what hold answers. +type HoldAnswer struct { + // it is held + Held bool `json:"held"` + // when the hold began, RFC 3339 + Since string `json:"since,omitempty"` +} + +// ReleaseInput is what release takes. +type ReleaseInput struct { + // the digest + Digest string `json:"digest"` + // the name + Name string `json:"name"` + // why it is let go + Reason string `json:"reason"` +} + +// ReleaseAnswer is what release answers. +type ReleaseAnswer struct { + // a hold was let go; false when there was none + Released bool `json:"released"` +} + +// ListInput is what list takes. +type ListInput struct { + // the repository, /; every repository of the seat when not given + Name string `json:"name,omitempty"` +} + +// ListAnswer is what list answers. +type ListAnswer struct { + // each repository named, in order of its name + Repositories []Repository `json:"repositories"` +} + +// PublishedEvent is the body of the event published. +type PublishedEvent = Outcome + +// RefusedEvent is the body of the event refused. +type RefusedEvent = Outcome + +// RemovedEvent is the body of the event removed. +type RemovedEvent = Outcome + +// KeptEvent is the body of the event kept. +type KeptEvent = Outcome + +// HeldEvent is the body of the event held. +type HeldEvent = Hold + +// ReleasedEvent is the body of the event released. +type ReleasedEvent = Hold + +// HoldsBySeatNameDigestValue is one value of the state holds under the key ... +type HoldsBySeatNameDigestValue struct { + By string `json:"by"` + Digest string `json:"digest"` + Kind string `json:"kind"` + Name string `json:"name"` + Seat string `json:"seat"` + // when (RFC 3339 time) + Since string `json:"since"` + Why string `json:"why,omitempty"` +} + +// WindowsByCallerValueDroppedItem is a type of the contract. +type WindowsByCallerValueDroppedItem struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + By string `json:"by,omitempty"` + Node string `json:"node"` + Why string `json:"why,omitempty"` +} + +// WindowsByCallerValue is one value of the state windows under the key . +type WindowsByCallerValue struct { + // when it last closed (RFC 3339 time) + Closed string `json:"closed,omitempty"` + // retired nodes dropped by hand + Dropped []WindowsByCallerValueDroppedItem `json:"dropped,omitempty"` + // the nodes holding it open + Holding []string `json:"holding,omitempty"` + LastHolding []string `json:"last_holding,omitempty"` + // the caller whose window it is + Name string `json:"name"` + Open bool `json:"open,omitempty"` + // node to when it began to hold + Since map[string]string `json:"since,omitempty"` + // of each node holding the window, why it holds it + Why map[string]string `json:"why,omitempty"` +} diff --git a/contracts/go/seats/containerimageregistry/contract_test.go b/contracts/go/seats/containerimageregistry/contract_test.go new file mode 100644 index 00000000..440e43fa --- /dev/null +++ b/contracts/go/seats/containerimageregistry/contract_test.go @@ -0,0 +1,82 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package containerimageregistry + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/container-image-registry/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "published on the seat's subject", subject: "mesh.seat.container-image-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.published$")}, + {name: "refused on the seat's subject", subject: "mesh.seat.container-image-registry.event.refused", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.refused$")}, + {name: "removed on the seat's subject", subject: "mesh.seat.container-image-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.removed$")}, + {name: "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", subject: "mesh.seat.container-image-registry.event.kept", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", + decode: func(b []byte) (any, error) { var v KeptEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.kept$")}, + {name: "held on the seat's subject", subject: "mesh.seat.container-image-registry.event.held", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", + decode: func(b []byte) (any, error) { var v HeldEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.held$")}, + {name: "released on the seat's subject", subject: "mesh.seat.container-image-registry.event.released", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", + decode: func(b []byte) (any, error) { var v ReleasedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.released$")}, + {name: "a hold", subject: "", given: "{\"by\":\"operator@shanks\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"kind\":\"base\",\"name\":\"base\",\"seat\":\"container-image-registry\",\"since\":\"2026-11-05T10:00:00Z\"}", wire: "{\"by\":\"operator@shanks\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"kind\":\"base\",\"name\":\"base\",\"seat\":\"container-image-registry\",\"since\":\"2026-11-05T10:00:00Z\"}", + decode: func(b []byte) (any, error) { + var v HoldsBySeatNameDigestValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "an open window", subject: "", given: "{\"holding\":[\"ace\"],\"name\":\"mesh-delivery-builder\",\"open\":true,\"since\":{\"ace\":\"2026-11-05T10:00:00Z\"}}", wire: "{\"holding\":[\"ace\"],\"name\":\"mesh-delivery-builder\",\"open\":true,\"since\":{\"ace\":\"2026-11-05T10:00:00Z\"}}", + decode: func(b []byte) (any, error) { var v WindowsByCallerValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"image\",\"later\":{\"x\":1},\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "publish answers what it lacks", subject: "", given: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", wire: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", + decode: func(b []byte) (any, error) { var v PublishAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a chunk of a part", subject: "", given: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", wire: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", + decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "the versions held", subject: "", given: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", + decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a release of what is not held answers released false", subject: "", given: "{\"released\":false}", wire: "{\"released\":false}", + decode: func(b []byte) (any, error) { var v ReleaseAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a tagged version sharing its digest with another tag is kept, not removed", subject: "", given: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", wire: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", + decode: func(b []byte) (any, error) { var v RemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "list answers an index with its platforms, its tag and its hold", subject: "", given: "{\"repositories\":[{\"manifests\":[{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"held\":false,\"index\":true,\"platforms\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"size\":2048,\"tags\":[\"3\"]},{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"held\":false,\"index\":false,\"size\":1024}],\"name\":\"mail/server\"}]}", wire: "{\"repositories\":[{\"manifests\":[{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"held\":false,\"index\":true,\"platforms\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"size\":2048,\"tags\":[\"3\"]},{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"held\":false,\"index\":false,\"size\":1024}],\"name\":\"mail/server\"}]}", + decode: func(b []byte) (any, error) { var v ListAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/contracts/go/seats/containerimageregistry/go.mod b/contracts/go/seats/containerimageregistry/go.mod new file mode 100644 index 00000000..26ccf02f --- /dev/null +++ b/contracts/go/seats/containerimageregistry/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry + +go 1.26.0 diff --git a/contracts/go/seats/gopackageregistry/contract.go b/contracts/go/seats/gopackageregistry/contract.go new file mode 100644 index 00000000..eb534618 --- /dev/null +++ b/contracts/go/seats/gopackageregistry/contract.go @@ -0,0 +1,262 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package gopackageregistry is the contract of the seat "go-package-registry", defined whole (seats/go-package-registry/seat.json): The mesh's Go modules: Go's module proxy protocol's read side, and publishing over the bus. +package gopackageregistry + +// Seat is the seat's name. +const Seat = "go-package-registry" + +const ( + VerbPublish = "publish" + VerbPublishPart = "publish-part" + VerbVersions = "versions" + VerbRemove = "remove" +) + +const ( + EventPublished = "published" + EventRefused = "refused" + EventRemoved = "removed" + EventKept = "kept" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventPublished, EventRefused, EventRemoved, EventKept} +} + +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. +const ( + Family = "go-package-registry.events" + FamilyMaxAgeDays = 7 +) + +// SubjectPublished is where published is published. +const SubjectPublished = "mesh.seat.go-package-registry.event.published" + +// SubjectRefused is where refused is published. +const SubjectRefused = "mesh.seat.go-package-registry.event.refused" + +// SubjectRemoved is where removed is published. +const SubjectRemoved = "mesh.seat.go-package-registry.event.removed" + +// SubjectKept is where kept is published. +const SubjectKept = "mesh.seat.go-package-registry.event.kept" + +// Artifact is one published thing, as every registry event and answer names it. +type Artifact struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) + Name string `json:"name"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// The values of Artifact's kind. +const ( + ArtifactKindPackage = "package" + ArtifactKindImage = "image" + ArtifactKindBundle = "bundle" + ArtifactKindArchive = "archive" + ArtifactKindUpstream = "upstream" +) + +// Outcome is the body of published, refused and removed. +type Outcome struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // the module holding the seat that said it + Holder string `json:"holder"` + // what it is + Kind string `json:"kind"` + // the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact + Module string `json:"module,omitempty"` + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) + Name string `json:"name"` + // why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) + Reason string `json:"reason,omitempty"` + // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) + Reference string `json:"reference,omitempty"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // for kept: the other tags still naming its digest + Tags []string `json:"tags,omitempty"` + // the transfer it ends, when it ended one + Transfer string `json:"transfer,omitempty"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// The values of Outcome's kind. +const ( + OutcomeKindPackage = "package" + OutcomeKindImage = "image" + OutcomeKindBundle = "bundle" + OutcomeKindArchive = "archive" + OutcomeKindUpstream = "upstream" +) + +// Part is one part of a transfer. +type Part struct { + // the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob + Digest string `json:"digest"` + // its size in bytes + Size int64 `json:"size"` +} + +// PublishInput is what publish takes. +type PublishInput struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) + Name string `json:"name"` + // every part's digest and size; none when the digest is already held + Parts []Part `json:"parts,omitempty"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the transfer's name, the caller's own; the same name opens the same transfer again + Transfer string `json:"transfer"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// The values of PublishInput's kind. +const ( + PublishInputKindPackage = "package" + PublishInputKindImage = "image" + PublishInputKindBundle = "bundle" + PublishInputKindArchive = "archive" + PublishInputKindUpstream = "upstream" +) + +// PublishAnswer is what publish answers. +type PublishAnswer struct { + // the digest is held already: nothing to send; the outcome follows as an event + Held bool `json:"held,omitempty"` + // the digests of the parts the holder lacks, to send by publish-part + Lacks []string `json:"lacks,omitempty"` + // the transfer + Transfer string `json:"transfer"` +} + +// PublishPartInput is what publish-part takes. +type PublishPartInput struct { + // the chunk's bytes, base64 (at most 760 KiB before encoding) + Data string `json:"data"` + // this chunk is the part's last + Last bool `json:"last"` + // the part's digest + Part string `json:"part"` + // the chunk's place in its part, from 0 + Sequence int64 `json:"sequence"` + // the transfer + Transfer string `json:"transfer"` +} + +// PublishPartAnswer is what publish-part answers. +type PublishPartAnswer struct { + // the part is whole and its bytes match its digest + Complete bool `json:"complete,omitempty"` + // the part + Part string `json:"part"` + // the chunks of the part held so far + Received int64 `json:"received,omitempty"` + // the transfer + Transfer string `json:"transfer"` +} + +// VersionsInput is what versions takes. +type VersionsInput struct { + // narrowed to one kind (optional) + Kind string `json:"kind,omitempty"` + // the name + Name string `json:"name"` +} + +// VersionsAnswer is what versions answers. +type VersionsAnswer struct { + // the name + Name string `json:"name"` + // newest first + Versions []Artifact `json:"versions"` +} + +// RemoveInput is what remove takes. +type RemoveInput struct { + // the digest (for a bundle, archive or copied base) + Digest string `json:"digest,omitempty"` + // the name + Name string `json:"name"` + // why it goes + Reason string `json:"reason"` + // the tag (for a tagged version) + Tag string `json:"tag,omitempty"` + // the version + Version string `json:"version,omitempty"` +} + +// The values of RemoveInput's reason. +const ( + RemoveInputReasonTagGone = "tag-gone" + RemoveInputReasonRetention = "retention" +) + +// RemoveAnswer is what remove answers. +type RemoveAnswer struct { + Artifact *Artifact `json:"artifact,omitempty"` + // it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) + Kept bool `json:"kept,omitempty"` + // why it was kept: shared-digest, another tag names its digest + Reason string `json:"reason,omitempty"` + // it is gone + Removed bool `json:"removed"` + // the other tags still naming its digest, when kept + Tags []string `json:"tags,omitempty"` +} + +// The values of RemoveAnswer's reason. +const ( + RemoveAnswerReasonSharedDigest = "shared-digest" +) + +// PublishedEvent is the body of the event published. +type PublishedEvent = Outcome + +// RefusedEvent is the body of the event refused. +type RefusedEvent = Outcome + +// RemovedEvent is the body of the event removed. +type RemovedEvent = Outcome + +// KeptEvent is the body of the event kept. +type KeptEvent = Outcome + +// GoPackageRegistryForm is the form of the provision go-package-registry. +type GoPackageRegistryForm struct { + // the read side's address + URL string `json:"url"` +} diff --git a/contracts/go/seats/gopackageregistry/contract_test.go b/contracts/go/seats/gopackageregistry/contract_test.go new file mode 100644 index 00000000..e5f69ec1 --- /dev/null +++ b/contracts/go/seats/gopackageregistry/contract_test.go @@ -0,0 +1,66 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package gopackageregistry + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/go-package-registry/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "published on the seat's subject", subject: "mesh.seat.go-package-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"module\":\"git.novox.be/novox/mesh-sdk/go\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"module\":\"git.novox.be/novox/mesh-sdk/go\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.published$")}, + {name: "refused on the seat's subject", subject: "mesh.seat.go-package-registry.event.refused", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"retention\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"retention\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.refused$")}, + {name: "removed on the seat's subject", subject: "mesh.seat.go-package-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"retention\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"retention\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.removed$")}, + {name: "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", subject: "mesh.seat.go-package-registry.event.kept", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"shared-digest\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"shared-digest\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", + decode: func(b []byte) (any, error) { var v KeptEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.kept$")}, + {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"later\":{\"x\":1},\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "publish answers what it lacks", subject: "", given: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", wire: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", + decode: func(b []byte) (any, error) { var v PublishAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a chunk of a part", subject: "", given: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", wire: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", + decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "the versions held", subject: "", given: "{\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"version\":\"0.1.19\"}]}", + decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a tagged version sharing its digest with another tag is kept, not removed", subject: "", given: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", wire: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", + decode: func(b []byte) (any, error) { var v RemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/contracts/go/seats/gopackageregistry/go.mod b/contracts/go/seats/gopackageregistry/go.mod new file mode 100644 index 00000000..065f758b --- /dev/null +++ b/contracts/go/seats/gopackageregistry/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry + +go 1.26.0 diff --git a/contracts/go/seats/meshcatalog/contract.go b/contracts/go/seats/meshcatalog/contract.go new file mode 100644 index 00000000..aeecc413 --- /dev/null +++ b/contracts/go/seats/meshcatalog/contract.go @@ -0,0 +1,594 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshcatalog is the contract of the seat "mesh-catalog", defined whole (seats/mesh-catalog/seat.json): The catalog of the mesh: every module the trunk holds with its manifest, fingerprint and verdict, the packages published, and the seat list (ADR 0636 §1, §4). +package meshcatalog + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +// Seat is the seat's name. +const Seat = "mesh-catalog" + +const ( + VerbModules = "modules" + VerbModule = "module" + VerbProvides = "provides" + VerbDependents = "dependents" +) + +const ( + EventSeatDefinitionChanged = "seat-definition-changed" + EventSeatDefinitionRemoved = "seat-definition-removed" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbModules, VerbModule, VerbProvides, VerbDependents} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventSeatDefinitionChanged, EventSeatDefinitionRemoved} +} + +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. +const ( + Family = "mesh-catalog.events" + FamilyMaxAgeDays = 7 +) + +// SubjectSeatDefinitionChanged is where seat-definition-changed is published. +const SubjectSeatDefinitionChanged = "mesh.seat.mesh-catalog.event.seat-definition-changed" + +// SubjectSeatDefinitionRemoved is where seat-definition-removed is published. +const SubjectSeatDefinitionRemoved = "mesh.seat.mesh-catalog.event.seat-definition-removed" + +const ( + StateManifests = "manifests" + StateModules = "modules" + StateCandidates = "candidates" + StateVerdicts = "verdicts" + StatePredefined = "predefined" + StateConfigCandidates = "config-candidates" + StatePackages = "packages" +) + +// ManifestsByDigestKey is the key of one value of the state manifests under the form "". +func ManifestsByDigestKey(digest string) string { return digest } + +// ParseManifestsByDigestKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseManifestsByDigestKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeManifestsByDigestValue reads one value of the state manifests under the form "", refusing a field ManifestsByDigestValue does not know and any text after the value. +func DecodeManifestsByDigestValue(raw []byte) (ManifestsByDigestValue, error) { + var v ManifestsByDigestValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// ModulesByModuleKey is the key of one value of the state modules under the form "". +func ModulesByModuleKey(module string) string { return module } + +// ParseModulesByModuleKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseModulesByModuleKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeModulesByModuleValue reads one value of the state modules under the form "", refusing a field ModulesByModuleValue does not know and any text after the value. +func DecodeModulesByModuleValue(raw []byte) (ModulesByModuleValue, error) { + var v ModulesByModuleValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// CandidatesByGroupKey is the key of one value of the state candidates under the form "". +func CandidatesByGroupKey(group string) string { return group } + +// ParseCandidatesByGroupKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseCandidatesByGroupKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeCandidatesByGroupValue reads one value of the state candidates under the form "", refusing a field CandidatesByGroupValue does not know and any text after the value. +func DecodeCandidatesByGroupValue(raw []byte) (CandidatesByGroupValue, error) { + var v CandidatesByGroupValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// VerdictsByDigestKey is the key of one value of the state verdicts under the form "". +func VerdictsByDigestKey(digest string) string { return digest } + +// ParseVerdictsByDigestKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseVerdictsByDigestKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeVerdictsByDigestValue reads one value of the state verdicts under the form "", refusing a field VerdictsByDigestValue does not know and any text after the value. +func DecodeVerdictsByDigestValue(raw []byte) (VerdictsByDigestValue, error) { + var v VerdictsByDigestValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PredefinedByListKey is the key of one value of the state predefined under the form "list". +func PredefinedByListKey() string { return "list" } + +// ParsePredefinedByListKey reads the parts of a key of the form "list", in the order of the form; the last result is false for a key of another form. +func ParsePredefinedByListKey(key string) bool { + rest := key + if !strings.HasPrefix(rest, "list") { + return false + } + rest = rest[4:] + if rest != "" { + return false + } + return true +} + +// DecodePredefinedByListValue reads one value of the state predefined under the form "list", refusing a field PredefinedByListValue does not know and any text after the value. +func DecodePredefinedByListValue(raw []byte) (PredefinedByListValue, error) { + var v PredefinedByListValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// ConfigCandidatesByPullKey is the key of one value of the state config-candidates under the form "". +func ConfigCandidatesByPullKey(pull string) string { return pull } + +// ParseConfigCandidatesByPullKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseConfigCandidatesByPullKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeConfigCandidatesByPullValue reads one value of the state config-candidates under the form "", refusing a field ConfigCandidatesByPullValue does not know and any text after the value. +func DecodeConfigCandidatesByPullValue(raw []byte) (ConfigCandidatesByPullValue, error) { + var v ConfigCandidatesByPullValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PackagesByModuleKey is the key of one value of the state packages under the form "". +func PackagesByModuleKey(module string) string { return module } + +// ParsePackagesByModuleKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParsePackagesByModuleKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodePackagesByModuleValue reads one value of the state packages under the form "", refusing a field PackagesByModuleValue does not know and any text after the value. +func DecodePackagesByModuleValue(raw []byte) (PackagesByModuleValue, error) { + var v PackagesByModuleValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PackagesByPackageSeriesKey is the key of one value of the state packages under the form ".". +func PackagesByPackageSeriesKey(packagePart, series string) string { return packagePart + "." + series } + +// ParsePackagesByPackageSeriesKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParsePackagesByPackageSeriesKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodePackagesByPackageSeriesValue reads one value of the state packages under the form ".", refusing a field PackagesByPackageSeriesValue does not know and any text after the value. +func DecodePackagesByPackageSeriesValue(raw []byte) (PackagesByPackageSeriesValue, error) { + var v PackagesByPackageSeriesValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// ModulesInput is what modules takes. +type ModulesInput struct { +} + +// ModulesAnswer is what modules answers. +type ModulesAnswer = map[string]any + +// ModuleInput is what module takes. +type ModuleInput struct { + // the module's name + Module string `json:"module"` +} + +// ModuleAnswer is what module answers. +type ModuleAnswer = map[string]any + +// ProvidesInput is what provides takes. +type ProvidesInput struct { + // the provision, such as postgres-database + Provision string `json:"provision"` +} + +// ProvidesAnswer is what provides answers. +type ProvidesAnswer = map[string]any + +// DependentsInput is what dependents takes. +type DependentsInput struct { + // the module's name + Module string `json:"module"` +} + +// DependentsAnswer is what dependents answers. +type DependentsAnswer = map[string]any + +// SeatDefinitionChangedEvent is the body of the event seat-definition-changed. +type SeatDefinitionChangedEvent struct { + Digest string `json:"digest"` + Seat string `json:"seat"` + // trunk + Source string `json:"source"` +} + +// SeatDefinitionRemovedEvent is the body of the event seat-definition-removed. +type SeatDefinitionRemovedEvent struct { + Seat string `json:"seat"` +} + +// ManifestsByDigestValue is one value of the state manifests under the key . +type ManifestsByDigestValue struct { + // the manifest, UTF-8 JSON text + Manifest string `json:"manifest"` + Module string `json:"module"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// ModulesByModuleValueInputs2 is a type of the contract. +type ModulesByModuleValueInputs2 struct { + Bases []string `json:"bases,omitempty"` + Builder string `json:"builder,omitempty"` + Contexts map[string]string `json:"contexts,omitempty"` + ImportForm string `json:"import-form,omitempty"` + Imports []string `json:"imports,omitempty"` + Locks map[string]string `json:"locks,omitempty"` + Packages []string `json:"packages,omitempty"` + Paths map[string]string `json:"paths,omitempty"` + Tree string `json:"tree"` +} + +// ModulesByModuleValue is one value of the state modules under the key . +type ModulesByModuleValue struct { + Commit string `json:"commit"` + // the old-form fingerprint + Fp string `json:"fp,omitempty"` + FpSrc int64 `json:"fp-src,omitempty"` + // src2: fingerprint + Fp2 string `json:"fp2,omitempty"` + Inputs2 *ModulesByModuleValueInputs2 `json:"inputs2,omitempty"` + // manifest digest + Manifest string `json:"manifest,omitempty"` + Needs []string `json:"needs,omitempty"` + None string `json:"none,omitempty"` + // why it has none + None2 string `json:"none2,omitempty"` + Packages map[string]string `json:"packages,omitempty"` + Path string `json:"path,omitempty"` + Paths map[string][]string `json:"paths,omitempty"` + Problems []string `json:"problems,omitempty"` + Repository string `json:"repository"` + // the source revision + Src int64 `json:"src,omitempty"` + Toolchains map[string]any `json:"toolchains,omitempty"` + Unknown []string `json:"unknown,omitempty"` + // the layout version + V int64 `json:"v,omitempty"` + Verdict string `json:"verdict,omitempty"` + Warnings []string `json:"warnings,omitempty"` +} + +// CandidatesByGroupValuePullsItemFpsValue is a type of the contract. +type CandidatesByGroupValuePullsItemFpsValue struct { + Fp2 string `json:"fp2,omitempty"` + Manifest string `json:"manifest,omitempty"` + None2 string `json:"none2,omitempty"` + Packages map[string]string `json:"packages,omitempty"` + Paths map[string]string `json:"paths,omitempty"` +} + +// CandidatesByGroupValuePullsItem is a type of the contract. +type CandidatesByGroupValuePullsItem struct { + Fps map[string]CandidatesByGroupValuePullsItemFpsValue `json:"fps,omitempty"` + Head string `json:"head"` + Modules []string `json:"modules"` + Number int64 `json:"number"` + Repository string `json:"repository"` +} + +// CandidatesByGroupValue is one value of the state candidates under the key . +type CandidatesByGroupValue struct { + // the source revision it is read on + Base int64 `json:"base"` + Fpset string `json:"fpset,omitempty"` + Group string `json:"group"` + Heads string `json:"heads"` + Pulls []CandidatesByGroupValuePullsItem `json:"pulls"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// VerdictsByDigestValue is one value of the state verdicts under the key . +type VerdictsByDigestValue struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + Judge string `json:"judge,omitempty"` + Module string `json:"module"` + Problems []string `json:"problems,omitempty"` + State string `json:"state"` + Unknown []string `json:"unknown,omitempty"` + // the layout version + V int64 `json:"v,omitempty"` + Warnings []string `json:"warnings,omitempty"` +} + +// PredefinedByListValue is one value of the state predefined under the key list. +type PredefinedByListValue struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + Commit string `json:"commit"` + // the foundation's predefined list as kept + List any `json:"list"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// ConfigCandidatesByPullValueDeclaresValueValue is a type of the contract. +type ConfigCandidatesByPullValueDeclaresValueValue struct { + // the default + Default any `json:"default"` + Description string `json:"description,omitempty"` + // the allowed values + Enum any `json:"enum,omitempty"` + Kind string `json:"kind"` + Maximum float64 `json:"maximum,omitempty"` + Minimum float64 `json:"minimum,omitempty"` + Pattern string `json:"pattern,omitempty"` + Predefined string `json:"predefined,omitempty"` + Required bool `json:"required,omitempty"` + Runtime bool `json:"runtime,omitempty"` + Trusted bool `json:"trusted,omitempty"` + Type string `json:"type,omitempty"` + Why string `json:"why"` +} + +// ConfigCandidatesByPullValue is one value of the state config-candidates under the key . +type ConfigCandidatesByPullValue struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + // module to key to declaration + Declares map[string]map[string]ConfigCandidatesByPullValueDeclaresValueValue `json:"declares"` + Head string `json:"head"` + Pull string `json:"pull"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// PackagesByModuleValuePackagesItem is a type of the contract. +type PackagesByModuleValuePackagesItem struct { + Digest string `json:"digest"` + Package string `json:"package"` +} + +// PackagesByModuleValue is one value of the state packages under the key . +type PackagesByModuleValue struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + Commit string `json:"commit"` + Dir string `json:"dir,omitempty"` + // the Go module path + Module string `json:"module"` + Packages []PackagesByModuleValuePackagesItem `json:"packages"` + Repository string `json:"repository"` + // digest of go.mod and go.sum + Requires string `json:"requires,omitempty"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// PackagesByPackageSeriesValueNewestContentsPackagesItem is a type of the contract. +type PackagesByPackageSeriesValueNewestContentsPackagesItem struct { + Digest string `json:"digest"` + Package string `json:"package"` +} + +// PackagesByPackageSeriesValueNewestContents is a type of the contract. +type PackagesByPackageSeriesValueNewestContents struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + Commit string `json:"commit"` + Dir string `json:"dir,omitempty"` + // the Go module path + Module string `json:"module"` + Packages []PackagesByPackageSeriesValueNewestContentsPackagesItem `json:"packages"` + Repository string `json:"repository"` + // digest of go.mod and go.sum + Requires string `json:"requires,omitempty"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// PackagesByPackageSeriesValueNewest is a type of the contract. +type PackagesByPackageSeriesValueNewest struct { + Commit string `json:"commit,omitempty"` + Contents *PackagesByPackageSeriesValueNewestContents `json:"contents,omitempty"` + Digest string `json:"digest,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + Version string `json:"version"` +} + +// PackagesByPackageSeriesValueNumberedContentsPackagesItem is a type of the contract. +type PackagesByPackageSeriesValueNumberedContentsPackagesItem struct { + Digest string `json:"digest"` + Package string `json:"package"` +} + +// PackagesByPackageSeriesValueNumberedContents is a type of the contract. +type PackagesByPackageSeriesValueNumberedContents struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + Commit string `json:"commit"` + Dir string `json:"dir,omitempty"` + // the Go module path + Module string `json:"module"` + Packages []PackagesByPackageSeriesValueNumberedContentsPackagesItem `json:"packages"` + Repository string `json:"repository"` + // digest of go.mod and go.sum + Requires string `json:"requires,omitempty"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// PackagesByPackageSeriesValueNumbered is a type of the contract. +type PackagesByPackageSeriesValueNumbered struct { + Commit string `json:"commit,omitempty"` + Contents *PackagesByPackageSeriesValueNumberedContents `json:"contents,omitempty"` + Digest string `json:"digest,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + Version string `json:"version"` +} + +// PackagesByPackageSeriesValue is one value of the state packages under the key .. +type PackagesByPackageSeriesValue struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + GoModule string `json:"go-module,omitempty"` + Name string `json:"name"` + Newest *PackagesByPackageSeriesValueNewest `json:"newest,omitempty"` + Numbered *PackagesByPackageSeriesValueNumbered `json:"numbered,omitempty"` + Package string `json:"package,omitempty"` + Seat string `json:"seat,omitempty"` + Series string `json:"series"` + // the layout version + V int64 `json:"v,omitempty"` +} diff --git a/contracts/go/seats/meshcatalog/contract_test.go b/contracts/go/seats/meshcatalog/contract_test.go new file mode 100644 index 00000000..96bb633f --- /dev/null +++ b/contracts/go/seats/meshcatalog/contract_test.go @@ -0,0 +1,70 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshcatalog + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-catalog/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "a manifest", subject: "", given: "{\"manifest\":\"{\\\"module\\\":\\\"nats\\\"}\",\"module\":\"nats\",\"v\":1}", wire: "{\"manifest\":\"{\\\"module\\\":\\\"nats\\\"}\",\"module\":\"nats\",\"v\":1}", + decode: func(b []byte) (any, error) { var v ManifestsByDigestValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a module", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"fp2\":\"src2:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"repository\":\"novox/mesh-catalog\",\"v\":1}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"fp2\":\"src2:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"repository\":\"novox/mesh-catalog\",\"v\":1}", + decode: func(b []byte) (any, error) { var v ModulesByModuleValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a candidate", subject: "", given: "{\"base\":3,\"group\":\"feat/registry\",\"heads\":\"0123456789ab\",\"pulls\":[{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"modules\":[\"nats\"],\"number\":1,\"repository\":\"novox/mesh-sdk\"}],\"v\":1}", wire: "{\"base\":3,\"group\":\"feat/registry\",\"heads\":\"0123456789ab\",\"pulls\":[{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"modules\":[\"nats\"],\"number\":1,\"repository\":\"novox/mesh-sdk\"}],\"v\":1}", + decode: func(b []byte) (any, error) { var v CandidatesByGroupValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a verdict", subject: "", given: "{\"judge\":\"trunk\",\"module\":\"nats\",\"state\":\"accepted\",\"v\":1}", wire: "{\"judge\":\"trunk\",\"module\":\"nats\",\"state\":\"accepted\",\"v\":1}", + decode: func(b []byte) (any, error) { var v VerdictsByDigestValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "the predefined list", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"list\":{\"streams\":[]},\"v\":1}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"list\":{\"streams\":[]},\"v\":1}", + decode: func(b []byte) (any, error) { var v PredefinedByListValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a pull request's configuration", subject: "", given: "{\"declares\":{\"nats\":{\"limit\":{\"default\":1,\"kind\":\"setting\",\"why\":\"a bound\"}}},\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"pull\":\"novox/mesh-catalog#1\",\"v\":1}", wire: "{\"declares\":{\"nats\":{\"limit\":{\"default\":1,\"kind\":\"setting\",\"why\":\"a bound\"}}},\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"pull\":\"novox/mesh-catalog#1\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v ConfigCandidatesByPullValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a published Go module", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"git.novox.be/novox/mesh-sdk/go\",\"packages\":[{\"digest\":\"ab\",\"package\":\"git.novox.be/novox/mesh-sdk/go/keys\"}],\"repository\":\"novox/mesh-sdk\",\"v\":1}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"git.novox.be/novox/mesh-sdk/go\",\"packages\":[{\"digest\":\"ab\",\"package\":\"git.novox.be/novox/mesh-sdk/go/keys\"}],\"repository\":\"novox/mesh-sdk\",\"v\":1}", + decode: func(b []byte) (any, error) { var v PackagesByModuleValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a package series", subject: "", given: "{\"name\":\"go-keys\",\"series\":\"0.1\",\"v\":1}", wire: "{\"name\":\"go-keys\",\"series\":\"0.1\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v PackagesByPackageSeriesValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/contracts/go/seats/meshcatalog/go.mod b/contracts/go/seats/meshcatalog/go.mod new file mode 100644 index 00000000..e6fd10a2 --- /dev/null +++ b/contracts/go/seats/meshcatalog/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog + +go 1.26.0 diff --git a/contracts/go/seats/meshdelivery/contract.go b/contracts/go/seats/meshdelivery/contract.go new file mode 100644 index 00000000..c70a8734 --- /dev/null +++ b/contracts/go/seats/meshdelivery/contract.go @@ -0,0 +1,454 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshdelivery is the contract of the seat "mesh-delivery", defined whole (seats/mesh-delivery/seat.json): The delivery gate: reads and steers deliveries, merges and the checks that decide them; the merge is made on the operator's Approve once both checks passed. +package meshdelivery + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +// Seat is the seat's name. +const Seat = "mesh-delivery" + +const ( + VerbDeliveries = "deliveries" + VerbTimes = "times" + VerbShow = "show" + VerbGroups = "groups" + VerbWhatIf = "what-if" + VerbChecks = "checks" + VerbTable = "table" + VerbStalled = "stalled" + VerbRecheck = "recheck" + VerbRelease = "release" + VerbStop = "stop" + VerbClose = "close" + VerbRetireHistory = "retire-history" + VerbSettle = "settle" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbDeliveries, VerbTimes, VerbShow, VerbGroups, VerbWhatIf, VerbChecks, VerbTable, VerbStalled, VerbRecheck, VerbRelease, VerbStop, VerbClose, VerbRetireHistory, VerbSettle} +} + +const ( + StateSource = "source" + StateMerges = "merges" + StateAsks = "asks" + StateOwed = "owed" + StateTurns = "turns" +) + +// SourceByMainKey is the key of one value of the state source under the form "main". +func SourceByMainKey() string { return "main" } + +// ParseSourceByMainKey reads the parts of a key of the form "main", in the order of the form; the last result is false for a key of another form. +func ParseSourceByMainKey(key string) bool { + rest := key + if !strings.HasPrefix(rest, "main") { + return false + } + rest = rest[4:] + if rest != "" { + return false + } + return true +} + +// DecodeSourceByMainValue reads one value of the state source under the form "main", refusing a field SourceByMainValue does not know and any text after the value. +func DecodeSourceByMainValue(raw []byte) (SourceByMainValue, error) { + var v SourceByMainValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// MergesByGroupKey is the key of one value of the state merges under the form "". +func MergesByGroupKey(group string) string { return group } + +// ParseMergesByGroupKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseMergesByGroupKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeMergesByGroupValue reads one value of the state merges under the form "", refusing a field MergesByGroupValue does not know and any text after the value. +func DecodeMergesByGroupValue(raw []byte) (MergesByGroupValue, error) { + var v MergesByGroupValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// AsksByMergeGroupHeadsKey is the key of one value of the state asks under the form "merge..". +func AsksByMergeGroupHeadsKey(group, heads string) string { return "merge." + group + "." + heads } + +// ParseAsksByMergeGroupHeadsKey reads the parts of a key of the form "merge..", in the order of the form; the last result is false for a key of another form. +func ParseAsksByMergeGroupHeadsKey(key string) (string, string, bool) { + rest := key + if !strings.HasPrefix(rest, "merge.") { + return "", "", false + } + rest = rest[6:] + v1, after1, found1 := strings.Cut(rest, ".") + if !found1 || v1 == "" { + return "", "", false + } + rest = after1 + if rest == "" { + return "", "", false + } + v3 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v1, v3, true +} + +// DecodeAsksByMergeGroupHeadsValue reads one value of the state asks under the form "merge..", refusing a field AsksByMergeGroupHeadsValue does not know and any text after the value. +func DecodeAsksByMergeGroupHeadsValue(raw []byte) (AsksByMergeGroupHeadsValue, error) { + var v AsksByMergeGroupHeadsValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// OwedByRepositoryNumberHeadActKey is the key of one value of the state owed under the form "...". +func OwedByRepositoryNumberHeadActKey(repository, number, head, act string) string { + return repository + "." + number + "." + head + "." + act +} + +// ParseOwedByRepositoryNumberHeadActKey reads the parts of a key of the form "...", in the order of the form; the last result is false for a key of another form. +func ParseOwedByRepositoryNumberHeadActKey(key string) (string, string, string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", "", "", false + } + rest = after0 + v2, after2, found2 := strings.Cut(rest, ".") + if !found2 || v2 == "" { + return "", "", "", "", false + } + rest = after2 + v4, after4, found4 := strings.Cut(rest, ".") + if !found4 || v4 == "" { + return "", "", "", "", false + } + rest = after4 + if rest == "" { + return "", "", "", "", false + } + v6 := rest + rest = "" + if rest != "" { + return "", "", "", "", false + } + return v0, v2, v4, v6, true +} + +// DecodeOwedByRepositoryNumberHeadActValue reads one value of the state owed under the form "...", refusing a field OwedByRepositoryNumberHeadActValue does not know and any text after the value. +func DecodeOwedByRepositoryNumberHeadActValue(raw []byte) (OwedByRepositoryNumberHeadActValue, error) { + var v OwedByRepositoryNumberHeadActValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// TurnsByRepositoryKey is the key of one value of the state turns under the form "". +func TurnsByRepositoryKey(repository string) string { return repository } + +// ParseTurnsByRepositoryKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseTurnsByRepositoryKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeTurnsByRepositoryValue reads one value of the state turns under the form "", refusing a field TurnsByRepositoryValue does not know and any text after the value. +func DecodeTurnsByRepositoryValue(raw []byte) (TurnsByRepositoryValue, error) { + var v TurnsByRepositoryValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// DeliveriesInput is what deliveries takes. +type DeliveriesInput struct { + // "true": the final ones of the last thirty days too + All string `json:"all,omitempty"` + // a group's id (its branch name) + Group string `json:"group,omitempty"` + // owner/repository + Repository string `json:"repository,omitempty"` + // one state, e.g. delivering or held + State string `json:"state,omitempty"` +} + +// The values of DeliveriesInput's all. +const ( + DeliveriesInputAllTrue = "true" + DeliveriesInputAllFalse = "false" +) + +// DeliveriesAnswer is what deliveries answers. +type DeliveriesAnswer = map[string]any + +// TimesInput is what times takes. +type TimesInput struct { +} + +// TimesAnswer is what times answers. +type TimesAnswer = map[string]any + +// ShowInput is what show takes. +type ShowInput struct { + // a delivery's id, or a group's + ID string `json:"id"` +} + +// ShowAnswer is what show answers. +type ShowAnswer = map[string]any + +// GroupsInput is what groups takes. +type GroupsInput struct { + // "true": the groups that ended too + All string `json:"all,omitempty"` +} + +// The values of GroupsInput's all. +const ( + GroupsInputAllTrue = "true" + GroupsInputAllFalse = "false" +) + +// GroupsAnswer is what groups answers. +type GroupsAnswer = map[string]any + +// WhatIfInput is what what-if takes. +type WhatIfInput struct { +} + +// WhatIfAnswer is what what-if answers. +type WhatIfAnswer = map[string]any + +// ChecksInput is what checks takes. +type ChecksInput struct { + // a commit's sha, or the start of one, instead of a pull request + Commit string `json:"commit,omitempty"` + // a pull request's number: its head is read + Number string `json:"number,omitempty"` + // owner/repository + Repository string `json:"repository"` +} + +// ChecksAnswer is what checks answers. +type ChecksAnswer = map[string]any + +// TableInput is what table takes. +type TableInput struct { +} + +// TableAnswer is what table answers. +type TableAnswer = map[string]any + +// StalledInput is what stalled takes. +type StalledInput struct { +} + +// StalledAnswer is what stalled answers. +type StalledAnswer = map[string]any + +// RecheckInput is what recheck takes. +type RecheckInput struct { + // the delivery's id + ID string `json:"id"` + // why, kept with the transition + Why string `json:"why"` +} + +// RecheckAnswer is what recheck answers. +type RecheckAnswer = map[string]any + +// ReleaseInput is what release takes. +type ReleaseInput struct { +} + +// ReleaseAnswer is what release answers. +type ReleaseAnswer = map[string]any + +// StopInput is what stop takes. +type StopInput struct { +} + +// StopAnswer is what stop answers. +type StopAnswer = map[string]any + +// CloseInput is what close takes. +type CloseInput struct { + // the delivery's id + ID string `json:"id"` + // the condition it answers + Why string `json:"why"` +} + +// CloseAnswer is what close answers. +type CloseAnswer = map[string]any + +// RetireHistoryInput is what retire-history takes. +type RetireHistoryInput struct { +} + +// RetireHistoryAnswer is what retire-history answers. +type RetireHistoryAnswer = map[string]any + +// SettleInput is what settle takes. +type SettleInput struct { + // the held deliveries' ids (owner/repository@commit) + Ids []string `json:"ids"` + // why they may be settled, shown to the operator + Why string `json:"why"` +} + +// SettleAnswer is what settle answers. +type SettleAnswer = map[string]any + +// SourceByMainValue is one value of the state source under the key main. +type SourceByMainValue struct { + // the group that moved it + Group string `json:"group"` + // repository to the merged head, a full commit + Heads map[string]string `json:"heads"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// MergesByGroupValuePullsItem is a type of the contract. +type MergesByGroupValuePullsItem struct { + Commit string `json:"commit,omitempty"` + Head string `json:"head"` + Number int64 `json:"number"` + Repository string `json:"repository"` + // merging, merged or refused + State string `json:"state"` + Words string `json:"words,omitempty"` +} + +// MergesByGroupValue is one value of the state merges under the key . +type MergesByGroupValue struct { + // @ to the approved head + Approved map[string]string `json:"approved,omitempty"` + // the ask key + Ask string `json:"ask"` + Group string `json:"group"` + Pulls []MergesByGroupValuePullsItem `json:"pulls"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// AsksByMergeGroupHeadsValueHeadsItem is a type of the contract. +type AsksByMergeGroupHeadsValueHeadsItem struct { + // the full commit + Head string `json:"head"` + Number int64 `json:"number"` + // / + Repository string `json:"repository"` +} + +// AsksByMergeGroupHeadsValue is one value of the state asks under the key merge... +type AsksByMergeGroupHeadsValue struct { + // the operator-channel ask as raised + Ask any `json:"ask"` + // the number of times it was asked + Asking int64 `json:"asking,omitempty"` + Group string `json:"group"` + Heads []AsksByMergeGroupHeadsValueHeadsItem `json:"heads"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// OwedByRepositoryNumberHeadActValue is one value of the state owed under the key .... +type OwedByRepositoryNumberHeadActValue struct { + // status=, view, note or comment. + Act string `json:"act"` + Context string `json:"context,omitempty"` + Ref string `json:"ref,omitempty"` + State string `json:"state,omitempty"` + Target string `json:"target,omitempty"` + Text string `json:"text,omitempty"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// TurnsByRepositoryValueWaitingItem is a type of the contract. +type TurnsByRepositoryValueWaitingItem struct { + // when (RFC 3339 time) + Approved string `json:"approved"` + Ask string `json:"ask,omitempty"` + Group string `json:"group"` + Heads string `json:"heads"` + // when (RFC 3339 time) + Since string `json:"since"` + Step string `json:"step,omitempty"` + WaitsFor string `json:"waits-for,omitempty"` + // when (RFC 3339 time) + WaitsSince string `json:"waits-since,omitempty"` +} + +// TurnsByRepositoryValue is one value of the state turns under the key . +type TurnsByRepositoryValue struct { + Repository string `json:"repository"` + // the layout version + V int64 `json:"v,omitempty"` + Waiting []TurnsByRepositoryValueWaitingItem `json:"waiting"` +} diff --git a/contracts/go/seats/meshdelivery/contract_test.go b/contracts/go/seats/meshdelivery/contract_test.go new file mode 100644 index 00000000..f128120d --- /dev/null +++ b/contracts/go/seats/meshdelivery/contract_test.go @@ -0,0 +1,64 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshdelivery + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-delivery/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "the source revision", subject: "", given: "{\"group\":\"feat/registry\",\"heads\":{\"novox/mesh-sdk\":\"cccccccccccccccccccccccccccccccccccccccc\"},\"v\":1}", wire: "{\"group\":\"feat/registry\",\"heads\":{\"novox/mesh-sdk\":\"cccccccccccccccccccccccccccccccccccccccc\"},\"v\":1}", + decode: func(b []byte) (any, error) { var v SourceByMainValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a merge", subject: "", given: "{\"ask\":\"merge.feat=registry.0123456789ab\",\"group\":\"feat/registry\",\"pulls\":[{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"number\":1,\"repository\":\"novox/mesh-sdk\",\"state\":\"merging\"}],\"v\":1}", wire: "{\"ask\":\"merge.feat=registry.0123456789ab\",\"group\":\"feat/registry\",\"pulls\":[{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"number\":1,\"repository\":\"novox/mesh-sdk\",\"state\":\"merging\"}],\"v\":1}", + decode: func(b []byte) (any, error) { var v MergesByGroupValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "an ask", subject: "", given: "{\"ask\":{\"headline\":\"merge\"},\"group\":\"feat/registry\",\"heads\":[{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"number\":1,\"repository\":\"novox/mesh-sdk\"}],\"v\":1}", wire: "{\"ask\":{\"headline\":\"merge\"},\"group\":\"feat/registry\",\"heads\":[{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"number\":1,\"repository\":\"novox/mesh-sdk\"}],\"v\":1}", + decode: func(b []byte) (any, error) { + var v AsksByMergeGroupHeadsValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "an act owed", subject: "", given: "{\"act\":\"view\",\"v\":1}", wire: "{\"act\":\"view\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v OwedByRepositoryNumberHeadActValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a turn", subject: "", given: "{\"repository\":\"novox/mesh-sdk\",\"v\":1,\"waiting\":[{\"approved\":\"2026-11-05T10:00:00Z\",\"group\":\"feat/registry\",\"heads\":\"0123456789ab\",\"since\":\"2026-11-05T10:01:00Z\"}]}", wire: "{\"repository\":\"novox/mesh-sdk\",\"v\":1,\"waiting\":[{\"approved\":\"2026-11-05T10:00:00Z\",\"group\":\"feat/registry\",\"heads\":\"0123456789ab\",\"since\":\"2026-11-05T10:01:00Z\"}]}", + decode: func(b []byte) (any, error) { var v TurnsByRepositoryValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/contracts/go/seats/meshdelivery/go.mod b/contracts/go/seats/meshdelivery/go.mod new file mode 100644 index 00000000..12356bcf --- /dev/null +++ b/contracts/go/seats/meshdelivery/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery + +go 1.26.0 diff --git a/contracts/go/seats/meshdeliverychecker/contract.go b/contracts/go/seats/meshdeliverychecker/contract.go new file mode 100644 index 00000000..790b8558 --- /dev/null +++ b/contracts/go/seats/meshdeliverychecker/contract.go @@ -0,0 +1,438 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshdeliverychecker is the contract of the seat "mesh-delivery-checker", defined whole (seats/mesh-delivery-checker/seat.json): The merge check's tries: start one, read one, end one. The checker's own events stay under its own subjects (ADR 0620 §2). +package meshdeliverychecker + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +// Seat is the seat's name. +const Seat = "mesh-delivery-checker" + +const ( + VerbTry = "try" + VerbTryRead = "try-read" + VerbTryEnd = "try-end" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbTry, VerbTryRead, VerbTryEnd} +} + +// The holder's own events, listed beside the seat and published under the holder's subjects (ADR 0620 §2). +const ( + HolderEventVerdict = "verdict" + HolderEventTagWanted = "tag-wanted" + HolderEventPreviewWritten = "preview.written" +) + +// HolderSubjectVerdict is where the holder publishes verdict, kept in checker.asks. +func HolderSubjectVerdict(holder string) string { return "mesh.stream." + holder + ".event.verdict" } + +// HolderSubjectTagWanted is where the holder publishes tag-wanted, kept in checker.asks. +func HolderSubjectTagWanted(holder string) string { + return "mesh.stream." + holder + ".event.tag-wanted" +} + +// HolderSubjectPreviewWritten is where the holder publishes preview.written, kept in checks. +func HolderSubjectPreviewWritten(holder string) string { + return "mesh.stream." + holder + ".event.preview.written" +} + +const ( + StateVerdicts = "verdicts" + StateKept = "kept" + StateOwed = "owed" + StatePreviews = "previews" +) + +// VerdictsByGroupHeadsKey is the key of one value of the state verdicts under the form ".". +func VerdictsByGroupHeadsKey(group, heads string) string { return group + "." + heads } + +// ParseVerdictsByGroupHeadsKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseVerdictsByGroupHeadsKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeVerdictsByGroupHeadsValue reads one value of the state verdicts under the form ".", refusing a field VerdictsByGroupHeadsValue does not know and any text after the value. +func DecodeVerdictsByGroupHeadsValue(raw []byte) (VerdictsByGroupHeadsValue, error) { + var v VerdictsByGroupHeadsValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// KeptByLevelDigestKey is the key of one value of the state kept under the form ".". +func KeptByLevelDigestKey(level, digest string) string { return level + "." + digest } + +// ParseKeptByLevelDigestKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseKeptByLevelDigestKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeKeptByLevelDigestValue reads one value of the state kept under the form ".", refusing a field KeptByLevelDigestValue does not know and any text after the value. +func DecodeKeptByLevelDigestValue(raw []byte) (KeptByLevelDigestValue, error) { + var v KeptByLevelDigestValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// OwedByRepositoryNumberHeadActKey is the key of one value of the state owed under the form "...". +func OwedByRepositoryNumberHeadActKey(repository, number, head, act string) string { + return repository + "." + number + "." + head + "." + act +} + +// ParseOwedByRepositoryNumberHeadActKey reads the parts of a key of the form "...", in the order of the form; the last result is false for a key of another form. +func ParseOwedByRepositoryNumberHeadActKey(key string) (string, string, string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", "", "", false + } + rest = after0 + v2, after2, found2 := strings.Cut(rest, ".") + if !found2 || v2 == "" { + return "", "", "", "", false + } + rest = after2 + v4, after4, found4 := strings.Cut(rest, ".") + if !found4 || v4 == "" { + return "", "", "", "", false + } + rest = after4 + if rest == "" { + return "", "", "", "", false + } + v6 := rest + rest = "" + if rest != "" { + return "", "", "", "", false + } + return v0, v2, v4, v6, true +} + +// DecodeOwedByRepositoryNumberHeadActValue reads one value of the state owed under the form "...", refusing a field OwedByRepositoryNumberHeadActValue does not know and any text after the value. +func DecodeOwedByRepositoryNumberHeadActValue(raw []byte) (OwedByRepositoryNumberHeadActValue, error) { + var v OwedByRepositoryNumberHeadActValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PreviewsByRepositoryHeadTrunkKey is the key of one value of the state previews under the form "..". +func PreviewsByRepositoryHeadTrunkKey(repository, head, trunk string) string { + return repository + "." + head + "." + trunk +} + +// ParsePreviewsByRepositoryHeadTrunkKey reads the parts of a key of the form "..", in the order of the form; the last result is false for a key of another form. +func ParsePreviewsByRepositoryHeadTrunkKey(key string) (string, string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", "", false + } + rest = after0 + v2, after2, found2 := strings.Cut(rest, ".") + if !found2 || v2 == "" { + return "", "", "", false + } + rest = after2 + if rest == "" { + return "", "", "", false + } + v4 := rest + rest = "" + if rest != "" { + return "", "", "", false + } + return v0, v2, v4, true +} + +// DecodePreviewsByRepositoryHeadTrunkValue reads one value of the state previews under the form "..", refusing a field PreviewsByRepositoryHeadTrunkValue does not know and any text after the value. +func DecodePreviewsByRepositoryHeadTrunkValue(raw []byte) (PreviewsByRepositoryHeadTrunkValue, error) { + var v PreviewsByRepositoryHeadTrunkValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// TryInput is what try takes. +type TryInput struct { + // the full commit to try, 40 or 64 hex digits + Head string `json:"head"` + // unit or integration: only this level; both when absent + Level string `json:"level,omitempty"` + // / + Repository string `json:"repository"` + // a go test -run pattern: only those tests run, and nothing is kept + Tests string `json:"tests,omitempty"` + // the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent + Trunk string `json:"trunk,omitempty"` +} + +// TryAnswer is what try answers. +type TryAnswer struct { + // the try's id + ID string `json:"id"` + // its state + State string `json:"state,omitempty"` +} + +// TryReadInput is what try-read takes. +type TryReadInput struct { + // the try's id, try- + ID string `json:"id"` +} + +// TryReadAnswer is what try-read answers. +type TryReadAnswer = map[string]any + +// TryEndInput is what try-end takes. +type TryEndInput struct { + // the try's id, try- + ID string `json:"id"` + // why it is ended, in words + Why string `json:"why"` +} + +// TryEndAnswer is what try-end answers. +type TryEndAnswer struct { + // it is ended + Ended bool `json:"ended"` + // the try + ID string `json:"id"` +} + +// VerdictHolderEventSeriesItem is a type of the contract. +type VerdictHolderEventSeriesItem struct { + // its artifact, or the package's ecosystem name as the module's packages name it + Artifact string `json:"artifact"` + // the series on trunk + From string `json:"from"` + // the module whose artifact's series changes + Module string `json:"module"` + // the series the checked head gives it + To string `json:"to"` +} + +// VerdictHolderEvent is the body of the holder's event verdict. +type VerdictHolderEvent struct { + // the commit checked + Head string `json:"head"` + // the generated files out of step with their seat definitions, each path (design 26) + OutOfStep []string `json:"out-of-step,omitempty"` + // the pull request's number, for a pull request's check + Pull int64 `json:"pull,omitempty"` + // owner/repo + Repository string `json:"repository"` + // each change of an artifact's series the checked head makes against trunk's manifests, once each (design 32); absent when it changes none + Series []VerdictHolderEventSeriesItem `json:"series,omitempty"` + // pass or fail + Verdict string `json:"verdict"` +} + +// TagWantedHolderEvent is the body of the holder's event tag-wanted. +type TagWantedHolderEvent struct { + // the pull request's branch: its tag is removed when the branch ends + Branch string `json:"branch"` + // its head + Head string `json:"head"` + // the group member + Module string `json:"module"` + // the package it declares + Package string `json:"package"` + // the pull request + Pull int64 `json:"pull,omitempty"` + // owner/repo + Repository string `json:"repository"` + // the tag wanted + Tag string `json:"tag,omitempty"` +} + +// VerdictsByGroupHeadsValueComposition is a type of the contract. +type VerdictsByGroupHeadsValueComposition struct { + Fpset string `json:"fpset,omitempty"` + Machines []string `json:"machines,omitempty"` + Sdk string `json:"sdk,omitempty"` + // not-run, pass or fail + State string `json:"state"` + Words string `json:"words,omitempty"` +} + +// VerdictsByGroupHeadsValueFailed is a type of the contract. +type VerdictsByGroupHeadsValueFailed struct { + Machine string `json:"machine,omitempty"` + Test string `json:"test,omitempty"` + Words string `json:"words,omitempty"` +} + +// VerdictsByGroupHeadsValueFailuresItem is a type of the contract. +type VerdictsByGroupHeadsValueFailuresItem struct { + Machine string `json:"machine,omitempty"` + Test string `json:"test,omitempty"` + Words string `json:"words,omitempty"` +} + +// VerdictsByGroupHeadsValueLevelsValue is a type of the contract. +type VerdictsByGroupHeadsValueLevelsValue struct { + Kept int64 `json:"kept"` + Run int64 `json:"run"` + Skipped int64 `json:"skipped,omitempty"` +} + +// VerdictsByGroupHeadsValueSeriesItem is a type of the contract. +type VerdictsByGroupHeadsValueSeriesItem struct { + Artifact string `json:"artifact"` + From string `json:"from"` + Module string `json:"module"` + To string `json:"to"` +} + +// VerdictsByGroupHeadsValueTook is milliseconds. +type VerdictsByGroupHeadsValueTook struct { + Clone int64 `json:"clone,omitempty"` + Composition int64 `json:"composition,omitempty"` + IntegrationRun int64 `json:"integration-run,omitempty"` + IntegrationSetup int64 `json:"integration-setup,omitempty"` + Lint int64 `json:"lint,omitempty"` + Queue int64 `json:"queue,omitempty"` + Unit int64 `json:"unit,omitempty"` +} + +// VerdictsByGroupHeadsValue is one value of the state verdicts under the key .. +type VerdictsByGroupHeadsValue struct { + Beside map[string]string `json:"beside,omitempty"` + CarriedFrom map[string]string `json:"carried-from,omitempty"` + Composition *VerdictsByGroupHeadsValueComposition `json:"composition,omitempty"` + Failed *VerdictsByGroupHeadsValueFailed `json:"failed,omitempty"` + Failures []VerdictsByGroupHeadsValueFailuresItem `json:"failures,omitempty"` + Group string `json:"group"` + // 12 hex digits + Heads string `json:"heads"` + // by level, unit or integration + Levels map[string]VerdictsByGroupHeadsValueLevelsValue `json:"levels,omitempty"` + Machines []string `json:"machines,omitempty"` + OverBound []string `json:"over-bound,omitempty"` + Sdk string `json:"sdk,omitempty"` + Series []VerdictsByGroupHeadsValueSeriesItem `json:"series,omitempty"` + // running, pass or fail + State string `json:"state"` + // milliseconds + Took *VerdictsByGroupHeadsValueTook `json:"took,omitempty"` + // repository to the trunk head merged onto + Trunk map[string]string `json:"trunk,omitempty"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// KeptByLevelDigestValue is one value of the state kept under the key .. +type KeptByLevelDigestValue struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + Level string `json:"level"` + Package string `json:"package"` + Repository string `json:"repository"` + // pass: a failure is never kept + State string `json:"state"` + Tests int64 `json:"tests,omitempty"` + Took int64 `json:"took,omitempty"` + Toolchain string `json:"toolchain,omitempty"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// OwedByRepositoryNumberHeadActValue is one value of the state owed under the key .... +type OwedByRepositoryNumberHeadActValue struct { + // status=, view, note or comment. + Act string `json:"act"` + Context string `json:"context,omitempty"` + Ref string `json:"ref,omitempty"` + State string `json:"state,omitempty"` + Target string `json:"target,omitempty"` + Text string `json:"text,omitempty"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// PreviewsByRepositoryHeadTrunkValueModulesValue is a type of the contract. +type PreviewsByRepositoryHeadTrunkValueModulesValue struct { + Locks map[string]string `json:"locks,omitempty"` + // manifest digest + Manifest string `json:"manifest"` + Packages []string `json:"packages,omitempty"` + Paths map[string]string `json:"paths,omitempty"` + Tree string `json:"tree"` +} + +// PreviewsByRepositoryHeadTrunkValue is one value of the state previews under the key ... +type PreviewsByRepositoryHeadTrunkValue struct { + // files in conflict + Conflicts []string `json:"conflicts,omitempty"` + Head string `json:"head"` + // the merge commit + Merge string `json:"merge,omitempty"` + // by directory + Modules map[string]PreviewsByRepositoryHeadTrunkValueModulesValue `json:"modules,omitempty"` + Repository string `json:"repository"` + Trunk string `json:"trunk"` + // the layout version + V int64 `json:"v,omitempty"` +} diff --git a/contracts/go/seats/meshdeliverychecker/contract_test.go b/contracts/go/seats/meshdeliverychecker/contract_test.go new file mode 100644 index 00000000..17611c6a --- /dev/null +++ b/contracts/go/seats/meshdeliverychecker/contract_test.go @@ -0,0 +1,72 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshdeliverychecker + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-delivery-checker/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "verdict on the checker's own family", subject: "mesh.stream.mesh-delivery-checker.event.verdict", given: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"out-of-step\":[\"go/seats/meshbroker/contract.go\"],\"pull\":3,\"repository\":\"novox/mesh-contracts\",\"series\":[{\"artifact\":\"go\",\"from\":\"0.1\",\"module\":\"mesh-contracts\",\"to\":\"0.2\"}],\"verdict\":\"fail\"}", wire: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"out-of-step\":[\"go/seats/meshbroker/contract.go\"],\"pull\":3,\"repository\":\"novox/mesh-contracts\",\"series\":[{\"artifact\":\"go\",\"from\":\"0.1\",\"module\":\"mesh-contracts\",\"to\":\"0.2\"}],\"verdict\":\"fail\"}", + decode: func(b []byte) (any, error) { var v VerdictHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.verdict$")}, + {name: "tag-wanted on the checker's own family", subject: "mesh.stream.mesh-delivery-checker.event.tag-wanted", given: "{\"branch\":\"feat/registry\",\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"mesh-sdk\",\"package\":\"go\",\"pull\":170,\"repository\":\"novox/mesh-sdk\",\"tag\":\"registry-1966\"}", wire: "{\"branch\":\"feat/registry\",\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"mesh-sdk\",\"package\":\"go\",\"pull\":170,\"repository\":\"novox/mesh-sdk\",\"tag\":\"registry-1966\"}", + decode: func(b []byte) (any, error) { var v TagWantedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.tag-wanted$")}, + {name: "preview.written on the checker's stream", subject: "mesh.stream.mesh-delivery-checker.event.preview.written", given: "{}", wire: "{}", + decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.preview\\.written$")}, + {name: "a running verdict", subject: "", given: "{\"group\":\"feat/registry\",\"heads\":\"0123456789ab\",\"state\":\"running\",\"v\":1}", wire: "{\"group\":\"feat/registry\",\"heads\":\"0123456789ab\",\"state\":\"running\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v VerdictsByGroupHeadsValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a kept pass", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"level\":\"unit\",\"package\":\"go/keys\",\"repository\":\"novox/mesh-sdk\",\"state\":\"pass\",\"tests\":4,\"took\":120,\"toolchain\":\"go1.26\",\"v\":1}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"level\":\"unit\",\"package\":\"go/keys\",\"repository\":\"novox/mesh-sdk\",\"state\":\"pass\",\"tests\":4,\"took\":120,\"toolchain\":\"go1.26\",\"v\":1}", + decode: func(b []byte) (any, error) { var v KeptByLevelDigestValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "the status owed for a head", subject: "", given: "{\"act\":\"status=mesh/check\",\"context\":\"mesh/check\",\"state\":\"success\",\"v\":1}", wire: "{\"act\":\"status=mesh/check\",\"context\":\"mesh/check\",\"state\":\"success\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v OwedByRepositoryNumberHeadActValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a preview", subject: "", given: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"merge\":\"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\",\"repository\":\"novox/mesh-sdk\",\"trunk\":\"dddddddddddddddddddddddddddddddddddddddd\",\"v\":1}", wire: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"merge\":\"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\",\"repository\":\"novox/mesh-sdk\",\"trunk\":\"dddddddddddddddddddddddddddddddddddddddd\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v PreviewsByRepositoryHeadTrunkValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/contracts/go/seats/meshdeliverychecker/go.mod b/contracts/go/seats/meshdeliverychecker/go.mod new file mode 100644 index 00000000..d50dcda1 --- /dev/null +++ b/contracts/go/seats/meshdeliverychecker/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker + +go 1.26.0 diff --git a/contracts/go/seats/nodebuildgo/contract.go b/contracts/go/seats/nodebuildgo/contract.go new file mode 100644 index 00000000..59b72a28 --- /dev/null +++ b/contracts/go/seats/nodebuildgo/contract.go @@ -0,0 +1,447 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package nodebuildgo is the contract of the seat "node-build-go", defined whole (seats/node-build-go/seat.json): The Go build module on a node: it takes the asks of the builder and the checker, runs them as steps and answers. +package nodebuildgo + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +// Seat is the seat's name. +const Seat = "node-build-go" + +const ( + VerbCurrent = "current" + VerbPause = "pause" + VerbResume = "resume" +) + +const ( + AcceptDelivery = "delivery" + AcceptCheck = "check" + AcceptRecheck = "recheck" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbCurrent, VerbPause, VerbResume} +} + +// AcceptSubjectDelivery is where delivery is sent. +const AcceptSubjectDelivery = "mesh.seat.node-build-go.accept.delivery" + +// AcceptSubjectCheck is where check is sent. +const AcceptSubjectCheck = "mesh.seat.node-build-go.accept.check" + +// AcceptSubjectRecheck is where recheck is sent. +const AcceptSubjectRecheck = "mesh.seat.node-build-go.accept.recheck" + +const ( + StateSteps = "steps" + StatePauses = "pauses" + StateToolchains = "toolchains" +) + +// StepsByNodeIDKey is the key of one value of the state steps under the form ".". +func StepsByNodeIDKey(node, id string) string { return node + "." + id } + +// ParseStepsByNodeIDKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseStepsByNodeIDKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeStepsByNodeIDValue reads one value of the state steps under the form ".", refusing a field StepsByNodeIDValue does not know and any text after the value. +func DecodeStepsByNodeIDValue(raw []byte) (StepsByNodeIDValue, error) { + var v StepsByNodeIDValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PausesByPausedNodeKey is the key of one value of the state pauses under the form "paused.". +func PausesByPausedNodeKey(node string) string { return "paused." + node } + +// ParsePausesByPausedNodeKey reads the parts of a key of the form "paused.", in the order of the form; the last result is false for a key of another form. +func ParsePausesByPausedNodeKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "paused.") { + return "", false + } + rest = rest[7:] + if rest == "" { + return "", false + } + v1 := rest + rest = "" + if rest != "" { + return "", false + } + return v1, true +} + +// DecodePausesByPausedNodeValue reads one value of the state pauses under the form "paused.", refusing a field PausesByPausedNodeValue does not know and any text after the value. +func DecodePausesByPausedNodeValue(raw []byte) (PausesByPausedNodeValue, error) { + var v PausesByPausedNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PausesByCapacityNodeKey is the key of one value of the state pauses under the form "capacity.". +func PausesByCapacityNodeKey(node string) string { return "capacity." + node } + +// ParsePausesByCapacityNodeKey reads the parts of a key of the form "capacity.", in the order of the form; the last result is false for a key of another form. +func ParsePausesByCapacityNodeKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "capacity.") { + return "", false + } + rest = rest[9:] + if rest == "" { + return "", false + } + v1 := rest + rest = "" + if rest != "" { + return "", false + } + return v1, true +} + +// DecodePausesByCapacityNodeValue reads one value of the state pauses under the form "capacity.", refusing a field PausesByCapacityNodeValue does not know and any text after the value. +func DecodePausesByCapacityNodeValue(raw []byte) (PausesByCapacityNodeValue, error) { + var v PausesByCapacityNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// ToolchainsByNodeLanguageKey is the key of one value of the state toolchains under the form ".". +func ToolchainsByNodeLanguageKey(node, language string) string { return node + "." + language } + +// ParseToolchainsByNodeLanguageKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseToolchainsByNodeLanguageKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeToolchainsByNodeLanguageValue reads one value of the state toolchains under the form ".", refusing a field ToolchainsByNodeLanguageValue does not know and any text after the value. +func DecodeToolchainsByNodeLanguageValue(raw []byte) (ToolchainsByNodeLanguageValue, error) { + var v ToolchainsByNodeLanguageValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + 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"` +} + +// 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"` +} + +// Capacity is how many steps the node runs at once. +type Capacity struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + BuildHours string `json:"build-hours,omitempty"` + BuildHoursFault string `json:"build-hours-fault,omitempty"` + ByCpu int64 `json:"by-cpu,omitempty"` + Configured int64 `json:"configured"` + ConfiguredFrom string `json:"configured-from,omitempty"` + CoresPerStep float64 `json:"cores-per-step,omitempty"` + Effective int64 `json:"effective"` + Ineligible string `json:"ineligible,omitempty"` + OutsideHours bool `json:"outside-hours,omitempty"` + QuotaCores float64 `json:"quota-cores,omitempty"` + QuotaFrom string `json:"quota-from,omitempty"` + // the layout version + V int64 `json:"v,omitempty"` + 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"` +} + +// ComposeAnswer is a type of the contract. +type ComposeAnswer struct { + Built bool `json:"built"` + Key string `json:"key,omitempty"` + // the compose-candidate verdict + Verdict any `json:"verdict,omitempty"` +} + +// Pause is whether the node takes no new ask. +type Pause struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + By string `json:"by,omitempty"` + Paused bool `json:"paused"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// Step is one step a build module runs on a node. +type Step struct { + // when (RFC 3339 time) + Ended string `json:"ended,omitempty"` + ID string `json:"id"` + Node string `json:"node"` + 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"` + // when (RFC 3339 time) + Started string `json:"started"` + // running, passed, failed or refused + State string `json:"state"` + Tool string `json:"tool"` + // the layout version + V int64 `json:"v,omitempty"` + 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"` +} + +// 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"` + Kept bool `json:"kept,omitempty"` + Levels []string `json:"levels"` + Packages []string `json:"packages,omitempty"` + Run string `json:"run,omitempty"` + Runner string `json:"runner,omitempty"` + RunnerWhen string `json:"runner_when,omitempty"` +} + +// 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"` + Package string `json:"package"` + Reader string `json:"reader,omitempty"` + // the compose-candidate request + Request any `json:"request"` +} + +// StepAsk is one ask of a build module. +type StepAsk struct { + Asker string `json:"asker"` + // when (RFC 3339 time) + At string `json:"at,omitempty"` + Build *StepAskBuild `json:"build,omitempty"` + Check *StepAskCheck `json:"check,omitempty"` + Compose *StepAskCompose `json:"compose,omitempty"` + ID string `json:"id"` + // build, check or compose + Kind string `json:"kind"` + // go, typescript or image + Language string `json:"language"` + Needs []string `json:"needs,omitempty"` + // package to the tagged version a build pins + Packages map[string]string `json:"packages,omitempty"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// Toolchain is the toolchain a node reports. +type Toolchain struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + // go or typescript + Language string `json:"language"` + Recipe string `json:"recipe"` + // the layout version + V int64 `json:"v,omitempty"` + Version string `json:"version"` +} + +// CurrentInput is what current takes. +type CurrentInput struct { +} + +// CurrentAnswer is what current answers. +type CurrentAnswer = map[string]any + +// PauseInput is what pause takes. +type PauseInput struct { +} + +// PauseAnswer is what pause answers. +type PauseAnswer = map[string]any + +// ResumeInput is what resume takes. +type ResumeInput struct { +} + +// ResumeAnswer is what resume answers. +type ResumeAnswer = map[string]any + +// DeliveryAccept is the body of delivery. +type DeliveryAccept = StepAsk + +// CheckAccept is the body of check. +type CheckAccept = StepAsk + +// RecheckAccept is the body of recheck. +type RecheckAccept = StepAsk + +// StepsByNodeIDValue is one value of the state steps under the key .. +type StepsByNodeIDValue = Step + +// PausesByPausedNodeValue is one value of the state pauses under the key paused.. +type PausesByPausedNodeValue = Pause + +// PausesByCapacityNodeValue is one value of the state pauses under the key capacity.. +type PausesByCapacityNodeValue = Capacity + +// ToolchainsByNodeLanguageValue is one value of the state toolchains under the key .. +type ToolchainsByNodeLanguageValue = Toolchain diff --git a/contracts/go/seats/nodebuildgo/contract_test.go b/contracts/go/seats/nodebuildgo/contract_test.go new file mode 100644 index 00000000..e7dd00cc --- /dev/null +++ b/contracts/go/seats/nodebuildgo/contract_test.go @@ -0,0 +1,68 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package nodebuildgo + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/node-build-go/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "a running step", subject: "", given: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", wire: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"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 paused node", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", + decode: func(b []byte) (any, error) { + var v PausesByPausedNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a node's capacity", subject: "", given: "{\"configured\":2,\"effective\":2,\"v\":1,\"why\":\"the configured steps fit the cores\"}", wire: "{\"configured\":2,\"effective\":2,\"v\":1,\"why\":\"the configured steps fit the cores\"}", + decode: func(b []byte) (any, error) { + var v PausesByCapacityNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a node's toolchain", subject: "", given: "{\"language\":\"go\",\"recipe\":\"1\",\"v\":1,\"version\":\"1.26.0\"}", wire: "{\"language\":\"go\",\"recipe\":\"1\",\"v\":1,\"version\":\"1.26.0\"}", + decode: func(b []byte) (any, error) { + var v ToolchainsByNodeLanguageValue + err := json.Unmarshal(b, &v) + return v, err + }, 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$")}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/contracts/go/seats/nodebuildgo/go.mod b/contracts/go/seats/nodebuildgo/go.mod new file mode 100644 index 00000000..7d786c14 --- /dev/null +++ b/contracts/go/seats/nodebuildgo/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo + +go 1.26.0 diff --git a/contracts/go/seats/nodebuildimage/contract.go b/contracts/go/seats/nodebuildimage/contract.go new file mode 100644 index 00000000..843c04ae --- /dev/null +++ b/contracts/go/seats/nodebuildimage/contract.go @@ -0,0 +1,447 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package nodebuildimage is the contract of the seat "node-build-image", defined whole (seats/node-build-image/seat.json): The image build module on a node: it takes the asks of the builder and the checker, runs them as steps and answers. +package nodebuildimage + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +// Seat is the seat's name. +const Seat = "node-build-image" + +const ( + VerbCurrent = "current" + VerbPause = "pause" + VerbResume = "resume" +) + +const ( + AcceptDelivery = "delivery" + AcceptCheck = "check" + AcceptRecheck = "recheck" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbCurrent, VerbPause, VerbResume} +} + +// AcceptSubjectDelivery is where delivery is sent. +const AcceptSubjectDelivery = "mesh.seat.node-build-image.accept.delivery" + +// AcceptSubjectCheck is where check is sent. +const AcceptSubjectCheck = "mesh.seat.node-build-image.accept.check" + +// AcceptSubjectRecheck is where recheck is sent. +const AcceptSubjectRecheck = "mesh.seat.node-build-image.accept.recheck" + +const ( + StateSteps = "steps" + StatePauses = "pauses" + StateToolchains = "toolchains" +) + +// StepsByNodeIDKey is the key of one value of the state steps under the form ".". +func StepsByNodeIDKey(node, id string) string { return node + "." + id } + +// ParseStepsByNodeIDKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseStepsByNodeIDKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeStepsByNodeIDValue reads one value of the state steps under the form ".", refusing a field StepsByNodeIDValue does not know and any text after the value. +func DecodeStepsByNodeIDValue(raw []byte) (StepsByNodeIDValue, error) { + var v StepsByNodeIDValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PausesByPausedNodeKey is the key of one value of the state pauses under the form "paused.". +func PausesByPausedNodeKey(node string) string { return "paused." + node } + +// ParsePausesByPausedNodeKey reads the parts of a key of the form "paused.", in the order of the form; the last result is false for a key of another form. +func ParsePausesByPausedNodeKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "paused.") { + return "", false + } + rest = rest[7:] + if rest == "" { + return "", false + } + v1 := rest + rest = "" + if rest != "" { + return "", false + } + return v1, true +} + +// DecodePausesByPausedNodeValue reads one value of the state pauses under the form "paused.", refusing a field PausesByPausedNodeValue does not know and any text after the value. +func DecodePausesByPausedNodeValue(raw []byte) (PausesByPausedNodeValue, error) { + var v PausesByPausedNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PausesByCapacityNodeKey is the key of one value of the state pauses under the form "capacity.". +func PausesByCapacityNodeKey(node string) string { return "capacity." + node } + +// ParsePausesByCapacityNodeKey reads the parts of a key of the form "capacity.", in the order of the form; the last result is false for a key of another form. +func ParsePausesByCapacityNodeKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "capacity.") { + return "", false + } + rest = rest[9:] + if rest == "" { + return "", false + } + v1 := rest + rest = "" + if rest != "" { + return "", false + } + return v1, true +} + +// DecodePausesByCapacityNodeValue reads one value of the state pauses under the form "capacity.", refusing a field PausesByCapacityNodeValue does not know and any text after the value. +func DecodePausesByCapacityNodeValue(raw []byte) (PausesByCapacityNodeValue, error) { + var v PausesByCapacityNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// ToolchainsByNodeLanguageKey is the key of one value of the state toolchains under the form ".". +func ToolchainsByNodeLanguageKey(node, language string) string { return node + "." + language } + +// ParseToolchainsByNodeLanguageKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseToolchainsByNodeLanguageKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeToolchainsByNodeLanguageValue reads one value of the state toolchains under the form ".", refusing a field ToolchainsByNodeLanguageValue does not know and any text after the value. +func DecodeToolchainsByNodeLanguageValue(raw []byte) (ToolchainsByNodeLanguageValue, error) { + var v ToolchainsByNodeLanguageValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + 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"` +} + +// 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"` +} + +// Capacity is how many steps the node runs at once. +type Capacity struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + BuildHours string `json:"build-hours,omitempty"` + BuildHoursFault string `json:"build-hours-fault,omitempty"` + ByCpu int64 `json:"by-cpu,omitempty"` + Configured int64 `json:"configured"` + ConfiguredFrom string `json:"configured-from,omitempty"` + CoresPerStep float64 `json:"cores-per-step,omitempty"` + Effective int64 `json:"effective"` + Ineligible string `json:"ineligible,omitempty"` + OutsideHours bool `json:"outside-hours,omitempty"` + QuotaCores float64 `json:"quota-cores,omitempty"` + QuotaFrom string `json:"quota-from,omitempty"` + // the layout version + V int64 `json:"v,omitempty"` + 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"` +} + +// ComposeAnswer is a type of the contract. +type ComposeAnswer struct { + Built bool `json:"built"` + Key string `json:"key,omitempty"` + // the compose-candidate verdict + Verdict any `json:"verdict,omitempty"` +} + +// Pause is whether the node takes no new ask. +type Pause struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + By string `json:"by,omitempty"` + Paused bool `json:"paused"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// Step is one step a build module runs on a node. +type Step struct { + // when (RFC 3339 time) + Ended string `json:"ended,omitempty"` + ID string `json:"id"` + Node string `json:"node"` + 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"` + // when (RFC 3339 time) + Started string `json:"started"` + // running, passed, failed or refused + State string `json:"state"` + Tool string `json:"tool"` + // the layout version + V int64 `json:"v,omitempty"` + 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"` +} + +// 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"` + Kept bool `json:"kept,omitempty"` + Levels []string `json:"levels"` + Packages []string `json:"packages,omitempty"` + Run string `json:"run,omitempty"` + Runner string `json:"runner,omitempty"` + RunnerWhen string `json:"runner_when,omitempty"` +} + +// 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"` + Package string `json:"package"` + Reader string `json:"reader,omitempty"` + // the compose-candidate request + Request any `json:"request"` +} + +// StepAsk is one ask of a build module. +type StepAsk struct { + Asker string `json:"asker"` + // when (RFC 3339 time) + At string `json:"at,omitempty"` + Build *StepAskBuild `json:"build,omitempty"` + Check *StepAskCheck `json:"check,omitempty"` + Compose *StepAskCompose `json:"compose,omitempty"` + ID string `json:"id"` + // build, check or compose + Kind string `json:"kind"` + // go, typescript or image + Language string `json:"language"` + Needs []string `json:"needs,omitempty"` + // package to the tagged version a build pins + Packages map[string]string `json:"packages,omitempty"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// Toolchain is the toolchain a node reports. +type Toolchain struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + // go or typescript + Language string `json:"language"` + Recipe string `json:"recipe"` + // the layout version + V int64 `json:"v,omitempty"` + Version string `json:"version"` +} + +// CurrentInput is what current takes. +type CurrentInput struct { +} + +// CurrentAnswer is what current answers. +type CurrentAnswer = map[string]any + +// PauseInput is what pause takes. +type PauseInput struct { +} + +// PauseAnswer is what pause answers. +type PauseAnswer = map[string]any + +// ResumeInput is what resume takes. +type ResumeInput struct { +} + +// ResumeAnswer is what resume answers. +type ResumeAnswer = map[string]any + +// DeliveryAccept is the body of delivery. +type DeliveryAccept = StepAsk + +// CheckAccept is the body of check. +type CheckAccept = StepAsk + +// RecheckAccept is the body of recheck. +type RecheckAccept = StepAsk + +// StepsByNodeIDValue is one value of the state steps under the key .. +type StepsByNodeIDValue = Step + +// PausesByPausedNodeValue is one value of the state pauses under the key paused.. +type PausesByPausedNodeValue = Pause + +// PausesByCapacityNodeValue is one value of the state pauses under the key capacity.. +type PausesByCapacityNodeValue = Capacity + +// ToolchainsByNodeLanguageValue is one value of the state toolchains under the key .. +type ToolchainsByNodeLanguageValue = Toolchain diff --git a/contracts/go/seats/nodebuildimage/contract_test.go b/contracts/go/seats/nodebuildimage/contract_test.go new file mode 100644 index 00000000..75649a4a --- /dev/null +++ b/contracts/go/seats/nodebuildimage/contract_test.go @@ -0,0 +1,68 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package nodebuildimage + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/node-build-image/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "a running step", subject: "", given: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", wire: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"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 paused node", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", + decode: func(b []byte) (any, error) { + var v PausesByPausedNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a node's capacity", subject: "", given: "{\"configured\":2,\"effective\":2,\"v\":1,\"why\":\"the configured steps fit the cores\"}", wire: "{\"configured\":2,\"effective\":2,\"v\":1,\"why\":\"the configured steps fit the cores\"}", + decode: func(b []byte) (any, error) { + var v PausesByCapacityNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a node's toolchain", subject: "", given: "{\"language\":\"go\",\"recipe\":\"1\",\"v\":1,\"version\":\"1.26.0\"}", wire: "{\"language\":\"go\",\"recipe\":\"1\",\"v\":1,\"version\":\"1.26.0\"}", + decode: func(b []byte) (any, error) { + var v ToolchainsByNodeLanguageValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a build ask", subject: "mesh.seat.node-build-image.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-image\\.accept\\.delivery$")}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/contracts/go/seats/nodebuildimage/go.mod b/contracts/go/seats/nodebuildimage/go.mod new file mode 100644 index 00000000..33cb3f5b --- /dev/null +++ b/contracts/go/seats/nodebuildimage/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildimage + +go 1.26.0 diff --git a/contracts/go/seats/nodebuildnode/contract.go b/contracts/go/seats/nodebuildnode/contract.go new file mode 100644 index 00000000..8e3e0448 --- /dev/null +++ b/contracts/go/seats/nodebuildnode/contract.go @@ -0,0 +1,447 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package nodebuildnode is the contract of the seat "node-build-node", defined whole (seats/node-build-node/seat.json): The Node build module on a node: it takes the asks of the builder and the checker, runs them as steps and answers. +package nodebuildnode + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +// Seat is the seat's name. +const Seat = "node-build-node" + +const ( + VerbCurrent = "current" + VerbPause = "pause" + VerbResume = "resume" +) + +const ( + AcceptDelivery = "delivery" + AcceptCheck = "check" + AcceptRecheck = "recheck" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbCurrent, VerbPause, VerbResume} +} + +// AcceptSubjectDelivery is where delivery is sent. +const AcceptSubjectDelivery = "mesh.seat.node-build-node.accept.delivery" + +// AcceptSubjectCheck is where check is sent. +const AcceptSubjectCheck = "mesh.seat.node-build-node.accept.check" + +// AcceptSubjectRecheck is where recheck is sent. +const AcceptSubjectRecheck = "mesh.seat.node-build-node.accept.recheck" + +const ( + StateSteps = "steps" + StatePauses = "pauses" + StateToolchains = "toolchains" +) + +// StepsByNodeIDKey is the key of one value of the state steps under the form ".". +func StepsByNodeIDKey(node, id string) string { return node + "." + id } + +// ParseStepsByNodeIDKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseStepsByNodeIDKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeStepsByNodeIDValue reads one value of the state steps under the form ".", refusing a field StepsByNodeIDValue does not know and any text after the value. +func DecodeStepsByNodeIDValue(raw []byte) (StepsByNodeIDValue, error) { + var v StepsByNodeIDValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PausesByPausedNodeKey is the key of one value of the state pauses under the form "paused.". +func PausesByPausedNodeKey(node string) string { return "paused." + node } + +// ParsePausesByPausedNodeKey reads the parts of a key of the form "paused.", in the order of the form; the last result is false for a key of another form. +func ParsePausesByPausedNodeKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "paused.") { + return "", false + } + rest = rest[7:] + if rest == "" { + return "", false + } + v1 := rest + rest = "" + if rest != "" { + return "", false + } + return v1, true +} + +// DecodePausesByPausedNodeValue reads one value of the state pauses under the form "paused.", refusing a field PausesByPausedNodeValue does not know and any text after the value. +func DecodePausesByPausedNodeValue(raw []byte) (PausesByPausedNodeValue, error) { + var v PausesByPausedNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PausesByCapacityNodeKey is the key of one value of the state pauses under the form "capacity.". +func PausesByCapacityNodeKey(node string) string { return "capacity." + node } + +// ParsePausesByCapacityNodeKey reads the parts of a key of the form "capacity.", in the order of the form; the last result is false for a key of another form. +func ParsePausesByCapacityNodeKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "capacity.") { + return "", false + } + rest = rest[9:] + if rest == "" { + return "", false + } + v1 := rest + rest = "" + if rest != "" { + return "", false + } + return v1, true +} + +// DecodePausesByCapacityNodeValue reads one value of the state pauses under the form "capacity.", refusing a field PausesByCapacityNodeValue does not know and any text after the value. +func DecodePausesByCapacityNodeValue(raw []byte) (PausesByCapacityNodeValue, error) { + var v PausesByCapacityNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// ToolchainsByNodeLanguageKey is the key of one value of the state toolchains under the form ".". +func ToolchainsByNodeLanguageKey(node, language string) string { return node + "." + language } + +// ParseToolchainsByNodeLanguageKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseToolchainsByNodeLanguageKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeToolchainsByNodeLanguageValue reads one value of the state toolchains under the form ".", refusing a field ToolchainsByNodeLanguageValue does not know and any text after the value. +func DecodeToolchainsByNodeLanguageValue(raw []byte) (ToolchainsByNodeLanguageValue, error) { + var v ToolchainsByNodeLanguageValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + 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"` +} + +// 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"` +} + +// Capacity is how many steps the node runs at once. +type Capacity struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + BuildHours string `json:"build-hours,omitempty"` + BuildHoursFault string `json:"build-hours-fault,omitempty"` + ByCpu int64 `json:"by-cpu,omitempty"` + Configured int64 `json:"configured"` + ConfiguredFrom string `json:"configured-from,omitempty"` + CoresPerStep float64 `json:"cores-per-step,omitempty"` + Effective int64 `json:"effective"` + Ineligible string `json:"ineligible,omitempty"` + OutsideHours bool `json:"outside-hours,omitempty"` + QuotaCores float64 `json:"quota-cores,omitempty"` + QuotaFrom string `json:"quota-from,omitempty"` + // the layout version + V int64 `json:"v,omitempty"` + 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"` +} + +// ComposeAnswer is a type of the contract. +type ComposeAnswer struct { + Built bool `json:"built"` + Key string `json:"key,omitempty"` + // the compose-candidate verdict + Verdict any `json:"verdict,omitempty"` +} + +// Pause is whether the node takes no new ask. +type Pause struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + By string `json:"by,omitempty"` + Paused bool `json:"paused"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// Step is one step a build module runs on a node. +type Step struct { + // when (RFC 3339 time) + Ended string `json:"ended,omitempty"` + ID string `json:"id"` + Node string `json:"node"` + 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"` + // when (RFC 3339 time) + Started string `json:"started"` + // running, passed, failed or refused + State string `json:"state"` + Tool string `json:"tool"` + // the layout version + V int64 `json:"v,omitempty"` + 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"` +} + +// 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"` + Kept bool `json:"kept,omitempty"` + Levels []string `json:"levels"` + Packages []string `json:"packages,omitempty"` + Run string `json:"run,omitempty"` + Runner string `json:"runner,omitempty"` + RunnerWhen string `json:"runner_when,omitempty"` +} + +// 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"` + Package string `json:"package"` + Reader string `json:"reader,omitempty"` + // the compose-candidate request + Request any `json:"request"` +} + +// StepAsk is one ask of a build module. +type StepAsk struct { + Asker string `json:"asker"` + // when (RFC 3339 time) + At string `json:"at,omitempty"` + Build *StepAskBuild `json:"build,omitempty"` + Check *StepAskCheck `json:"check,omitempty"` + Compose *StepAskCompose `json:"compose,omitempty"` + ID string `json:"id"` + // build, check or compose + Kind string `json:"kind"` + // go, typescript or image + Language string `json:"language"` + Needs []string `json:"needs,omitempty"` + // package to the tagged version a build pins + Packages map[string]string `json:"packages,omitempty"` + // the layout version + V int64 `json:"v,omitempty"` +} + +// Toolchain is the toolchain a node reports. +type Toolchain struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + // go or typescript + Language string `json:"language"` + Recipe string `json:"recipe"` + // the layout version + V int64 `json:"v,omitempty"` + Version string `json:"version"` +} + +// CurrentInput is what current takes. +type CurrentInput struct { +} + +// CurrentAnswer is what current answers. +type CurrentAnswer = map[string]any + +// PauseInput is what pause takes. +type PauseInput struct { +} + +// PauseAnswer is what pause answers. +type PauseAnswer = map[string]any + +// ResumeInput is what resume takes. +type ResumeInput struct { +} + +// ResumeAnswer is what resume answers. +type ResumeAnswer = map[string]any + +// DeliveryAccept is the body of delivery. +type DeliveryAccept = StepAsk + +// CheckAccept is the body of check. +type CheckAccept = StepAsk + +// RecheckAccept is the body of recheck. +type RecheckAccept = StepAsk + +// StepsByNodeIDValue is one value of the state steps under the key .. +type StepsByNodeIDValue = Step + +// PausesByPausedNodeValue is one value of the state pauses under the key paused.. +type PausesByPausedNodeValue = Pause + +// PausesByCapacityNodeValue is one value of the state pauses under the key capacity.. +type PausesByCapacityNodeValue = Capacity + +// ToolchainsByNodeLanguageValue is one value of the state toolchains under the key .. +type ToolchainsByNodeLanguageValue = Toolchain diff --git a/contracts/go/seats/nodebuildnode/contract_test.go b/contracts/go/seats/nodebuildnode/contract_test.go new file mode 100644 index 00000000..9bfd4c18 --- /dev/null +++ b/contracts/go/seats/nodebuildnode/contract_test.go @@ -0,0 +1,68 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package nodebuildnode + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/node-build-node/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "a running step", subject: "", given: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", wire: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"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 paused node", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", + decode: func(b []byte) (any, error) { + var v PausesByPausedNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a node's capacity", subject: "", given: "{\"configured\":2,\"effective\":2,\"v\":1,\"why\":\"the configured steps fit the cores\"}", wire: "{\"configured\":2,\"effective\":2,\"v\":1,\"why\":\"the configured steps fit the cores\"}", + decode: func(b []byte) (any, error) { + var v PausesByCapacityNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a node's toolchain", subject: "", given: "{\"language\":\"go\",\"recipe\":\"1\",\"v\":1,\"version\":\"1.26.0\"}", wire: "{\"language\":\"go\",\"recipe\":\"1\",\"v\":1,\"version\":\"1.26.0\"}", + decode: func(b []byte) (any, error) { + var v ToolchainsByNodeLanguageValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a build ask", subject: "mesh.seat.node-build-node.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-node\\.accept\\.delivery$")}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/contracts/go/seats/nodebuildnode/go.mod b/contracts/go/seats/nodebuildnode/go.mod new file mode 100644 index 00000000..836a6dc8 --- /dev/null +++ b/contracts/go/seats/nodebuildnode/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildnode + +go 1.26.0 diff --git a/contracts/go/seats/npmpackageregistry/contract.go b/contracts/go/seats/npmpackageregistry/contract.go new file mode 100644 index 00000000..5f39cb8d --- /dev/null +++ b/contracts/go/seats/npmpackageregistry/contract.go @@ -0,0 +1,262 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package npmpackageregistry is the contract of the seat "npm-package-registry", defined whole (seats/npm-package-registry/seat.json): The mesh's npm packages: the npm registry protocol's read side, and publishing over the bus. +package npmpackageregistry + +// Seat is the seat's name. +const Seat = "npm-package-registry" + +const ( + VerbPublish = "publish" + VerbPublishPart = "publish-part" + VerbVersions = "versions" + VerbRemove = "remove" +) + +const ( + EventPublished = "published" + EventRefused = "refused" + EventRemoved = "removed" + EventKept = "kept" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventPublished, EventRefused, EventRemoved, EventKept} +} + +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. +const ( + Family = "npm-package-registry.events" + FamilyMaxAgeDays = 7 +) + +// SubjectPublished is where published is published. +const SubjectPublished = "mesh.seat.npm-package-registry.event.published" + +// SubjectRefused is where refused is published. +const SubjectRefused = "mesh.seat.npm-package-registry.event.refused" + +// SubjectRemoved is where removed is published. +const SubjectRemoved = "mesh.seat.npm-package-registry.event.removed" + +// SubjectKept is where kept is published. +const SubjectKept = "mesh.seat.npm-package-registry.event.kept" + +// Artifact is one published thing, as every registry event and answer names it. +type Artifact struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) + Name string `json:"name"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// The values of Artifact's kind. +const ( + ArtifactKindPackage = "package" + ArtifactKindImage = "image" + ArtifactKindBundle = "bundle" + ArtifactKindArchive = "archive" + ArtifactKindUpstream = "upstream" +) + +// Outcome is the body of published, refused and removed. +type Outcome struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // the module holding the seat that said it + Holder string `json:"holder"` + // what it is + Kind string `json:"kind"` + // the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact + Module string `json:"module,omitempty"` + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) + Name string `json:"name"` + // why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) + Reason string `json:"reason,omitempty"` + // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) + Reference string `json:"reference,omitempty"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // for kept: the other tags still naming its digest + Tags []string `json:"tags,omitempty"` + // the transfer it ends, when it ended one + Transfer string `json:"transfer,omitempty"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// The values of Outcome's kind. +const ( + OutcomeKindPackage = "package" + OutcomeKindImage = "image" + OutcomeKindBundle = "bundle" + OutcomeKindArchive = "archive" + OutcomeKindUpstream = "upstream" +) + +// Part is one part of a transfer. +type Part struct { + // the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob + Digest string `json:"digest"` + // its size in bytes + Size int64 `json:"size"` +} + +// PublishInput is what publish takes. +type PublishInput struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) + Name string `json:"name"` + // every part's digest and size; none when the digest is already held + Parts []Part `json:"parts,omitempty"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the transfer's name, the caller's own; the same name opens the same transfer again + Transfer string `json:"transfer"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// The values of PublishInput's kind. +const ( + PublishInputKindPackage = "package" + PublishInputKindImage = "image" + PublishInputKindBundle = "bundle" + PublishInputKindArchive = "archive" + PublishInputKindUpstream = "upstream" +) + +// PublishAnswer is what publish answers. +type PublishAnswer struct { + // the digest is held already: nothing to send; the outcome follows as an event + Held bool `json:"held,omitempty"` + // the digests of the parts the holder lacks, to send by publish-part + Lacks []string `json:"lacks,omitempty"` + // the transfer + Transfer string `json:"transfer"` +} + +// PublishPartInput is what publish-part takes. +type PublishPartInput struct { + // the chunk's bytes, base64 (at most 760 KiB before encoding) + Data string `json:"data"` + // this chunk is the part's last + Last bool `json:"last"` + // the part's digest + Part string `json:"part"` + // the chunk's place in its part, from 0 + Sequence int64 `json:"sequence"` + // the transfer + Transfer string `json:"transfer"` +} + +// PublishPartAnswer is what publish-part answers. +type PublishPartAnswer struct { + // the part is whole and its bytes match its digest + Complete bool `json:"complete,omitempty"` + // the part + Part string `json:"part"` + // the chunks of the part held so far + Received int64 `json:"received,omitempty"` + // the transfer + Transfer string `json:"transfer"` +} + +// VersionsInput is what versions takes. +type VersionsInput struct { + // narrowed to one kind (optional) + Kind string `json:"kind,omitempty"` + // the name + Name string `json:"name"` +} + +// VersionsAnswer is what versions answers. +type VersionsAnswer struct { + // the name + Name string `json:"name"` + // newest first + Versions []Artifact `json:"versions"` +} + +// RemoveInput is what remove takes. +type RemoveInput struct { + // the digest (for a bundle, archive or copied base) + Digest string `json:"digest,omitempty"` + // the name + Name string `json:"name"` + // why it goes + Reason string `json:"reason"` + // the tag (for a tagged version) + Tag string `json:"tag,omitempty"` + // the version + Version string `json:"version,omitempty"` +} + +// The values of RemoveInput's reason. +const ( + RemoveInputReasonTagGone = "tag-gone" + RemoveInputReasonRetention = "retention" +) + +// RemoveAnswer is what remove answers. +type RemoveAnswer struct { + Artifact *Artifact `json:"artifact,omitempty"` + // it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) + Kept bool `json:"kept,omitempty"` + // why it was kept: shared-digest, another tag names its digest + Reason string `json:"reason,omitempty"` + // it is gone + Removed bool `json:"removed"` + // the other tags still naming its digest, when kept + Tags []string `json:"tags,omitempty"` +} + +// The values of RemoveAnswer's reason. +const ( + RemoveAnswerReasonSharedDigest = "shared-digest" +) + +// PublishedEvent is the body of the event published. +type PublishedEvent = Outcome + +// RefusedEvent is the body of the event refused. +type RefusedEvent = Outcome + +// RemovedEvent is the body of the event removed. +type RemovedEvent = Outcome + +// KeptEvent is the body of the event kept. +type KeptEvent = Outcome + +// NpmPackageRegistryForm is the form of the provision npm-package-registry. +type NpmPackageRegistryForm struct { + // the read side's address + URL string `json:"url"` +} diff --git a/contracts/go/seats/npmpackageregistry/contract_test.go b/contracts/go/seats/npmpackageregistry/contract_test.go new file mode 100644 index 00000000..3092b370 --- /dev/null +++ b/contracts/go/seats/npmpackageregistry/contract_test.go @@ -0,0 +1,66 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package npmpackageregistry + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/npm-package-registry/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "published on the seat's subject", subject: "mesh.seat.npm-package-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.published$")}, + {name: "refused on the seat's subject", subject: "mesh.seat.npm-package-registry.event.refused", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"retention\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"retention\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.refused$")}, + {name: "removed on the seat's subject", subject: "mesh.seat.npm-package-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"retention\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"retention\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.removed$")}, + {name: "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", subject: "mesh.seat.npm-package-registry.event.kept", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", + decode: func(b []byte) (any, error) { var v KeptEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.kept$")}, + {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"later\":{\"x\":1},\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "publish answers what it lacks", subject: "", given: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", wire: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", + decode: func(b []byte) (any, error) { var v PublishAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a chunk of a part", subject: "", given: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", wire: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", + decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "the versions held", subject: "", given: "{\"name\":\"@novox/mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"@novox/mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"version\":\"0.1.19\"}]}", + decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a tagged version sharing its digest with another tag is kept, not removed", subject: "", given: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", wire: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", + decode: func(b []byte) (any, error) { var v RemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/contracts/go/seats/npmpackageregistry/go.mod b/contracts/go/seats/npmpackageregistry/go.mod new file mode 100644 index 00000000..acddbbfb --- /dev/null +++ b/contracts/go/seats/npmpackageregistry/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry + +go 1.26.0 diff --git a/contracts/go/seats/ociartifactregistry/contract.go b/contracts/go/seats/ociartifactregistry/contract.go new file mode 100644 index 00000000..8d2a7e6a --- /dev/null +++ b/contracts/go/seats/ociartifactregistry/contract.go @@ -0,0 +1,486 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package ociartifactregistry is the contract of the seat "oci-artifact-registry", defined whole (seats/oci-artifact-registry/seat.json): The mesh's bundles and archives, by digest, in the one artifact store: the OCI distribution protocol's read side. +package ociartifactregistry + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +// Seat is the seat's name. +const Seat = "oci-artifact-registry" + +const ( + VerbPublish = "publish" + VerbPublishPart = "publish-part" + VerbVersions = "versions" + VerbRemove = "remove" + VerbHold = "hold" + VerbRelease = "release" + VerbList = "list" +) + +const ( + EventPublished = "published" + EventRefused = "refused" + EventRemoved = "removed" + EventKept = "kept" + EventHeld = "held" + EventReleased = "released" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove, VerbHold, VerbRelease, VerbList} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventPublished, EventRefused, EventRemoved, EventKept, EventHeld, EventReleased} +} + +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. +const ( + Family = "oci-artifact-registry.events" + FamilyMaxAgeDays = 7 +) + +// SubjectPublished is where published is published. +const SubjectPublished = "mesh.seat.oci-artifact-registry.event.published" + +// SubjectRefused is where refused is published. +const SubjectRefused = "mesh.seat.oci-artifact-registry.event.refused" + +// SubjectRemoved is where removed is published. +const SubjectRemoved = "mesh.seat.oci-artifact-registry.event.removed" + +// SubjectKept is where kept is published. +const SubjectKept = "mesh.seat.oci-artifact-registry.event.kept" + +// SubjectHeld is where held is published. +const SubjectHeld = "mesh.seat.oci-artifact-registry.event.held" + +// SubjectReleased is where released is published. +const SubjectReleased = "mesh.seat.oci-artifact-registry.event.released" + +const ( + StateHolds = "holds" + StateWindows = "windows" +) + +// HoldsBySeatNameDigestKey is the key of one value of the state holds under the form "..". +func HoldsBySeatNameDigestKey(seat, name, digest string) string { + return seat + "." + name + "." + digest +} + +// ParseHoldsBySeatNameDigestKey reads the parts of a key of the form "..", in the order of the form; the last result is false for a key of another form. +func ParseHoldsBySeatNameDigestKey(key string) (string, string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", "", false + } + rest = after0 + v2, after2, found2 := strings.Cut(rest, ".") + if !found2 || v2 == "" { + return "", "", "", false + } + rest = after2 + if rest == "" { + return "", "", "", false + } + v4 := rest + rest = "" + if rest != "" { + return "", "", "", false + } + return v0, v2, v4, true +} + +// DecodeHoldsBySeatNameDigestValue reads one value of the state holds under the form "..", refusing a field HoldsBySeatNameDigestValue does not know and any text after the value. +func DecodeHoldsBySeatNameDigestValue(raw []byte) (HoldsBySeatNameDigestValue, error) { + var v HoldsBySeatNameDigestValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// WindowsByCallerKey is the key of one value of the state windows under the form "". +func WindowsByCallerKey(caller string) string { return caller } + +// ParseWindowsByCallerKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseWindowsByCallerKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeWindowsByCallerValue reads one value of the state windows under the form "", refusing a field WindowsByCallerValue does not know and any text after the value. +func DecodeWindowsByCallerValue(raw []byte) (WindowsByCallerValue, error) { + var v WindowsByCallerValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// Artifact is one published thing, as every registry event and answer names it. +type Artifact struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) + Name string `json:"name"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// The values of Artifact's kind. +const ( + ArtifactKindPackage = "package" + ArtifactKindImage = "image" + ArtifactKindBundle = "bundle" + ArtifactKindArchive = "archive" + ArtifactKindUpstream = "upstream" +) + +// Hold is the body of held and released. +type Hold struct { + // when, RFC 3339 + At string `json:"at,omitempty"` + // the caller the holder proved + By string `json:"by,omitempty"` + // the digest + Digest string `json:"digest"` + // the module holding the seat + Holder string `json:"holder"` + // the name + Name string `json:"name"` + // why + Reason string `json:"reason,omitempty"` +} + +// Listed is a type of the contract. +type Listed struct { + // for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by + Artifact string `json:"artifact,omitempty"` + // the manifest's digest, sha256: + Digest string `json:"digest"` + // a hold keeps it from retention + Held bool `json:"held"` + // it is an index, naming a manifest per platform + Index bool `json:"index"` + // for an index, the digests of the manifests it names + Platforms []string `json:"platforms,omitempty"` + // its bytes: its own content and every blob it names + Size int64 `json:"size"` + // the tags naming it, none for an untagged one + Tags []string `json:"tags,omitempty"` +} + +// Outcome is the body of published, refused and removed. +type Outcome struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // the module holding the seat that said it + Holder string `json:"holder"` + // what it is + Kind string `json:"kind"` + // the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact + Module string `json:"module,omitempty"` + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) + Name string `json:"name"` + // why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) + Reason string `json:"reason,omitempty"` + // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) + Reference string `json:"reference,omitempty"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // for kept: the other tags still naming its digest + Tags []string `json:"tags,omitempty"` + // the transfer it ends, when it ended one + Transfer string `json:"transfer,omitempty"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// The values of Outcome's kind. +const ( + OutcomeKindPackage = "package" + OutcomeKindImage = "image" + OutcomeKindBundle = "bundle" + OutcomeKindArchive = "archive" + OutcomeKindUpstream = "upstream" +) + +// Part is one part of a transfer. +type Part struct { + // the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob + Digest string `json:"digest"` + // its size in bytes + Size int64 `json:"size"` +} + +// Repository is a type of the contract. +type Repository struct { + // each manifest it holds, by digest + Manifests []Listed `json:"manifests"` + // the repository + Name string `json:"name"` +} + +// PublishInput is what publish takes. +type PublishInput struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) + Name string `json:"name"` + // every part's digest and size; none when the digest is already held + Parts []Part `json:"parts,omitempty"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the transfer's name, the caller's own; the same name opens the same transfer again + Transfer string `json:"transfer"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// The values of PublishInput's kind. +const ( + PublishInputKindPackage = "package" + PublishInputKindImage = "image" + PublishInputKindBundle = "bundle" + PublishInputKindArchive = "archive" + PublishInputKindUpstream = "upstream" +) + +// PublishAnswer is what publish answers. +type PublishAnswer struct { + // the digest is held already: nothing to send; the outcome follows as an event + Held bool `json:"held,omitempty"` + // the digests of the parts the holder lacks, to send by publish-part + Lacks []string `json:"lacks,omitempty"` + // the transfer + Transfer string `json:"transfer"` +} + +// PublishPartInput is what publish-part takes. +type PublishPartInput struct { + // the chunk's bytes, base64 (at most 760 KiB before encoding) + Data string `json:"data"` + // this chunk is the part's last + Last bool `json:"last"` + // the part's digest + Part string `json:"part"` + // the chunk's place in its part, from 0 + Sequence int64 `json:"sequence"` + // the transfer + Transfer string `json:"transfer"` +} + +// PublishPartAnswer is what publish-part answers. +type PublishPartAnswer struct { + // the part is whole and its bytes match its digest + Complete bool `json:"complete,omitempty"` + // the part + Part string `json:"part"` + // the chunks of the part held so far + Received int64 `json:"received,omitempty"` + // the transfer + Transfer string `json:"transfer"` +} + +// VersionsInput is what versions takes. +type VersionsInput struct { + // narrowed to one kind (optional) + Kind string `json:"kind,omitempty"` + // the name + Name string `json:"name"` +} + +// VersionsAnswer is what versions answers. +type VersionsAnswer struct { + // the name + Name string `json:"name"` + // newest first + Versions []Artifact `json:"versions"` +} + +// RemoveInput is what remove takes. +type RemoveInput struct { + // the digest (for a bundle, archive or copied base) + Digest string `json:"digest,omitempty"` + // the name + Name string `json:"name"` + // why it goes + Reason string `json:"reason"` + // the tag (for a tagged version) + Tag string `json:"tag,omitempty"` + // the version + Version string `json:"version,omitempty"` +} + +// The values of RemoveInput's reason. +const ( + RemoveInputReasonTagGone = "tag-gone" + RemoveInputReasonRetention = "retention" +) + +// RemoveAnswer is what remove answers. +type RemoveAnswer struct { + Artifact *Artifact `json:"artifact,omitempty"` + // it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) + Kept bool `json:"kept,omitempty"` + // why it was kept: shared-digest, another tag names its digest + Reason string `json:"reason,omitempty"` + // it is gone + Removed bool `json:"removed"` + // the other tags still naming its digest, when kept + Tags []string `json:"tags,omitempty"` +} + +// The values of RemoveAnswer's reason. +const ( + RemoveAnswerReasonSharedDigest = "shared-digest" +) + +// HoldInput is what hold takes. +type HoldInput struct { + // the digest + Digest string `json:"digest"` + // the name + Name string `json:"name"` + // which of the sweep's rules keeps it + Reason string `json:"reason"` +} + +// HoldAnswer is what hold answers. +type HoldAnswer struct { + // it is held + Held bool `json:"held"` + // when the hold began, RFC 3339 + Since string `json:"since,omitempty"` +} + +// ReleaseInput is what release takes. +type ReleaseInput struct { + // the digest + Digest string `json:"digest"` + // the name + Name string `json:"name"` + // why it is let go + Reason string `json:"reason"` +} + +// ReleaseAnswer is what release answers. +type ReleaseAnswer struct { + // a hold was let go; false when there was none + Released bool `json:"released"` +} + +// ListInput is what list takes. +type ListInput struct { + // the repository, /; every repository of the seat when not given + Name string `json:"name,omitempty"` +} + +// ListAnswer is what list answers. +type ListAnswer struct { + // each repository named, in order of its name + Repositories []Repository `json:"repositories"` +} + +// PublishedEvent is the body of the event published. +type PublishedEvent = Outcome + +// RefusedEvent is the body of the event refused. +type RefusedEvent = Outcome + +// RemovedEvent is the body of the event removed. +type RemovedEvent = Outcome + +// KeptEvent is the body of the event kept. +type KeptEvent = Outcome + +// HeldEvent is the body of the event held. +type HeldEvent = Hold + +// ReleasedEvent is the body of the event released. +type ReleasedEvent = Hold + +// HoldsBySeatNameDigestValue is one value of the state holds under the key ... +type HoldsBySeatNameDigestValue struct { + By string `json:"by"` + Digest string `json:"digest"` + Kind string `json:"kind"` + Name string `json:"name"` + Seat string `json:"seat"` + // when (RFC 3339 time) + Since string `json:"since"` + Why string `json:"why,omitempty"` +} + +// WindowsByCallerValueDroppedItem is a type of the contract. +type WindowsByCallerValueDroppedItem struct { + // when (RFC 3339 time) + At string `json:"at,omitempty"` + By string `json:"by,omitempty"` + Node string `json:"node"` + Why string `json:"why,omitempty"` +} + +// WindowsByCallerValue is one value of the state windows under the key . +type WindowsByCallerValue struct { + // when it last closed (RFC 3339 time) + Closed string `json:"closed,omitempty"` + // retired nodes dropped by hand + Dropped []WindowsByCallerValueDroppedItem `json:"dropped,omitempty"` + // the nodes holding it open + Holding []string `json:"holding,omitempty"` + LastHolding []string `json:"last_holding,omitempty"` + // the caller whose window it is + Name string `json:"name"` + Open bool `json:"open,omitempty"` + // node to when it began to hold + Since map[string]string `json:"since,omitempty"` + // of each node holding the window, why it holds it + Why map[string]string `json:"why,omitempty"` +} diff --git a/contracts/go/seats/ociartifactregistry/contract_test.go b/contracts/go/seats/ociartifactregistry/contract_test.go new file mode 100644 index 00000000..afd6ee2a --- /dev/null +++ b/contracts/go/seats/ociartifactregistry/contract_test.go @@ -0,0 +1,82 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package ociartifactregistry + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/oci-artifact-registry/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "published on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.published$")}, + {name: "refused on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.refused", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.refused$")}, + {name: "removed on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.removed$")}, + {name: "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", subject: "mesh.seat.oci-artifact-registry.event.kept", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", + decode: func(b []byte) (any, error) { var v KeptEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.kept$")}, + {name: "held on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.held", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", + decode: func(b []byte) (any, error) { var v HeldEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.held$")}, + {name: "released on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.released", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", + decode: func(b []byte) (any, error) { var v ReleasedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.released$")}, + {name: "a hold", subject: "", given: "{\"by\":\"operator@shanks\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"kind\":\"base\",\"name\":\"base\",\"seat\":\"oci-artifact-registry\",\"since\":\"2026-11-05T10:00:00Z\"}", wire: "{\"by\":\"operator@shanks\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"kind\":\"base\",\"name\":\"base\",\"seat\":\"oci-artifact-registry\",\"since\":\"2026-11-05T10:00:00Z\"}", + decode: func(b []byte) (any, error) { + var v HoldsBySeatNameDigestValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "an open window", subject: "", given: "{\"holding\":[\"ace\"],\"name\":\"mesh-delivery-builder\",\"open\":true,\"since\":{\"ace\":\"2026-11-05T10:00:00Z\"}}", wire: "{\"holding\":[\"ace\"],\"name\":\"mesh-delivery-builder\",\"open\":true,\"since\":{\"ace\":\"2026-11-05T10:00:00Z\"}}", + decode: func(b []byte) (any, error) { var v WindowsByCallerValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"bundle\",\"later\":{\"x\":1},\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "publish answers what it lacks", subject: "", given: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", wire: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", + decode: func(b []byte) (any, error) { var v PublishAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a chunk of a part", subject: "", given: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", wire: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", + decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "the versions held", subject: "", given: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", + decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a release of what is not held answers released false", subject: "", given: "{\"released\":false}", wire: "{\"released\":false}", + decode: func(b []byte) (any, error) { var v ReleaseAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a tagged version sharing its digest with another tag is kept, not removed", subject: "", given: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", wire: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", + decode: func(b []byte) (any, error) { var v RemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "list answers an untagged archive by the blob it keeps", subject: "", given: "{\"repositories\":[{\"manifests\":[{\"artifact\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"held\":true,\"index\":false,\"size\":4096}],\"name\":\"mail/files\"}]}", wire: "{\"repositories\":[{\"manifests\":[{\"artifact\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"held\":true,\"index\":false,\"size\":4096}],\"name\":\"mail/files\"}]}", + decode: func(b []byte) (any, error) { var v ListAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/contracts/go/seats/ociartifactregistry/go.mod b/contracts/go/seats/ociartifactregistry/go.mod new file mode 100644 index 00000000..f145223c --- /dev/null +++ b/contracts/go/seats/ociartifactregistry/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry + +go 1.26.0 diff --git a/contracts/module.json b/contracts/module.json new file mode 100644 index 00000000..113c2fc6 --- /dev/null +++ b/contracts/module.json @@ -0,0 +1,245 @@ +{ + "module": "mesh-delivery-contracts", + "version": "1", + "build": { + "artifacts": [ + { + "name": "go-containerimageregistry", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/containerimageregistry" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-gopackageregistry", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/gopackageregistry" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshcatalog", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshcatalog" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshdelivery", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshdelivery" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshdeliverychecker", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshdeliverychecker" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-nodebuildgo", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/nodebuildgo" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-nodebuildimage", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/nodebuildimage" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-nodebuildnode", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/nodebuildnode" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-npmpackageregistry", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/npmpackageregistry" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-ociartifactregistry", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/ociartifactregistry" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-containerimageregistryclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/containerimageregistryclient" + ], + "series": "0.1" + }, + { + "name": "go-gopackageregistryclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/gopackageregistryclient" + ], + "series": "0.1" + }, + { + "name": "go-npmpackageregistryclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/npmpackageregistryclient" + ], + "series": "0.1" + }, + { + "name": "go-ociartifactregistryclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/ociartifactregistryclient" + ], + "series": "0.1" + }, + { + "name": "ts-containerimageregistry", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/containerimageregistry" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-gopackageregistry", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/gopackageregistry" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-meshcatalog", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshcatalog" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-meshdelivery", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshdelivery" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-meshdeliverychecker", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshdeliverychecker" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-nodebuildgo", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/nodebuildgo" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-nodebuildimage", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/nodebuildimage" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-nodebuildnode", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/nodebuildnode" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-npmpackageregistry", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/npmpackageregistry" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-ociartifactregistry", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/ociartifactregistry" + ], + "series": "0.1", + "generated-from": "." + } + ] + }, + "resources": [] +} diff --git a/contracts/parts/build-step.json b/contracts/parts/build-step.json new file mode 100644 index 00000000..dfc809f6 --- /dev/null +++ b/contracts/parts/build-step.json @@ -0,0 +1,588 @@ +{ + "format": 1, + "part": "build-step", + "description": "What the three build seats share: the verbs that read and steer a node's builds, and the layouts of the asks they accept and the steps they keep. A seat lists its states against these (a part carries no states).", + "verbs": [ + { + "name": "current", + "description": "The asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", + "input": { + "type": "object", + "properties": {} + }, + "answer": { + "type": "object", + "description": "the steps running, the capacity and whether paused" + } + }, + { + "name": "pause", + "description": "Take no new ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module. A capacity of 0 is the lasting way to keep a node from building.", + "input": { + "type": "object", + "properties": {} + }, + "answer": { + "type": "object", + "description": "whether the node is paused" + } + }, + { + "name": "resume", + "description": "Take asks again on this node.", + "input": { + "type": "object", + "properties": {} + }, + "answer": { + "type": "object", + "description": "whether the node is paused" + } + } + ], + "defs": { + "StepAsk": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "id": { + "type": "string" + }, + "kind": { + "type": "string", + "description": "build, check or compose" + }, + "language": { + "type": "string", + "description": "go, typescript or image" + }, + "asker": { + "type": "string" + }, + "at": { + "type": "string", + "description": "when (RFC 3339 time)" + }, + "build": { + "type": "object", + "properties": { + "module": { + "type": "string" + }, + "path": { + "type": "string" + }, + "fp": { + "type": "string" + }, + "fetch": { + "description": "what the build module fetches: the repository, commit and directory, as the indexer states it" + }, + "artifacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "artifact": { + "type": "string" + }, + "from": { + "type": "string" + }, + "seat": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "required": [ + "artifact" + ] + } + }, + "publish": { + "type": "boolean" + }, + "toolchain": { + "type": "object", + "properties": { + "language": { + "type": "string" + }, + "version": { + "type": "string" + }, + "recipe": { + "type": "string" + } + }, + "required": [ + "language", + "version", + "recipe" + ] + } + }, + "required": [ + "module", + "artifacts" + ] + }, + "check": { + "type": "object", + "properties": { + "fetch": { + "description": "what the build module fetches: the repository, commit and directory, as the indexer states it" + }, + "levels": { + "type": "array", + "items": { + "type": "string", + "description": "unit or integration" + } + }, + "kept": { + "type": "boolean" + }, + "runner": { + "type": "string" + }, + "runner_when": { + "type": "string" + }, + "run": { + "type": "string" + }, + "packages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "fetch", + "levels" + ] + }, + "compose": { + "type": "object", + "properties": { + "fetch": { + "description": "what the build module fetches: the repository, commit and directory, as the indexer states it" + }, + "dir": { + "type": "string" + }, + "package": { + "type": "string" + }, + "request": { + "description": "the compose-candidate request" + }, + "reader": { + "type": "string" + } + }, + "required": [ + "fetch", + "dir", + "package", + "request" + ] + }, + "needs": { + "type": "array", + "items": { + "type": "string" + } + }, + "packages": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "package to the tagged version a build pins" + } + }, + "required": [ + "id", + "kind", + "language", + "asker" + ], + "description": "one ask of a build module" + }, + "BuildAnswer": { + "type": "object", + "properties": { + "artifacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "artifact": { + "type": "string" + }, + "digest": { + "type": "string" + }, + "reference": { + "type": "string" + } + }, + "required": [ + "artifact", + "digest" + ] + } + }, + "toolchain": { + "type": "object", + "properties": { + "language": { + "type": "string" + }, + "version": { + "type": "string" + }, + "recipe": { + "type": "string" + } + }, + "required": [ + "language", + "version", + "recipe" + ] + }, + "imports": { + "type": "array", + "items": { + "type": "string" + } + }, + "out-of-step": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "artifacts" + ] + }, + "CheckAnswer": { + "type": "object", + "properties": { + "fetch": { + "description": "what the build module fetches: the repository, commit and directory, as the indexer states it" + }, + "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" + ] + } + } + }, + "required": [ + "fetch" + ] + }, + "ComposeAnswer": { + "type": "object", + "properties": { + "built": { + "type": "boolean" + }, + "key": { + "type": "string" + }, + "verdict": { + "description": "the compose-candidate verdict" + } + }, + "required": [ + "built" + ] + }, + "Step": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "id": { + "type": "string" + }, + "tool": { + "type": "string" + }, + "node": { + "type": "string" + }, + "state": { + "type": "string", + "description": "running, passed, failed or refused" + }, + "started": { + "type": "string", + "description": "when (RFC 3339 time)" + }, + "ended": { + "type": "string", + "description": "when (RFC 3339 time)" + }, + "reason": { + "type": "string" + }, + "result": { + "description": "what the step answered" + }, + "what": { + "type": "string" + }, + "phase": { + "type": "string" + }, + "phase-at": { + "type": "string", + "description": "when (RFC 3339 time)" + }, + "owner": { + "type": "string" + } + }, + "required": [ + "id", + "tool", + "node", + "state", + "started" + ], + "description": "one step a build module runs on a node" + }, + "Toolchain": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "language": { + "type": "string", + "description": "go or typescript" + }, + "version": { + "type": "string" + }, + "recipe": { + "type": "string" + }, + "at": { + "type": "string", + "description": "when (RFC 3339 time)" + } + }, + "required": [ + "language", + "version", + "recipe" + ], + "description": "the toolchain a node reports" + }, + "Pause": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "paused": { + "type": "boolean" + }, + "by": { + "type": "string" + }, + "at": { + "type": "string", + "description": "when (RFC 3339 time)" + } + }, + "required": [ + "paused" + ], + "description": "whether the node takes no new ask" + }, + "Capacity": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "effective": { + "type": "integer" + }, + "configured": { + "type": "integer" + }, + "configured-from": { + "type": "string" + }, + "quota-cores": { + "type": "number" + }, + "quota-from": { + "type": "string" + }, + "cores-per-step": { + "type": "number" + }, + "by-cpu": { + "type": "integer" + }, + "build-hours": { + "type": "string" + }, + "outside-hours": { + "type": "boolean" + }, + "build-hours-fault": { + "type": "string" + }, + "ineligible": { + "type": "string" + }, + "why": { + "type": "string" + }, + "at": { + "type": "string", + "description": "when (RFC 3339 time)" + } + }, + "required": [ + "effective", + "configured", + "why" + ], + "description": "how many steps the node runs at once" + } + } +} diff --git a/contracts/parts/registry.json b/contracts/parts/registry.json new file mode 100644 index 00000000..ca0022f8 --- /dev/null +++ b/contracts/parts/registry.json @@ -0,0 +1,436 @@ +{ + "format": 1, + "part": "registry", + "description": "The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved.", + "verbs": [ + { + "name": "publish", + "description": "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", + "callers": "the build modules", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)" + }, + "version": { + "type": "string", + "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone" + }, + "kind": { + "type": "string", + "enum": [ + "package", + "image", + "bundle", + "archive", + "upstream" + ], + "description": "what it is" + }, + "tag": { + "type": "string", + "description": "the tag it carries, for a tagged version" + }, + "digest": { + "type": "string", + "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)" + }, + "fingerprint": { + "type": "string", + "description": "the build's fingerprint" + }, + "commit": { + "type": "string", + "description": "the source commit it was built from" + }, + "transfer": { + "type": "string", + "description": "the transfer's name, the caller's own; the same name opens the same transfer again" + }, + "parts": { + "type": "array", + "items": { + "$ref": "#/defs/Part" + }, + "description": "every part's digest and size; none when the digest is already held" + } + }, + "required": [ + "transfer", + "name", + "kind", + "digest" + ] + }, + "answer": { + "type": "object", + "properties": { + "transfer": { + "type": "string", + "description": "the transfer" + }, + "lacks": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the digests of the parts the holder lacks, to send by publish-part" + }, + "held": { + "type": "boolean", + "description": "the digest is held already: nothing to send; the outcome follows as an event" + } + }, + "required": [ + "transfer" + ] + }, + "optional": true + }, + { + "name": "publish-part", + "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", + "callers": "the build modules, and only the proved caller that opened the transfer", + "input": { + "type": "object", + "properties": { + "transfer": { + "type": "string", + "description": "the transfer" + }, + "part": { + "type": "string", + "description": "the part's digest" + }, + "sequence": { + "type": "integer", + "description": "the chunk's place in its part, from 0" + }, + "last": { + "type": "boolean", + "description": "this chunk is the part's last" + }, + "data": { + "type": "string", + "description": "the chunk's bytes, base64 (at most 760 KiB before encoding)" + } + }, + "required": [ + "transfer", + "part", + "sequence", + "last", + "data" + ] + }, + "answer": { + "type": "object", + "properties": { + "transfer": { + "type": "string", + "description": "the transfer" + }, + "part": { + "type": "string", + "description": "the part" + }, + "received": { + "type": "integer", + "description": "the chunks of the part held so far" + }, + "complete": { + "type": "boolean", + "description": "the part is whole and its bytes match its digest" + } + }, + "required": [ + "transfer", + "part" + ] + }, + "optional": true + }, + { + "name": "versions", + "description": "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", + "callers": "any caller whose role grants it", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the name" + }, + "kind": { + "type": "string", + "description": "narrowed to one kind (optional)" + } + }, + "required": [ + "name" + ] + }, + "answer": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the name" + }, + "versions": { + "type": "array", + "items": { + "$ref": "#/defs/Artifact" + }, + "description": "newest first" + } + }, + "required": [ + "name", + "versions" + ] + }, + "optional": true + }, + { + "name": "remove", + "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", + "callers": "the builder, for a tagged version; the artifact sweep's holder, for retention", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the name" + }, + "version": { + "type": "string", + "description": "the version" + }, + "tag": { + "type": "string", + "description": "the tag (for a tagged version)" + }, + "digest": { + "type": "string", + "description": "the digest (for a bundle, archive or copied base)" + }, + "reason": { + "type": "string", + "enum": [ + "tag-gone", + "retention" + ], + "description": "why it goes" + } + }, + "required": [ + "name", + "reason" + ] + }, + "answer": { + "type": "object", + "properties": { + "removed": { + "type": "boolean", + "description": "it is gone" + }, + "kept": { + "type": "boolean", + "description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)" + }, + "reason": { + "type": "string", + "enum": [ + "shared-digest" + ], + "description": "why it was kept: shared-digest, another tag names its digest" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the other tags still naming its digest, when kept" + }, + "artifact": { + "$ref": "#/defs/Artifact" + } + }, + "required": [ + "removed" + ] + }, + "optional": true + } + ], + "events": [ + { + "name": "published", + "body": { + "$ref": "#/defs/Outcome" + } + }, + { + "name": "refused", + "body": { + "$ref": "#/defs/Outcome" + } + }, + { + "name": "removed", + "body": { + "$ref": "#/defs/Outcome" + } + }, + { + "name": "kept", + "body": { + "$ref": "#/defs/Outcome" + } + } + ], + "defs": { + "Artifact": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)" + }, + "version": { + "type": "string", + "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone" + }, + "kind": { + "type": "string", + "enum": [ + "package", + "image", + "bundle", + "archive", + "upstream" + ], + "description": "what it is" + }, + "tag": { + "type": "string", + "description": "the tag it carries, for a tagged version" + }, + "digest": { + "type": "string", + "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)" + }, + "fingerprint": { + "type": "string", + "description": "the build's fingerprint" + }, + "commit": { + "type": "string", + "description": "the source commit it was built from" + } + }, + "required": [ + "name", + "kind", + "digest" + ], + "description": "one published thing, as every registry event and answer names it" + }, + "Part": { + "type": "object", + "properties": { + "digest": { + "type": "string", + "description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob" + }, + "size": { + "type": "integer", + "description": "its size in bytes" + } + }, + "required": [ + "digest", + "size" + ], + "description": "one part of a transfer" + }, + "Outcome": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)" + }, + "module": { + "type": "string", + "description": "the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact" + }, + "version": { + "type": "string", + "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone" + }, + "kind": { + "type": "string", + "enum": [ + "package", + "image", + "bundle", + "archive", + "upstream" + ], + "description": "what it is" + }, + "tag": { + "type": "string", + "description": "the tag it carries, for a tagged version" + }, + "digest": { + "type": "string", + "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)" + }, + "fingerprint": { + "type": "string", + "description": "the build's fingerprint" + }, + "commit": { + "type": "string", + "description": "the source commit it was built from" + }, + "holder": { + "type": "string", + "description": "the module holding the seat that said it" + }, + "transfer": { + "type": "string", + "description": "the transfer it ends, when it ended one" + }, + "reference": { + "type": "string", + "description": "the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9)" + }, + "reason": { + "type": "string", + "description": "why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest)" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "for kept: the other tags still naming its digest" + } + }, + "required": [ + "name", + "kind", + "digest", + "holder" + ], + "description": "the body of published, refused and removed" + } + } +} diff --git a/contracts/seats/container-image-registry/fixtures/events.json b/contracts/seats/container-image-registry/fixtures/events.json new file mode 100644 index 00000000..65e98e58 --- /dev/null +++ b/contracts/seats/container-image-registry/fixtures/events.json @@ -0,0 +1,98 @@ +[ + { + "name": "published on the seat's subject", + "of": "event:published", + "subject": "mesh.seat.container-image-registry.event.published", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "image", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "distribution", + "transfer": "t1", + "reference": "container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + }, + { + "name": "refused on the seat's subject", + "of": "event:refused", + "subject": "mesh.seat.container-image-registry.event.refused", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "image", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "distribution", + "transfer": "t1", + "reference": "container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "reason": "retention" + } + }, + { + "name": "removed on the seat's subject", + "of": "event:removed", + "subject": "mesh.seat.container-image-registry.event.removed", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "image", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "distribution", + "transfer": "t1", + "reference": "container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "reason": "retention" + } + }, + { + "name": "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", + "of": "event:kept", + "subject": "mesh.seat.container-image-registry.event.kept", + "given": { + "name": "mesh-sdk", + "version": "0.1.19-feat-x.2", + "kind": "image", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "distribution", + "reference": "container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "reason": "shared-digest", + "tag": "0.1.19-feat-x.2", + "tags": [ + "0.1.19" + ] + } + }, + { + "name": "held on the seat's subject", + "of": "event:held", + "subject": "mesh.seat.container-image-registry.event.held", + "given": { + "name": "mesh-sdk", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reason": "kept by the sweep's rule", + "holder": "distribution", + "by": "mesh-artifact-sweep", + "at": "2026-11-04T12:00:00Z" + } + }, + { + "name": "released on the seat's subject", + "of": "event:released", + "subject": "mesh.seat.container-image-registry.event.released", + "given": { + "name": "mesh-sdk", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reason": "kept by the sweep's rule", + "holder": "distribution", + "by": "mesh-artifact-sweep", + "at": "2026-11-04T12:00:00Z" + } + } +] diff --git a/contracts/seats/container-image-registry/fixtures/states.json b/contracts/seats/container-image-registry/fixtures/states.json new file mode 100644 index 00000000..70952e23 --- /dev/null +++ b/contracts/seats/container-image-registry/fixtures/states.json @@ -0,0 +1,28 @@ +[ + { + "name": "a hold", + "of": "state:holds/..", + "given": { + "seat": "container-image-registry", + "name": "base", + "kind": "base", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "by": "operator@shanks", + "since": "2026-11-05T10:00:00Z" + } + }, + { + "name": "an open window", + "of": "state:windows/", + "given": { + "name": "mesh-delivery-builder", + "open": true, + "holding": [ + "ace" + ], + "since": { + "ace": "2026-11-05T10:00:00Z" + } + } + } +] diff --git a/contracts/seats/container-image-registry/fixtures/verbs.json b/contracts/seats/container-image-registry/fixtures/verbs.json new file mode 100644 index 00000000..e024a813 --- /dev/null +++ b/contracts/seats/container-image-registry/fixtures/verbs.json @@ -0,0 +1,149 @@ +[ + { + "name": "publish opens a transfer", + "of": "verb:publish.input", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "image", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ] + } + }, + { + "name": "a field the reader does not know is dropped", + "of": "verb:publish.input", + "why": "ADR 0608 §6: a reader ignores a field it does not know", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "image", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ], + "later": { + "x": 1 + } + }, + "wire": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "image", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ] + } + }, + { + "name": "publish answers what it lacks", + "of": "verb:publish.answer", + "given": { + "transfer": "t1", + "lacks": [ + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ] + } + }, + { + "name": "a chunk of a part", + "of": "verb:publish-part.input", + "given": { + "transfer": "t1", + "part": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "sequence": 0, + "last": true, + "data": "aGVsbG8=" + } + }, + { + "name": "the versions held", + "of": "verb:versions.answer", + "given": { + "name": "mesh-sdk", + "versions": [ + { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc" + } + ] + } + }, + { + "name": "a release of what is not held answers released false", + "of": "verb:release.answer", + "why": "A3t: the outcome asked holds already, so it is no error", + "given": { + "released": false + } + }, + { + "name": "a tagged version sharing its digest with another tag is kept, not removed", + "of": "verb:remove.answer", + "why": "G-sdk-contracts 3: the store cannot remove the tag alone, so the answer says what holds", + "given": { + "removed": false, + "kept": true, + "reason": "shared-digest", + "tags": [ + "0.1.19" + ] + } + }, + { + "name": "list answers an index with its platforms, its tag and its hold", + "of": "verb:list.answer", + "given": { + "repositories": [ + { + "name": "mail/server", + "manifests": [ + { + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "index": true, + "platforms": [ + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "tags": [ + "3" + ], + "size": 2048, + "held": false + }, + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "index": false, + "size": 1024, + "held": false + } + ] + } + ] + } + } +] diff --git a/contracts/seats/container-image-registry/seat.json b/contracts/seats/container-image-registry/seat.json new file mode 100644 index 00000000..9f95f1e4 --- /dev/null +++ b/contracts/seats/container-image-registry/seat.json @@ -0,0 +1,479 @@ +{ + "format": 1, + "seat": "container-image-registry", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0605", + "description": "The mesh's container images, in the one artifact store: the OCI distribution protocol's read side, an image's tags and mirroring an outside image.", + "includes": [ + "registry" + ], + "verbs": [ + { + "name": "mirror", + "description": "Copy an outside image into the store by its pinned digest; emits published or refused.", + "callers": "mesh-build-image's holder of the image build seat", + "input": { + "type": "object", + "properties": { + "source": { + "type": "string", + "description": "the outside image's reference" + }, + "digest": { + "type": "string", + "description": "its pinned digest, sha256:" + }, + "name": { + "type": "string", + "description": "the name it is kept under" + } + }, + "required": [ + "source", + "digest", + "name" + ] + }, + "answer": { + "type": "object", + "properties": { + "reference": { + "type": "string", + "description": "the reference the seat answers" + }, + "digest": { + "type": "string", + "description": "the digest stored" + } + }, + "required": [ + "reference", + "digest" + ] + }, + "optional": true + }, + { + "name": "tags", + "description": "The tags of one image and the digest each names.", + "callers": "any caller whose role grants it", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the image" + } + }, + "required": [ + "name" + ] + }, + "answer": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the image" + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "description": "the tag" + }, + "digest": { + "type": "string", + "description": "its digest" + } + }, + "required": [ + "tag", + "digest" + ] + }, + "description": "every tag" + } + }, + "required": [ + "name", + "tags" + ] + }, + "optional": true + }, + { + "name": "hold", + "description": "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", + "callers": "the module that runs the artifact sweep alone", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the name" + }, + "digest": { + "type": "string", + "description": "the digest" + }, + "reason": { + "type": "string", + "description": "which of the sweep's rules keeps it" + } + }, + "required": [ + "name", + "digest", + "reason" + ] + }, + "answer": { + "type": "object", + "properties": { + "held": { + "type": "boolean", + "description": "it is held" + }, + "since": { + "type": "string", + "description": "when the hold began, RFC 3339" + } + }, + "required": [ + "held" + ] + }, + "optional": true + }, + { + "name": "release", + "description": "Let a hold go; emits released. A release of what is not held answers released false and emits nothing, never an error: the outcome asked holds already. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", + "callers": "the module that runs the artifact sweep; the operator's session through the holder's tool", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the name" + }, + "digest": { + "type": "string", + "description": "the digest" + }, + "reason": { + "type": "string", + "description": "why it is let go" + } + }, + "required": [ + "name", + "digest", + "reason" + ] + }, + "answer": { + "type": "object", + "properties": { + "released": { + "type": "boolean", + "description": "a hold was let go; false when there was none" + } + }, + "required": [ + "released" + ] + }, + "optional": true + }, + { + "name": "list", + "description": "What the store holds of one repository, or of every repository the seat holds when none is named: every image manifest and index, each with its digest, whether it is an index (and the manifests an index names), the tags naming it, its size and whether it is held. Read from the store itself, so a manifest no tag names is listed too; it reads only (ADR 0632 §3).", + "callers": "any caller whose role grants it", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the repository, /; every repository of the seat when not given" + } + } + }, + "answer": { + "type": "object", + "properties": { + "repositories": { + "type": "array", + "items": { + "$ref": "#/defs/Repository" + }, + "description": "each repository named, in order of its name" + } + }, + "required": [ + "repositories" + ] + }, + "optional": true + } + ], + "events": [ + { + "name": "held", + "body": { + "$ref": "#/defs/Hold" + } + }, + { + "name": "released", + "body": { + "$ref": "#/defs/Hold" + } + } + ], + "family": { + "name": "container-image-registry.events", + "max-age-days": 7 + }, + "defs": { + "Hold": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the name" + }, + "digest": { + "type": "string", + "description": "the digest" + }, + "reason": { + "type": "string", + "description": "why" + }, + "holder": { + "type": "string", + "description": "the module holding the seat" + }, + "by": { + "type": "string", + "description": "the caller the holder proved" + }, + "at": { + "type": "string", + "description": "when, RFC 3339" + } + }, + "required": [ + "name", + "digest", + "holder" + ], + "description": "the body of held and released" + }, + "Repository": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the repository" + }, + "manifests": { + "type": "array", + "items": { + "$ref": "#/defs/Listed" + }, + "description": "each manifest it holds, by digest" + } + }, + "required": [ + "name", + "manifests" + ] + }, + "Listed": { + "type": "object", + "properties": { + "digest": { + "type": "string", + "description": "the manifest's digest, sha256:" + }, + "index": { + "type": "boolean", + "description": "it is an index, naming a manifest per platform" + }, + "platforms": { + "type": "array", + "items": { + "type": "string" + }, + "description": "for an index, the digests of the manifests it names" + }, + "artifact": { + "type": "string", + "description": "for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the tags naming it, none for an untagged one" + }, + "size": { + "type": "integer", + "description": "its bytes: its own content and every blob it names" + }, + "held": { + "type": "boolean", + "description": "a hold keeps it from retention" + } + }, + "required": [ + "digest", + "index", + "size", + "held" + ] + } + }, + "states": [ + { + "name": "holds", + "template": "state", + "history": 1, + "description": "the bundles, archives and copied bases kept from retention's removal", + "key-forms": [ + { + "key": "..", + "value": { + "type": "object", + "description": "one hold: an untagged bundle, archive or copied base kept from retention's removal", + "properties": { + "seat": { + "type": "string" + }, + "name": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "digest": { + "type": "string" + }, + "by": { + "type": "string" + }, + "why": { + "type": "string" + }, + "since": { + "type": "string", + "description": "when (RFC 3339 time)" + } + }, + "required": [ + "seat", + "name", + "kind", + "digest", + "by", + "since" + ] + }, + "description": "one hold" + } + ] + }, + { + "name": "windows", + "template": "state", + "history": 1, + "description": "the direct-write windows, one per caller", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "description": "the direct-write window of one caller: open while a node holds it open", + "properties": { + "name": { + "type": "string", + "description": "the caller whose window it is" + }, + "open": { + "type": "boolean" + }, + "holding": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the nodes holding it open" + }, + "since": { + "type": "object", + "additionalProperties": { + "type": "string", + "description": "when (RFC 3339 time)" + }, + "description": "node to when it began to hold" + }, + "closed": { + "type": "string", + "description": "when it last closed (RFC 3339 time)" + }, + "last_holding": { + "type": "array", + "items": { + "type": "string" + } + }, + "dropped": { + "type": "array", + "items": { + "type": "object", + "properties": { + "node": { + "type": "string" + }, + "by": { + "type": "string" + }, + "why": { + "type": "string" + }, + "at": { + "type": "string", + "description": "when (RFC 3339 time)" + } + }, + "required": [ + "node" + ] + }, + "description": "retired nodes dropped by hand" + }, + "why": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "of each node holding the window, why it holds it" + } + }, + "required": [ + "name" + ] + }, + "description": "one caller's window" + } + ] + } + ] +} diff --git a/contracts/seats/git/seat.json b/contracts/seats/git/seat.json new file mode 100644 index 00000000..836285ab --- /dev/null +++ b/contracts/seats/git/seat.json @@ -0,0 +1,8 @@ +{ + "format": 1, + "seat": "git", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0111", + "delivers": "git" +} diff --git a/contracts/seats/go-package-registry/fixtures/events.json b/contracts/seats/go-package-registry/fixtures/events.json new file mode 100644 index 00000000..c3a8b16d --- /dev/null +++ b/contracts/seats/go-package-registry/fixtures/events.json @@ -0,0 +1,73 @@ +[ + { + "name": "published on the seat's subject", + "of": "event:published", + "subject": "mesh.seat.go-package-registry.event.published", + "given": { + "name": "git.novox.be/novox/mesh-sdk/go", + "module": "git.novox.be/novox/mesh-sdk/go", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "gitea", + "transfer": "t1", + "reference": "git.novox.be/novox/mesh-sdk/go@0.1.19" + } + }, + { + "name": "refused on the seat's subject", + "of": "event:refused", + "subject": "mesh.seat.go-package-registry.event.refused", + "given": { + "name": "git.novox.be/novox/mesh-sdk/go", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "gitea", + "transfer": "t1", + "reference": "git.novox.be/novox/mesh-sdk/go@0.1.19", + "reason": "retention" + } + }, + { + "name": "removed on the seat's subject", + "of": "event:removed", + "subject": "mesh.seat.go-package-registry.event.removed", + "given": { + "name": "git.novox.be/novox/mesh-sdk/go", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "gitea", + "transfer": "t1", + "reference": "git.novox.be/novox/mesh-sdk/go@0.1.19", + "reason": "retention" + } + }, + { + "name": "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", + "of": "event:kept", + "subject": "mesh.seat.go-package-registry.event.kept", + "given": { + "name": "git.novox.be/novox/mesh-sdk/go", + "version": "0.1.19-feat-x.2", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "gitea", + "reference": "git.novox.be/novox/mesh-sdk/go@0.1.19", + "reason": "shared-digest", + "tag": "0.1.19-feat-x.2", + "tags": [ + "0.1.19" + ] + } + } +] diff --git a/contracts/seats/go-package-registry/fixtures/verbs.json b/contracts/seats/go-package-registry/fixtures/verbs.json new file mode 100644 index 00000000..be6a53a3 --- /dev/null +++ b/contracts/seats/go-package-registry/fixtures/verbs.json @@ -0,0 +1,110 @@ +[ + { + "name": "publish opens a transfer", + "of": "verb:publish.input", + "given": { + "name": "git.novox.be/novox/mesh-sdk/go", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ] + } + }, + { + "name": "a field the reader does not know is dropped", + "of": "verb:publish.input", + "why": "ADR 0608 §6: a reader ignores a field it does not know", + "given": { + "name": "git.novox.be/novox/mesh-sdk/go", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ], + "later": { + "x": 1 + } + }, + "wire": { + "name": "git.novox.be/novox/mesh-sdk/go", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ] + } + }, + { + "name": "publish answers what it lacks", + "of": "verb:publish.answer", + "given": { + "transfer": "t1", + "lacks": [ + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ] + } + }, + { + "name": "a chunk of a part", + "of": "verb:publish-part.input", + "given": { + "transfer": "t1", + "part": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "sequence": 0, + "last": true, + "data": "aGVsbG8=" + } + }, + { + "name": "the versions held", + "of": "verb:versions.answer", + "given": { + "name": "git.novox.be/novox/mesh-sdk/go", + "versions": [ + { + "name": "git.novox.be/novox/mesh-sdk/go", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc" + } + ] + } + }, + { + "name": "a tagged version sharing its digest with another tag is kept, not removed", + "of": "verb:remove.answer", + "why": "G-sdk-contracts 3: the store cannot remove the tag alone, so the answer says what holds", + "given": { + "removed": false, + "kept": true, + "reason": "shared-digest", + "tags": [ + "0.1.19" + ] + } + } +] diff --git a/contracts/seats/go-package-registry/seat.json b/contracts/seats/go-package-registry/seat.json new file mode 100644 index 00000000..edf39986 --- /dev/null +++ b/contracts/seats/go-package-registry/seat.json @@ -0,0 +1,34 @@ +{ + "format": 1, + "seat": "go-package-registry", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0605", + "description": "The mesh's Go modules: Go's module proxy protocol's read side, and publishing over the bus.", + "delivers": "go-package-registry", + "includes": [ + "registry" + ], + "family": { + "name": "go-package-registry.events", + "max-age-days": 7 + }, + "provisions": [ + { + "name": "go-package-registry", + "form": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "the read side's address" + } + }, + "required": [ + "url" + ], + "description": "form none: the Go module proxy's read side, answered anonymously, with no credential (design 51)" + } + } + ] +} diff --git a/contracts/seats/mesh-artifact-store/seat.json b/contracts/seats/mesh-artifact-store/seat.json new file mode 100644 index 00000000..84c8a57e --- /dev/null +++ b/contracts/seats/mesh-artifact-store/seat.json @@ -0,0 +1,7 @@ +{ + "format": 1, + "seat": "mesh-artifact-store", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0075" +} diff --git a/contracts/seats/mesh-artifact-sweep/seat.json b/contracts/seats/mesh-artifact-sweep/seat.json new file mode 100644 index 00000000..602a5f8d --- /dev/null +++ b/contracts/seats/mesh-artifact-sweep/seat.json @@ -0,0 +1,103 @@ +{ + "format": 1, + "seat": "mesh-artifact-sweep", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0571", + "verbs": [ + { + "name": "artifacts", + "description": "Every artifact the mesh made that lives in its artifact store, each with its repository, digest, kind (image or archive) and state: kept, and why (a want names it, or one of the five newest builds of its module in the builder's log), eligible (made, kept for no reason, not yet let go), or unrecorded (in the store and named by nothing the mesh made: never removed). Reads only.", + "input": { + "properties": { + "collected": { + "description": "\"true\": list the collected references too (counted always)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "repository": { + "description": "only this repository's references, as \u003cmodule\u003e/\u003cartifact\u003e (the counts stay whole)", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "held", + "description": "Whether the artifact store holds each reference given, asked with one HEAD each: held, gone (the store answered 404) or unknown with why. The builder asks it before it reuses a done key. Reads only.", + "input": { + "properties": { + "references": { + "description": "the artifacts' references, artifact-store://\u003cmodule\u003e/\u003cartifact\u003e/blobs/sha256:\u003chex\u003e: a list, or one text separated by commas", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "references" + ], + "type": "object" + } + }, + { + "name": "collect", + "description": "The artifact store's sweep, now: what is eligible is let go of. Without confirm a dry run: what would be let go, nothing held or deleted. With confirm and why: every kept archive held first, then each eligible artifact let go of, oldest first, an index with the platform manifests no kept index names, each deletion recorded in the hand-act log. Bounded by most (500 by default, at most 5000) and 45 seconds; what is left is said. Never touches a digest the mesh did not make.", + "input": { + "properties": { + "confirm": { + "description": "\"true\": let go of what is eligible (needs why); without it nothing is done", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "most": { + "description": "let go of at most this many (500 by default, at most 5000)", + "type": "string" + }, + "why": { + "description": "with confirm: why, required and recorded", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "mirrors", + "description": "The bases the mesh copied into its artifact store, each kept (a kept build stood on it), eligible (only builds no longer kept stood on it) or unrecorded (no build the builder's log holds stood on it: never removed), by what the builder's log says each build stood on. Records nothing by hand.", + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "images", + "description": "Every container image one machine's want and have name, each with the resources naming it: what the machine keeps when it prunes its images. Reads only.", + "input": { + "properties": { + "node": { + "description": "the machine's name", + "type": "string" + } + }, + "required": [ + "node" + ], + "type": "object" + } + } + ], + "events": [ + { + "name": "artifacts-gone" + } + ] +} diff --git a/contracts/seats/mesh-catalog/fixtures/states.json b/contracts/seats/mesh-catalog/fixtures/states.json new file mode 100644 index 00000000..9d902dc9 --- /dev/null +++ b/contracts/seats/mesh-catalog/fixtures/states.json @@ -0,0 +1,105 @@ +[ + { + "name": "a manifest", + "of": "state:manifests/", + "given": { + "v": 1, + "module": "nats", + "manifest": "{\"module\":\"nats\"}" + } + }, + { + "name": "a module", + "of": "state:modules/", + "given": { + "v": 1, + "repository": "novox/mesh-catalog", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "fp2": "src2:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + }, + { + "name": "a candidate", + "of": "state:candidates/", + "given": { + "v": 1, + "group": "feat/registry", + "base": 3, + "heads": "0123456789ab", + "pulls": [ + { + "repository": "novox/mesh-sdk", + "number": 1, + "head": "cccccccccccccccccccccccccccccccccccccccc", + "modules": [ + "nats" + ] + } + ] + } + }, + { + "name": "a verdict", + "of": "state:verdicts/", + "given": { + "v": 1, + "module": "nats", + "state": "accepted", + "judge": "trunk" + } + }, + { + "name": "the predefined list", + "of": "state:predefined/list", + "given": { + "v": 1, + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "list": { + "streams": [] + } + } + }, + { + "name": "a pull request's configuration", + "of": "state:config-candidates/", + "given": { + "v": 1, + "pull": "novox/mesh-catalog#1", + "head": "cccccccccccccccccccccccccccccccccccccccc", + "declares": { + "nats": { + "limit": { + "kind": "setting", + "default": 1, + "why": "a bound" + } + } + } + } + }, + { + "name": "a published Go module", + "of": "state:packages/", + "given": { + "v": 1, + "module": "git.novox.be/novox/mesh-sdk/go", + "repository": "novox/mesh-sdk", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "packages": [ + { + "package": "git.novox.be/novox/mesh-sdk/go/keys", + "digest": "ab" + } + ] + } + }, + { + "name": "a package series", + "of": "state:packages/.", + "given": { + "v": 1, + "name": "go-keys", + "series": "0.1" + } + } +] diff --git a/contracts/seats/mesh-catalog/seat.json b/contracts/seats/mesh-catalog/seat.json new file mode 100644 index 00000000..92a8307e --- /dev/null +++ b/contracts/seats/mesh-catalog/seat.json @@ -0,0 +1,858 @@ +{ + "format": 1, + "seat": "mesh-catalog", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0604", + "verbs": [ + { + "name": "modules", + "description": "Every module the trunk holds, each with its repository, its directory there, the merge commit it was last changed at and its fingerprint (or why it has none): the catalog of the mesh, read from its holder's state. Reads only.", + "optional": true, + "input": { + "properties": {}, + "type": "object" + }, + "answer": { + "type": "object", + "description": "what modules answers, as its description says" + } + }, + { + "name": "module", + "description": "One module whole: its repository, directory, commit and fingerprint, and its manifest as the trunk holds it. Older versions are in its repository's history. Reads only.", + "optional": true, + "input": { + "properties": { + "module": { + "description": "the module's name", + "type": "string" + } + }, + "required": [ + "module" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "what module answers, as its description says" + } + }, + { + "name": "provides", + "description": "Which modules provide a provision (a module provides its own name too) and which require it, from the module graph's provision edges. Reads only.", + "optional": true, + "input": { + "properties": { + "provision": { + "description": "the provision, such as postgres-database", + "type": "string" + } + }, + "required": [ + "provision" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "what provides answers, as its description says" + } + }, + { + "name": "dependents", + "description": "The modules whose build edges reach a module, directly or through others (a base, a mesh package it publishes, the build module that builds them): what a change of it builds again, from the module graph. Reads only.", + "optional": true, + "input": { + "properties": { + "module": { + "description": "the module's name", + "type": "string" + } + }, + "required": [ + "module" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "what dependents answers, as its description says" + } + } + ], + "description": "The catalog of the mesh: every module the trunk holds with its manifest, fingerprint and verdict, the packages published, and the seat list (ADR 0636 §1, §4).", + "family": { + "name": "mesh-catalog.events", + "max-age-days": 7 + }, + "events": [ + { + "name": "seat-definition-changed", + "body": { + "type": "object", + "properties": { + "seat": { + "type": "string" + }, + "source": { + "type": "string", + "description": "trunk" + }, + "digest": { + "type": "string" + } + }, + "required": [ + "seat", + "source", + "digest" + ] + } + }, + { + "name": "seat-definition-removed", + "body": { + "type": "object", + "properties": { + "seat": { + "type": "string" + } + }, + "required": [ + "seat" + ] + } + } + ], + "states": [ + { + "name": "manifests", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "module": { + "type": "string" + }, + "manifest": { + "type": "string", + "description": "the manifest, UTF-8 JSON text" + } + }, + "required": [ + "module", + "manifest" + ] + }, + "description": "a manifest by its digest, written once" + } + ], + "description": "every manifest the indexer judged", + "history": 1 + }, + { + "name": "modules", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "fp": { + "type": "string", + "description": "the old-form fingerprint" + }, + "none": { + "type": "string" + }, + "fp2": { + "type": "string", + "description": "src2: fingerprint" + }, + "none2": { + "type": "string", + "description": "why it has none" + }, + "manifest": { + "type": "string", + "description": "manifest digest" + }, + "inputs2": { + "type": "object", + "properties": { + "tree": { + "type": "string" + }, + "bases": { + "type": "array", + "items": { + "type": "string" + } + }, + "builder": { + "type": "string" + }, + "contexts": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "locks": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "paths": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "packages": { + "type": "array", + "items": { + "type": "string" + } + }, + "imports": { + "type": "array", + "items": { + "type": "string" + } + }, + "import-form": { + "type": "string" + } + }, + "required": [ + "tree" + ] + }, + "src": { + "type": "integer", + "description": "the source revision" + }, + "fp-src": { + "type": "integer" + }, + "repository": { + "type": "string" + }, + "commit": { + "type": "string" + }, + "path": { + "type": "string" + }, + "toolchains": { + "type": "object", + "additionalProperties": { + "description": "toolchain used" + } + }, + "paths": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "packages": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "verdict": { + "type": "string" + }, + "problems": { + "type": "array", + "items": { + "type": "string" + } + }, + "unknown": { + "type": "array", + "items": { + "type": "string" + } + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + }, + "needs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "repository", + "commit" + ], + "description": "one module as the trunk holds it" + }, + "description": "a module the trunk holds" + } + ], + "description": "the module catalog", + "history": 20 + }, + { + "name": "candidates", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "group": { + "type": "string" + }, + "base": { + "type": "integer", + "description": "the source revision it is read on" + }, + "heads": { + "type": "string" + }, + "pulls": { + "type": "array", + "items": { + "type": "object", + "properties": { + "repository": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "head": { + "type": "string" + }, + "modules": { + "type": "array", + "items": { + "type": "string" + } + }, + "fps": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "fp2": { + "type": "string" + }, + "none2": { + "type": "string" + }, + "manifest": { + "type": "string" + }, + "paths": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "packages": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "required": [ + "repository", + "number", + "head", + "modules" + ] + } + }, + "fpset": { + "type": "string" + } + }, + "required": [ + "group", + "base", + "heads", + "pulls" + ], + "description": "the modules a group of open pull requests would change" + }, + "description": "a group's candidate" + } + ], + "description": "the candidates of open groups", + "history": 10 + }, + { + "name": "verdicts", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "module": { + "type": "string" + }, + "state": { + "type": "string" + }, + "problems": { + "type": "array", + "items": { + "type": "string" + } + }, + "unknown": { + "type": "array", + "items": { + "type": "string" + } + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + }, + "judge": { + "type": "string" + }, + "at": { + "type": "string", + "description": "when (RFC 3339 time)" + } + }, + "required": [ + "module", + "state" + ], + "description": "a manifest's verdict, by the judge named" + }, + "description": "a manifest's verdict by its digest" + } + ], + "description": "the manifest verdicts", + "history": 5 + }, + { + "name": "predefined", + "template": "state", + "key-forms": [ + { + "key": "list", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "commit": { + "type": "string" + }, + "at": { + "type": "string", + "description": "when (RFC 3339 time)" + }, + "list": { + "description": "the foundation's predefined list as kept" + } + }, + "required": [ + "commit", + "list" + ] + }, + "description": "the one key" + } + ], + "description": "the kept predefined list", + "history": 5 + }, + { + "name": "config-candidates", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "pull": { + "type": "string" + }, + "head": { + "type": "string" + }, + "declares": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "kind": { + "type": "string" + }, + "default": { + "description": "the default" + }, + "why": { + "type": "string" + }, + "type": { + "type": "string" + }, + "description": { + "type": "string" + }, + "enum": { + "description": "the allowed values" + }, + "minimum": { + "type": "number" + }, + "maximum": { + "type": "number" + }, + "pattern": { + "type": "string" + }, + "trusted": { + "type": "boolean" + }, + "predefined": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "runtime": { + "type": "boolean" + } + }, + "required": [ + "kind", + "default", + "why" + ] + } + }, + "description": "module to key to declaration" + }, + "at": { + "type": "string", + "description": "when (RFC 3339 time)" + } + }, + "required": [ + "pull", + "head", + "declares" + ] + }, + "description": "the configuration a pull request declares" + } + ], + "description": "the configuration declared by open pull requests", + "history": 5 + }, + { + "name": "packages", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "module": { + "type": "string", + "description": "the Go module path" + }, + "repository": { + "type": "string" + }, + "dir": { + "type": "string" + }, + "commit": { + "type": "string" + }, + "requires": { + "type": "string", + "description": "digest of go.mod and go.sum" + }, + "packages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "package": { + "type": "string" + }, + "digest": { + "type": "string" + } + }, + "required": [ + "package", + "digest" + ] + } + }, + "at": { + "type": "string", + "description": "when (RFC 3339 time)" + } + }, + "required": [ + "module", + "repository", + "commit", + "packages" + ] + }, + "description": "a mesh Go module as its latest trunk merge published it" + }, + { + "key": ".", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "name": { + "type": "string" + }, + "package": { + "type": "string" + }, + "series": { + "type": "string" + }, + "seat": { + "type": "string" + }, + "go-module": { + "type": "string" + }, + "newest": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "commit": { + "type": "string" + }, + "digest": { + "type": "string" + }, + "fingerprint": { + "type": "string" + }, + "contents": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "module": { + "type": "string", + "description": "the Go module path" + }, + "repository": { + "type": "string" + }, + "dir": { + "type": "string" + }, + "commit": { + "type": "string" + }, + "requires": { + "type": "string", + "description": "digest of go.mod and go.sum" + }, + "packages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "package": { + "type": "string" + }, + "digest": { + "type": "string" + } + }, + "required": [ + "package", + "digest" + ] + } + }, + "at": { + "type": "string", + "description": "when (RFC 3339 time)" + } + }, + "required": [ + "module", + "repository", + "commit", + "packages" + ] + } + }, + "required": [ + "version" + ] + }, + "numbered": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "commit": { + "type": "string" + }, + "digest": { + "type": "string" + }, + "fingerprint": { + "type": "string" + }, + "contents": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "module": { + "type": "string", + "description": "the Go module path" + }, + "repository": { + "type": "string" + }, + "dir": { + "type": "string" + }, + "commit": { + "type": "string" + }, + "requires": { + "type": "string", + "description": "digest of go.mod and go.sum" + }, + "packages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "package": { + "type": "string" + }, + "digest": { + "type": "string" + } + }, + "required": [ + "package", + "digest" + ] + } + }, + "at": { + "type": "string", + "description": "when (RFC 3339 time)" + } + }, + "required": [ + "module", + "repository", + "commit", + "packages" + ] + } + }, + "required": [ + "version" + ] + }, + "at": { + "type": "string", + "description": "when (RFC 3339 time)" + } + }, + "required": [ + "name", + "series" + ] + }, + "description": "a published package series, numbered and newest" + } + ], + "description": "the published packages", + "history": 5 + } + ] +} diff --git a/contracts/seats/mesh-delivery-checker/fixtures/events.json b/contracts/seats/mesh-delivery-checker/fixtures/events.json new file mode 100644 index 00000000..7df46f68 --- /dev/null +++ b/contracts/seats/mesh-delivery-checker/fixtures/events.json @@ -0,0 +1,44 @@ +[ + { + "name": "verdict on the checker's own family", + "of": "holder-event:verdict", + "subject": "mesh.stream.mesh-delivery-checker.event.verdict", + "given": { + "repository": "novox/mesh-contracts", + "pull": 3, + "head": "cccccccccccccccccccccccccccccccccccccccc", + "verdict": "fail", + "out-of-step": [ + "go/seats/meshbroker/contract.go" + ], + "series": [ + { + "module": "mesh-contracts", + "artifact": "go", + "from": "0.1", + "to": "0.2" + } + ] + } + }, + { + "name": "tag-wanted on the checker's own family", + "of": "holder-event:tag-wanted", + "subject": "mesh.stream.mesh-delivery-checker.event.tag-wanted", + "given": { + "repository": "novox/mesh-sdk", + "pull": 170, + "head": "cccccccccccccccccccccccccccccccccccccccc", + "branch": "feat/registry", + "module": "mesh-sdk", + "package": "go", + "tag": "registry-1966" + } + }, + { + "name": "preview.written on the checker's stream", + "of": "holder-event:preview.written", + "subject": "mesh.stream.mesh-delivery-checker.event.preview.written", + "given": {} + } +] diff --git a/contracts/seats/mesh-delivery-checker/fixtures/states.json b/contracts/seats/mesh-delivery-checker/fixtures/states.json new file mode 100644 index 00000000..ead92035 --- /dev/null +++ b/contracts/seats/mesh-delivery-checker/fixtures/states.json @@ -0,0 +1,48 @@ +[ + { + "name": "a running verdict", + "of": "state:verdicts/.", + "given": { + "v": 1, + "group": "feat/registry", + "heads": "0123456789ab", + "state": "running" + } + }, + { + "name": "a kept pass", + "of": "state:kept/.", + "given": { + "v": 1, + "state": "pass", + "repository": "novox/mesh-sdk", + "package": "go/keys", + "level": "unit", + "tests": 4, + "took": 120, + "toolchain": "go1.26", + "at": "2026-11-05T10:00:00Z" + } + }, + { + "name": "the status owed for a head", + "of": "state:owed/...", + "given": { + "v": 1, + "act": "status=mesh/check", + "context": "mesh/check", + "state": "success" + } + }, + { + "name": "a preview", + "of": "state:previews/..", + "given": { + "v": 1, + "repository": "novox/mesh-sdk", + "head": "cccccccccccccccccccccccccccccccccccccccc", + "trunk": "dddddddddddddddddddddddddddddddddddddddd", + "merge": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + } + } +] diff --git a/contracts/seats/mesh-delivery-checker/seat.json b/contracts/seats/mesh-delivery-checker/seat.json new file mode 100644 index 00000000..2aaba6bf --- /dev/null +++ b/contracts/seats/mesh-delivery-checker/seat.json @@ -0,0 +1,635 @@ +{ + "format": 1, + "seat": "mesh-delivery-checker", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0547", + "verbs": [ + { + "name": "try", + "description": "Try a pushed head before its pull request is opened: the checker judges it as it judges a pull request's head, merged onto its trunk's head, its unit and integration levels through the build modules, and owes the forge nothing. Answers the try's id at once; try-read reads where it stands. A try of the same head that waits or runs is the answer to a second ask; at most 4 wait or run at once, mesh-wide.", + "optional": true, + "replaces": [ + "sh merge-check.sh", + "docker run postgres" + ], + "input": { + "properties": { + "head": { + "description": "the full commit to try, 40 or 64 hex digits", + "type": "string" + }, + "level": { + "description": "unit or integration: only this level; both when absent", + "type": "string" + }, + "repository": { + "description": "/", + "type": "string" + }, + "tests": { + "description": "a go test -run pattern: only those tests run, and nothing is kept", + "type": "string" + }, + "trunk": { + "description": "the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent", + "type": "string" + } + }, + "required": [ + "repository", + "head" + ], + "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "the try's id" + }, + "state": { + "type": "string", + "description": "its state" + } + }, + "required": [ + "id" + ] + } + }, + { + "name": "try-read", + "description": "Where a try stands, as stored: queued, running, or its end (pass, fail, or error when it could not run), with what was said of it, the head and the trunk commit it was merged onto, the time it took and what failed first. Read-only.", + "optional": true, + "input": { + "properties": { + "id": { + "description": "the try's id, try-", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the try: its state, its steps and its verdict when it has one" + } + }, + { + "name": "try-end", + "description": "End a try that waits or runs, as error, by hand: say why (kept in what it says). Its place among the tries is free at once and the next try of its head asks anew with a new id. A try that ended already is refused.", + "optional": true, + "input": { + "properties": { + "id": { + "description": "the try's id, try-", + "type": "string" + }, + "why": { + "description": "why it is ended, in words", + "type": "string" + } + }, + "required": [ + "id", + "why" + ], + "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "the try" + }, + "ended": { + "type": "boolean", + "description": "it is ended" + } + }, + "required": [ + "id", + "ended" + ] + } + } + ], + "description": "The merge check's tries: start one, read one, end one. The checker's own events stay under its own subjects (ADR 0620 §2).", + "holder-events": [ + { + "name": "verdict", + "subject": "mesh.stream..event.verdict", + "family": "checker.asks", + "body": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "description": "owner/repo" + }, + "pull": { + "type": "integer", + "description": "the pull request's number, for a pull request's check" + }, + "head": { + "type": "string", + "description": "the commit checked" + }, + "verdict": { + "type": "string", + "description": "pass or fail" + }, + "out-of-step": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the generated files out of step with their seat definitions, each path (design 26)" + }, + "series": { + "type": "array", + "items": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module whose artifact's series changes" + }, + "artifact": { + "type": "string", + "description": "its artifact, or the package's ecosystem name as the module's packages name it" + }, + "from": { + "type": "string", + "description": "the series on trunk" + }, + "to": { + "type": "string", + "description": "the series the checked head gives it" + } + }, + "required": [ + "module", + "artifact", + "from", + "to" + ] + }, + "description": "each change of an artifact's series the checked head makes against trunk's manifests, once each (design 32); absent when it changes none" + } + }, + "required": [ + "repository", + "head", + "verdict" + ] + } + }, + { + "name": "tag-wanted", + "subject": "mesh.stream..event.tag-wanted", + "family": "checker.asks", + "body": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "description": "owner/repo" + }, + "pull": { + "type": "integer", + "description": "the pull request" + }, + "head": { + "type": "string", + "description": "its head" + }, + "branch": { + "type": "string", + "description": "the pull request's branch: its tag is removed when the branch ends" + }, + "module": { + "type": "string", + "description": "the group member" + }, + "package": { + "type": "string", + "description": "the package it declares" + }, + "tag": { + "type": "string", + "description": "the tag wanted" + } + }, + "required": [ + "repository", + "head", + "branch", + "module", + "package" + ] + } + }, + { + "name": "preview.written", + "subject": "mesh.stream..event.preview.written", + "family": "checks" + } + ], + "states": [ + { + "name": "verdicts", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "group": { + "type": "string" + }, + "heads": { + "type": "string", + "description": "12 hex digits" + }, + "trunk": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "repository to the trunk head merged onto" + }, + "state": { + "type": "string", + "description": "running, pass or fail" + }, + "failed": { + "type": "object", + "properties": { + "test": { + "type": "string" + }, + "machine": { + "type": "string" + }, + "words": { + "type": "string" + } + } + }, + "failures": { + "type": "array", + "items": { + "type": "object", + "properties": { + "test": { + "type": "string" + }, + "machine": { + "type": "string" + }, + "words": { + "type": "string" + } + } + } + }, + "levels": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "run": { + "type": "integer" + }, + "kept": { + "type": "integer" + }, + "skipped": { + "type": "integer" + } + }, + "required": [ + "run", + "kept" + ] + }, + "description": "by level, unit or integration" + }, + "took": { + "type": "object", + "properties": { + "queue": { + "type": "integer" + }, + "clone": { + "type": "integer" + }, + "unit": { + "type": "integer" + }, + "lint": { + "type": "integer" + }, + "integration-setup": { + "type": "integer" + }, + "integration-run": { + "type": "integer" + }, + "composition": { + "type": "integer" + } + }, + "description": "milliseconds" + }, + "machines": { + "type": "array", + "items": { + "type": "string" + } + }, + "sdk": { + "type": "string" + }, + "composition": { + "type": "object", + "properties": { + "state": { + "type": "string", + "description": "not-run, pass or fail" + }, + "words": { + "type": "string" + }, + "sdk": { + "type": "string" + }, + "machines": { + "type": "array", + "items": { + "type": "string" + } + }, + "fpset": { + "type": "string" + } + }, + "required": [ + "state" + ] + }, + "over-bound": { + "type": "array", + "items": { + "type": "string" + } + }, + "beside": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "carried-from": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "series": { + "type": "array", + "items": { + "type": "object", + "properties": { + "module": { + "type": "string" + }, + "artifact": { + "type": "string" + }, + "from": { + "type": "string" + }, + "to": { + "type": "string" + } + }, + "required": [ + "module", + "artifact", + "from", + "to" + ] + } + } + }, + "required": [ + "group", + "heads", + "state" + ], + "description": "one check of a group at one set of heads" + }, + "description": "one check of a group; the group is an escaped branch name, heads 12 hex digits" + } + ], + "description": "the checker's verdict of each group at each set of heads", + "history": 5 + }, + { + "name": "kept", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "state": { + "type": "string", + "description": "pass: a failure is never kept" + }, + "repository": { + "type": "string" + }, + "package": { + "type": "string" + }, + "level": { + "type": "string" + }, + "tests": { + "type": "integer" + }, + "took": { + "type": "integer" + }, + "toolchain": { + "type": "string" + }, + "at": { + "type": "string", + "description": "when (RFC 3339 time)" + } + }, + "required": [ + "state", + "repository", + "package", + "level" + ], + "description": "one passing level of one package, kept so the next check does not run it again" + }, + "description": "a passing level of one package by its content digest" + } + ], + "description": "the kept passing levels", + "history": 1 + }, + { + "name": "owed", + "template": "state", + "key-forms": [ + { + "key": "...", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "act": { + "type": "string", + "description": "status=, view, note or comment." + }, + "context": { + "type": "string" + }, + "target": { + "type": "string" + }, + "state": { + "type": "string" + }, + "text": { + "type": "string" + }, + "ref": { + "type": "string" + } + }, + "required": [ + "act" + ], + "description": "one act owed to the forge, written by its owner and done by the forge bridge" + }, + "description": "the status mesh/check the checker owes the forge for a head (its act is status=mesh/check)" + } + ], + "description": "the status the checker owes the forge", + "history": 5 + }, + { + "name": "previews", + "template": "state", + "key-forms": [ + { + "key": "..", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "repository": { + "type": "string" + }, + "head": { + "type": "string" + }, + "trunk": { + "type": "string" + }, + "merge": { + "type": "string", + "description": "the merge commit" + }, + "conflicts": { + "type": "array", + "items": { + "type": "string" + }, + "description": "files in conflict" + }, + "modules": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "tree": { + "type": "string" + }, + "manifest": { + "type": "string", + "description": "manifest digest" + }, + "locks": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "paths": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "packages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "tree", + "manifest" + ] + }, + "description": "by directory" + } + }, + "required": [ + "repository", + "head", + "trunk" + ], + "description": "the merge of a head onto a trunk commit, read before a pull request is checked" + }, + "description": "the merge of a head onto a trunk commit" + } + ], + "description": "the previews of a head merged onto a trunk", + "history": 5 + } + ] +} diff --git a/contracts/seats/mesh-delivery/fixtures/states.json b/contracts/seats/mesh-delivery/fixtures/states.json new file mode 100644 index 00000000..0721e1da --- /dev/null +++ b/contracts/seats/mesh-delivery/fixtures/states.json @@ -0,0 +1,72 @@ +[ + { + "name": "the source revision", + "of": "state:source/main", + "given": { + "v": 1, + "group": "feat/registry", + "heads": { + "novox/mesh-sdk": "cccccccccccccccccccccccccccccccccccccccc" + } + } + }, + { + "name": "a merge", + "of": "state:merges/", + "given": { + "v": 1, + "group": "feat/registry", + "ask": "merge.feat=registry.0123456789ab", + "pulls": [ + { + "repository": "novox/mesh-sdk", + "number": 1, + "head": "cccccccccccccccccccccccccccccccccccccccc", + "state": "merging" + } + ] + } + }, + { + "name": "an ask", + "of": "state:asks/merge..", + "given": { + "v": 1, + "group": "feat/registry", + "heads": [ + { + "repository": "novox/mesh-sdk", + "number": 1, + "head": "cccccccccccccccccccccccccccccccccccccccc" + } + ], + "ask": { + "headline": "merge" + } + } + }, + { + "name": "an act owed", + "of": "state:owed/...", + "given": { + "v": 1, + "act": "view" + } + }, + { + "name": "a turn", + "of": "state:turns/", + "given": { + "v": 1, + "repository": "novox/mesh-sdk", + "waiting": [ + { + "group": "feat/registry", + "heads": "0123456789ab", + "approved": "2026-11-05T10:00:00Z", + "since": "2026-11-05T10:01:00Z" + } + ] + } + } +] diff --git a/contracts/seats/mesh-delivery/seat.json b/contracts/seats/mesh-delivery/seat.json new file mode 100644 index 00000000..bc08d0ff --- /dev/null +++ b/contracts/seats/mesh-delivery/seat.json @@ -0,0 +1,570 @@ +{ + "format": 1, + "seat": "mesh-delivery", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0239", + "verbs": [ + { + "name": "deliveries", + "description": "Every delivery not final, and those that ended in the last day, one line each: its id (owner/repository@commit), its state, its group, what it waits for and since when. Narrowed by state, repository or group.", + "input": { + "properties": { + "all": { + "description": "\"true\": the final ones of the last thirty days too", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "group": { + "description": "a group's id (its branch name)", + "type": "string" + }, + "repository": { + "description": "owner/repository", + "type": "string" + }, + "state": { + "description": "one state, e.g. delivering or held", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "what deliveries answers, as its description says" + } + }, + { + "name": "times", + "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the delivery view (nox-delivery): each machine's want and have, and since when", + "optional": true, + "input": { + "properties": {}, + "type": "object" + }, + "answer": { + "type": "object", + "description": "what times answers, as its description says" + } + }, + { + "name": "show", + "description": "One delivery or group whole: its delivery plan (what it builds, what each machine receives, what is not an ordinary send), every transition with when and why, the machine steps of its walk, its group and its order.", + "input": { + "properties": { + "id": { + "description": "a delivery's id, or a group's", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "what show answers, as its description says" + } + }, + { + "name": "groups", + "description": "Every delivery group: its members in order, why each pair is ordered (declared, built by, version skew, engine before controller, by name), its composed check and its state, derived from its members.", + "input": { + "properties": { + "all": { + "description": "\"true\": the groups that ended too", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "what groups answers, as its description says" + } + }, + { + "name": "what-if", + "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the merge check's composition, which composes every machine a change touches", + "optional": true, + "input": { + "properties": {}, + "type": "object" + }, + "answer": { + "type": "object", + "description": "what what-if answers, as its description says" + } + }, + { + "name": "checks", + "description": "What the mesh's checks said of a pull request's head or of one commit: each of the commit's mesh statuses (mesh/check, the one a main requires from R4e, hq's mesh/repo-check, mesh/delivery, …) with its state, description and when it was set; the merge check's full verdict as the controller said it — each layer's summary, the machine that ran it, when, its build id and its report; and whether the branch's protection would let it merge, every required status being success.", + "optional": true, + "input": { + "properties": { + "commit": { + "description": "a commit's sha, or the start of one, instead of a pull request", + "type": "string" + }, + "number": { + "description": "a pull request's number: its head is read", + "type": "string" + }, + "repository": { + "description": "owner/repository", + "type": "string" + } + }, + "required": [ + "repository" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "what checks answers, as its description says" + } + }, + { + "name": "table", + "description": "The state table every delivery runs by: each transition with its guard, each state's bound and what healer H2 may do once it has passed; and the machine steps' table.", + "input": { + "properties": {}, + "type": "object" + }, + "answer": { + "type": "object", + "description": "what table answers, as its description says" + } + }, + { + "name": "stalled", + "description": "Every delivery held past its state's bound, with the transition the table lets healer H2 take for it, or none.", + "input": { + "properties": {}, + "type": "object" + }, + "answer": { + "type": "object", + "description": "what stalled answers, as its description says" + } + }, + { + "name": "recheck", + "description": "Check a rejected or ready delivery again: it goes back to proposed and the controller is asked for its check. With why.", + "input": { + "properties": { + "id": { + "description": "the delivery's id", + "type": "string" + }, + "why": { + "description": "why, kept with the transition", + "type": "string" + } + }, + "required": [ + "id", + "why" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "what recheck answers, as its description says" + } + }, + { + "name": "release", + "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the assigner's asks (hold, restore, backup, record), answered on the phone or in the delivery view", + "optional": true, + "input": { + "properties": {}, + "type": "object" + }, + "answer": { + "type": "object", + "description": "what release answers, as its description says" + } + }, + { + "name": "stop", + "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: a bad fingerprint is held and its canary set back by the assigner (ADR 0429 §6, ADR 0539 clause 18)", + "optional": true, + "input": { + "properties": {}, + "type": "object" + }, + "answer": { + "type": "object", + "description": "what stop answers, as its description says" + } + }, + { + "name": "close", + "description": "Healer H2's verb: take the one transition the table names for a delivery held past its bound. Refused for any other.", + "input": { + "properties": { + "id": { + "description": "the delivery's id", + "type": "string" + }, + "why": { + "description": "the condition it answers", + "type": "string" + } + }, + "required": [ + "id", + "why" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "what close answers, as its description says" + } + }, + { + "name": "retire-history", + "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: nothing to retire: the gate keeps no delivery after its merge", + "optional": true, + "input": { + "properties": {}, + "type": "object" + }, + "answer": { + "type": "object", + "description": "what retire-history answers, as its description says" + } + }, + { + "name": "settle", + "description": "Ask the operator to settle deliveries held because their merge reached the trunk without a passing check: one ask on the operator's channel for all of them, as an Approve is asked. The verb moves nothing; on the operator's answer each goes to merged, and whether it is on the machines is derived as for any merge (novox/hq ADR 0556). Refused for a delivery not held, or held for another reason, naming why.", + "optional": true, + "input": { + "additionalProperties": false, + "properties": { + "ids": { + "description": "the held deliveries' ids (owner/repository@commit)", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "why": { + "description": "why they may be settled, shown to the operator", + "type": "string" + } + }, + "required": [ + "ids", + "why" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "what settle answers, as its description says" + } + } + ], + "description": "The delivery gate: reads and steers deliveries, merges and the checks that decide them; the merge is made on the operator's Approve once both checks passed.", + "states": [ + { + "name": "source", + "template": "state", + "key-forms": [ + { + "key": "main", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "group": { + "type": "string", + "description": "the group that moved it" + }, + "heads": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "repository to the merged head, a full commit" + } + }, + "required": [ + "group", + "heads" + ], + "description": "every repository's merged head; its revision is the mesh's source revision" + }, + "description": "the one key, written by the holder alone by compare-and-set" + } + ], + "description": "the mesh's source revision", + "history": 64 + }, + { + "name": "merges", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "group": { + "type": "string" + }, + "ask": { + "type": "string", + "description": "the ask key" + }, + "approved": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "@ to the approved head" + }, + "pulls": { + "type": "array", + "items": { + "type": "object", + "properties": { + "repository": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "head": { + "type": "string" + }, + "state": { + "type": "string", + "description": "merging, merged or refused" + }, + "commit": { + "type": "string" + }, + "words": { + "type": "string" + } + }, + "required": [ + "repository", + "number", + "head", + "state" + ] + } + } + }, + "required": [ + "group", + "ask", + "pulls" + ], + "description": "one group's merge and how far each pull request is" + }, + "description": "a group's merge" + } + ], + "description": "the merges", + "history": 10 + }, + { + "name": "asks", + "template": "state", + "key-forms": [ + { + "key": "merge..", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "group": { + "type": "string" + }, + "heads": { + "type": "array", + "items": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "description": "/" + }, + "number": { + "type": "integer" + }, + "head": { + "type": "string", + "description": "the full commit" + } + }, + "required": [ + "repository", + "number", + "head" + ] + } + }, + "ask": { + "description": "the operator-channel ask as raised" + }, + "asking": { + "type": "integer", + "description": "the number of times it was asked" + } + }, + "required": [ + "group", + "heads", + "ask" + ] + }, + "description": "the ask of the operator for one group at one set of heads" + } + ], + "description": "the asks of the operator for a merge", + "history": 5 + }, + { + "name": "owed", + "template": "state", + "key-forms": [ + { + "key": "...", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "act": { + "type": "string", + "description": "status=, view, note or comment." + }, + "context": { + "type": "string" + }, + "target": { + "type": "string" + }, + "state": { + "type": "string" + }, + "text": { + "type": "string" + }, + "ref": { + "type": "string" + } + }, + "required": [ + "act" + ], + "description": "one act owed to the forge, written by its owner and done by the forge bridge" + }, + "description": "one act owed to the forge" + } + ], + "description": "the acts the gate owes the forge", + "history": 5 + }, + { + "name": "turns", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout version" + }, + "repository": { + "type": "string" + }, + "waiting": { + "type": "array", + "items": { + "type": "object", + "properties": { + "group": { + "type": "string" + }, + "heads": { + "type": "string" + }, + "ask": { + "type": "string" + }, + "approved": { + "type": "string", + "description": "when (RFC 3339 time)" + }, + "since": { + "type": "string", + "description": "when (RFC 3339 time)" + }, + "waits-for": { + "type": "string" + }, + "waits-since": { + "type": "string", + "description": "when (RFC 3339 time)" + }, + "step": { + "type": "string" + } + }, + "required": [ + "group", + "heads", + "approved", + "since" + ] + } + } + }, + "required": [ + "repository", + "waiting" + ], + "description": "the merges of a repository in the order they wait for their turn" + }, + "description": "the merges waiting on a repository" + } + ], + "description": "which merge waits for which", + "history": 5 + } + ] +} diff --git a/contracts/seats/node-build-go/fixtures/states.json b/contracts/seats/node-build-go/fixtures/states.json new file mode 100644 index 00000000..1988c3d4 --- /dev/null +++ b/contracts/seats/node-build-go/fixtures/states.json @@ -0,0 +1,63 @@ +[ + { + "name": "a running step", + "of": "state:steps/.", + "given": { + "v": 1, + "id": "a1b2", + "tool": "build", + "node": "ace", + "state": "running", + "started": "2026-11-05T10:00:00Z" + } + }, + { + "name": "a paused node", + "of": "state:pauses/paused.", + "given": { + "v": 1, + "paused": true, + "by": "operator@shanks", + "at": "2026-11-05T10:00:00Z" + } + }, + { + "name": "a node's capacity", + "of": "state:pauses/capacity.", + "given": { + "v": 1, + "effective": 2, + "configured": 2, + "why": "the configured steps fit the cores" + } + }, + { + "name": "a node's toolchain", + "of": "state:toolchains/.", + "given": { + "v": 1, + "language": "go", + "version": "1.26.0", + "recipe": "1" + } + }, + { + "name": "a build ask", + "of": "accept:delivery", + "subject": "mesh.seat.node-build-go.accept.delivery", + "given": { + "id": "a1b2", + "kind": "build", + "language": "go", + "asker": "mesh-delivery-builder", + "build": { + "module": "nats", + "artifacts": [ + { + "artifact": "go" + } + ] + } + } + } +] diff --git a/contracts/seats/node-build-go/seat.json b/contracts/seats/node-build-go/seat.json new file mode 100644 index 00000000..ff273bdb --- /dev/null +++ b/contracts/seats/node-build-go/seat.json @@ -0,0 +1,87 @@ +{ + "format": 1, + "seat": "node-build-go", + "scope": "node", + "grain": "whole", + "decision": "novox/hq ADR 0525, ADR 0536, ADR 0553", + "pulled-by-kind": true, + "yields": true, + "accepts": [ + { + "name": "delivery", + "body": { + "$ref": "#/defs/StepAsk" + } + }, + { + "name": "check", + "body": { + "$ref": "#/defs/StepAsk" + } + }, + { + "name": "recheck", + "body": { + "$ref": "#/defs/StepAsk" + } + } + ], + "description": "The Go build module on a node: it takes the asks of the builder and the checker, runs them as steps and answers.", + "includes": [ + "build-step" + ], + "states": [ + { + "name": "steps", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "$ref": "#/defs/Step" + }, + "description": "a step on a node" + } + ], + "description": "the steps this module runs", + "history": 5 + }, + { + "name": "pauses", + "template": "state", + "key-forms": [ + { + "key": "paused.", + "value": { + "$ref": "#/defs/Pause" + }, + "description": "whether a node is paused" + }, + { + "key": "capacity.", + "value": { + "$ref": "#/defs/Capacity" + }, + "description": "a node's capacity" + } + ], + "description": "the pause and the capacity of each node", + "history": 1 + }, + { + "name": "toolchains", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "$ref": "#/defs/Toolchain" + }, + "description": "the toolchain a node reports" + } + ], + "description": "the toolchains the nodes report", + "history": 1 + } + ] +} diff --git a/contracts/seats/node-build-image/fixtures/states.json b/contracts/seats/node-build-image/fixtures/states.json new file mode 100644 index 00000000..b49e8c53 --- /dev/null +++ b/contracts/seats/node-build-image/fixtures/states.json @@ -0,0 +1,63 @@ +[ + { + "name": "a running step", + "of": "state:steps/.", + "given": { + "v": 1, + "id": "a1b2", + "tool": "build", + "node": "ace", + "state": "running", + "started": "2026-11-05T10:00:00Z" + } + }, + { + "name": "a paused node", + "of": "state:pauses/paused.", + "given": { + "v": 1, + "paused": true, + "by": "operator@shanks", + "at": "2026-11-05T10:00:00Z" + } + }, + { + "name": "a node's capacity", + "of": "state:pauses/capacity.", + "given": { + "v": 1, + "effective": 2, + "configured": 2, + "why": "the configured steps fit the cores" + } + }, + { + "name": "a node's toolchain", + "of": "state:toolchains/.", + "given": { + "v": 1, + "language": "go", + "version": "1.26.0", + "recipe": "1" + } + }, + { + "name": "a build ask", + "of": "accept:delivery", + "subject": "mesh.seat.node-build-image.accept.delivery", + "given": { + "id": "a1b2", + "kind": "build", + "language": "go", + "asker": "mesh-delivery-builder", + "build": { + "module": "nats", + "artifacts": [ + { + "artifact": "go" + } + ] + } + } + } +] diff --git a/contracts/seats/node-build-image/seat.json b/contracts/seats/node-build-image/seat.json new file mode 100644 index 00000000..f0ca605b --- /dev/null +++ b/contracts/seats/node-build-image/seat.json @@ -0,0 +1,87 @@ +{ + "format": 1, + "seat": "node-build-image", + "scope": "node", + "grain": "whole", + "decision": "novox/hq ADR 0525, ADR 0536, ADR 0553", + "pulled-by-kind": true, + "yields": true, + "accepts": [ + { + "name": "delivery", + "body": { + "$ref": "#/defs/StepAsk" + } + }, + { + "name": "check", + "body": { + "$ref": "#/defs/StepAsk" + } + }, + { + "name": "recheck", + "body": { + "$ref": "#/defs/StepAsk" + } + } + ], + "description": "The image build module on a node: it takes the asks of the builder and the checker, runs them as steps and answers.", + "includes": [ + "build-step" + ], + "states": [ + { + "name": "steps", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "$ref": "#/defs/Step" + }, + "description": "a step on a node" + } + ], + "description": "the steps this module runs", + "history": 5 + }, + { + "name": "pauses", + "template": "state", + "key-forms": [ + { + "key": "paused.", + "value": { + "$ref": "#/defs/Pause" + }, + "description": "whether a node is paused" + }, + { + "key": "capacity.", + "value": { + "$ref": "#/defs/Capacity" + }, + "description": "a node's capacity" + } + ], + "description": "the pause and the capacity of each node", + "history": 1 + }, + { + "name": "toolchains", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "$ref": "#/defs/Toolchain" + }, + "description": "the toolchain a node reports" + } + ], + "description": "the toolchains the nodes report", + "history": 1 + } + ] +} diff --git a/contracts/seats/node-build-node/fixtures/states.json b/contracts/seats/node-build-node/fixtures/states.json new file mode 100644 index 00000000..9a80337a --- /dev/null +++ b/contracts/seats/node-build-node/fixtures/states.json @@ -0,0 +1,63 @@ +[ + { + "name": "a running step", + "of": "state:steps/.", + "given": { + "v": 1, + "id": "a1b2", + "tool": "build", + "node": "ace", + "state": "running", + "started": "2026-11-05T10:00:00Z" + } + }, + { + "name": "a paused node", + "of": "state:pauses/paused.", + "given": { + "v": 1, + "paused": true, + "by": "operator@shanks", + "at": "2026-11-05T10:00:00Z" + } + }, + { + "name": "a node's capacity", + "of": "state:pauses/capacity.", + "given": { + "v": 1, + "effective": 2, + "configured": 2, + "why": "the configured steps fit the cores" + } + }, + { + "name": "a node's toolchain", + "of": "state:toolchains/.", + "given": { + "v": 1, + "language": "go", + "version": "1.26.0", + "recipe": "1" + } + }, + { + "name": "a build ask", + "of": "accept:delivery", + "subject": "mesh.seat.node-build-node.accept.delivery", + "given": { + "id": "a1b2", + "kind": "build", + "language": "go", + "asker": "mesh-delivery-builder", + "build": { + "module": "nats", + "artifacts": [ + { + "artifact": "go" + } + ] + } + } + } +] diff --git a/contracts/seats/node-build-node/seat.json b/contracts/seats/node-build-node/seat.json new file mode 100644 index 00000000..34ee7310 --- /dev/null +++ b/contracts/seats/node-build-node/seat.json @@ -0,0 +1,87 @@ +{ + "format": 1, + "seat": "node-build-node", + "scope": "node", + "grain": "whole", + "decision": "novox/hq ADR 0525, ADR 0536, ADR 0553", + "pulled-by-kind": true, + "yields": true, + "accepts": [ + { + "name": "delivery", + "body": { + "$ref": "#/defs/StepAsk" + } + }, + { + "name": "check", + "body": { + "$ref": "#/defs/StepAsk" + } + }, + { + "name": "recheck", + "body": { + "$ref": "#/defs/StepAsk" + } + } + ], + "description": "The Node build module on a node: it takes the asks of the builder and the checker, runs them as steps and answers.", + "includes": [ + "build-step" + ], + "states": [ + { + "name": "steps", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "$ref": "#/defs/Step" + }, + "description": "a step on a node" + } + ], + "description": "the steps this module runs", + "history": 5 + }, + { + "name": "pauses", + "template": "state", + "key-forms": [ + { + "key": "paused.", + "value": { + "$ref": "#/defs/Pause" + }, + "description": "whether a node is paused" + }, + { + "key": "capacity.", + "value": { + "$ref": "#/defs/Capacity" + }, + "description": "a node's capacity" + } + ], + "description": "the pause and the capacity of each node", + "history": 1 + }, + { + "name": "toolchains", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "$ref": "#/defs/Toolchain" + }, + "description": "the toolchain a node reports" + } + ], + "description": "the toolchains the nodes report", + "history": 1 + } + ] +} diff --git a/contracts/seats/npm-package-registry/fixtures/events.json b/contracts/seats/npm-package-registry/fixtures/events.json new file mode 100644 index 00000000..4cadc302 --- /dev/null +++ b/contracts/seats/npm-package-registry/fixtures/events.json @@ -0,0 +1,72 @@ +[ + { + "name": "published on the seat's subject", + "of": "event:published", + "subject": "mesh.seat.npm-package-registry.event.published", + "given": { + "name": "@novox/mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "gitea", + "transfer": "t1", + "reference": "@novox/mesh-sdk@0.1.19" + } + }, + { + "name": "refused on the seat's subject", + "of": "event:refused", + "subject": "mesh.seat.npm-package-registry.event.refused", + "given": { + "name": "@novox/mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "gitea", + "transfer": "t1", + "reference": "@novox/mesh-sdk@0.1.19", + "reason": "retention" + } + }, + { + "name": "removed on the seat's subject", + "of": "event:removed", + "subject": "mesh.seat.npm-package-registry.event.removed", + "given": { + "name": "@novox/mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "gitea", + "transfer": "t1", + "reference": "@novox/mesh-sdk@0.1.19", + "reason": "retention" + } + }, + { + "name": "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", + "of": "event:kept", + "subject": "mesh.seat.npm-package-registry.event.kept", + "given": { + "name": "@novox/mesh-sdk", + "version": "0.1.19-feat-x.2", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "gitea", + "reference": "@novox/mesh-sdk@0.1.19", + "reason": "shared-digest", + "tag": "0.1.19-feat-x.2", + "tags": [ + "0.1.19" + ] + } + } +] diff --git a/contracts/seats/npm-package-registry/fixtures/verbs.json b/contracts/seats/npm-package-registry/fixtures/verbs.json new file mode 100644 index 00000000..0b88f08e --- /dev/null +++ b/contracts/seats/npm-package-registry/fixtures/verbs.json @@ -0,0 +1,110 @@ +[ + { + "name": "publish opens a transfer", + "of": "verb:publish.input", + "given": { + "name": "@novox/mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ] + } + }, + { + "name": "a field the reader does not know is dropped", + "of": "verb:publish.input", + "why": "ADR 0608 §6: a reader ignores a field it does not know", + "given": { + "name": "@novox/mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ], + "later": { + "x": 1 + } + }, + "wire": { + "name": "@novox/mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ] + } + }, + { + "name": "publish answers what it lacks", + "of": "verb:publish.answer", + "given": { + "transfer": "t1", + "lacks": [ + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ] + } + }, + { + "name": "a chunk of a part", + "of": "verb:publish-part.input", + "given": { + "transfer": "t1", + "part": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "sequence": 0, + "last": true, + "data": "aGVsbG8=" + } + }, + { + "name": "the versions held", + "of": "verb:versions.answer", + "given": { + "name": "@novox/mesh-sdk", + "versions": [ + { + "name": "@novox/mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc" + } + ] + } + }, + { + "name": "a tagged version sharing its digest with another tag is kept, not removed", + "of": "verb:remove.answer", + "why": "G-sdk-contracts 3: the store cannot remove the tag alone, so the answer says what holds", + "given": { + "removed": false, + "kept": true, + "reason": "shared-digest", + "tags": [ + "0.1.19" + ] + } + } +] diff --git a/contracts/seats/npm-package-registry/seat.json b/contracts/seats/npm-package-registry/seat.json new file mode 100644 index 00000000..61f77c32 --- /dev/null +++ b/contracts/seats/npm-package-registry/seat.json @@ -0,0 +1,34 @@ +{ + "format": 1, + "seat": "npm-package-registry", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0109", + "description": "The mesh's npm packages: the npm registry protocol's read side, and publishing over the bus.", + "delivers": "npm-package-registry", + "includes": [ + "registry" + ], + "family": { + "name": "npm-package-registry.events", + "max-age-days": 7 + }, + "provisions": [ + { + "name": "npm-package-registry", + "form": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "the read side's address" + } + }, + "required": [ + "url" + ], + "description": "form none: the npm registry's read side, answered anonymously, with no credential (design 51)" + } + } + ] +} diff --git a/contracts/seats/oci-artifact-registry/fixtures/events.json b/contracts/seats/oci-artifact-registry/fixtures/events.json new file mode 100644 index 00000000..e0a8cde3 --- /dev/null +++ b/contracts/seats/oci-artifact-registry/fixtures/events.json @@ -0,0 +1,98 @@ +[ + { + "name": "published on the seat's subject", + "of": "event:published", + "subject": "mesh.seat.oci-artifact-registry.event.published", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "bundle", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "distribution", + "transfer": "t1", + "reference": "oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + }, + { + "name": "refused on the seat's subject", + "of": "event:refused", + "subject": "mesh.seat.oci-artifact-registry.event.refused", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "bundle", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "distribution", + "transfer": "t1", + "reference": "oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "reason": "retention" + } + }, + { + "name": "removed on the seat's subject", + "of": "event:removed", + "subject": "mesh.seat.oci-artifact-registry.event.removed", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "bundle", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "distribution", + "transfer": "t1", + "reference": "oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "reason": "retention" + } + }, + { + "name": "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", + "of": "event:kept", + "subject": "mesh.seat.oci-artifact-registry.event.kept", + "given": { + "name": "mesh-sdk", + "version": "0.1.19-feat-x.2", + "kind": "bundle", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "distribution", + "reference": "oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "reason": "shared-digest", + "tag": "0.1.19-feat-x.2", + "tags": [ + "0.1.19" + ] + } + }, + { + "name": "held on the seat's subject", + "of": "event:held", + "subject": "mesh.seat.oci-artifact-registry.event.held", + "given": { + "name": "mesh-sdk", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reason": "kept by the sweep's rule", + "holder": "distribution", + "by": "mesh-artifact-sweep", + "at": "2026-11-04T12:00:00Z" + } + }, + { + "name": "released on the seat's subject", + "of": "event:released", + "subject": "mesh.seat.oci-artifact-registry.event.released", + "given": { + "name": "mesh-sdk", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reason": "kept by the sweep's rule", + "holder": "distribution", + "by": "mesh-artifact-sweep", + "at": "2026-11-04T12:00:00Z" + } + } +] diff --git a/contracts/seats/oci-artifact-registry/fixtures/states.json b/contracts/seats/oci-artifact-registry/fixtures/states.json new file mode 100644 index 00000000..f67f2d5d --- /dev/null +++ b/contracts/seats/oci-artifact-registry/fixtures/states.json @@ -0,0 +1,28 @@ +[ + { + "name": "a hold", + "of": "state:holds/..", + "given": { + "seat": "oci-artifact-registry", + "name": "base", + "kind": "base", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "by": "operator@shanks", + "since": "2026-11-05T10:00:00Z" + } + }, + { + "name": "an open window", + "of": "state:windows/", + "given": { + "name": "mesh-delivery-builder", + "open": true, + "holding": [ + "ace" + ], + "since": { + "ace": "2026-11-05T10:00:00Z" + } + } + } +] diff --git a/contracts/seats/oci-artifact-registry/fixtures/verbs.json b/contracts/seats/oci-artifact-registry/fixtures/verbs.json new file mode 100644 index 00000000..039c38b6 --- /dev/null +++ b/contracts/seats/oci-artifact-registry/fixtures/verbs.json @@ -0,0 +1,138 @@ +[ + { + "name": "publish opens a transfer", + "of": "verb:publish.input", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "bundle", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ] + } + }, + { + "name": "a field the reader does not know is dropped", + "of": "verb:publish.input", + "why": "ADR 0608 §6: a reader ignores a field it does not know", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "bundle", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ], + "later": { + "x": 1 + } + }, + "wire": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "bundle", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ] + } + }, + { + "name": "publish answers what it lacks", + "of": "verb:publish.answer", + "given": { + "transfer": "t1", + "lacks": [ + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ] + } + }, + { + "name": "a chunk of a part", + "of": "verb:publish-part.input", + "given": { + "transfer": "t1", + "part": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "sequence": 0, + "last": true, + "data": "aGVsbG8=" + } + }, + { + "name": "the versions held", + "of": "verb:versions.answer", + "given": { + "name": "mesh-sdk", + "versions": [ + { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc" + } + ] + } + }, + { + "name": "a release of what is not held answers released false", + "of": "verb:release.answer", + "why": "A3t: the outcome asked holds already, so it is no error", + "given": { + "released": false + } + }, + { + "name": "a tagged version sharing its digest with another tag is kept, not removed", + "of": "verb:remove.answer", + "why": "G-sdk-contracts 3: the store cannot remove the tag alone, so the answer says what holds", + "given": { + "removed": false, + "kept": true, + "reason": "shared-digest", + "tags": [ + "0.1.19" + ] + } + }, + { + "name": "list answers an untagged archive by the blob it keeps", + "of": "verb:list.answer", + "given": { + "repositories": [ + { + "name": "mail/files", + "manifests": [ + { + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "index": false, + "artifact": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "size": 4096, + "held": true + } + ] + } + ] + } + } +] diff --git a/contracts/seats/oci-artifact-registry/seat.json b/contracts/seats/oci-artifact-registry/seat.json new file mode 100644 index 00000000..ae30e92c --- /dev/null +++ b/contracts/seats/oci-artifact-registry/seat.json @@ -0,0 +1,382 @@ +{ + "format": 1, + "seat": "oci-artifact-registry", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0605", + "description": "The mesh's bundles and archives, by digest, in the one artifact store: the OCI distribution protocol's read side.", + "includes": [ + "registry" + ], + "verbs": [ + { + "name": "hold", + "description": "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", + "callers": "the module that runs the artifact sweep alone", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the name" + }, + "digest": { + "type": "string", + "description": "the digest" + }, + "reason": { + "type": "string", + "description": "which of the sweep's rules keeps it" + } + }, + "required": [ + "name", + "digest", + "reason" + ] + }, + "answer": { + "type": "object", + "properties": { + "held": { + "type": "boolean", + "description": "it is held" + }, + "since": { + "type": "string", + "description": "when the hold began, RFC 3339" + } + }, + "required": [ + "held" + ] + }, + "optional": true + }, + { + "name": "release", + "description": "Let a hold go; emits released. A release of what is not held answers released false and emits nothing, never an error: the outcome asked holds already. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", + "callers": "the module that runs the artifact sweep; the operator's session through the holder's tool", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the name" + }, + "digest": { + "type": "string", + "description": "the digest" + }, + "reason": { + "type": "string", + "description": "why it is let go" + } + }, + "required": [ + "name", + "digest", + "reason" + ] + }, + "answer": { + "type": "object", + "properties": { + "released": { + "type": "boolean", + "description": "a hold was let go; false when there was none" + } + }, + "required": [ + "released" + ] + }, + "optional": true + }, + { + "name": "list", + "description": "What the store holds of one repository, or of every repository the seat holds when none is named: every manifest keeping one bundle or archive, each with its digest, whether it is an index (and the manifests an index names), the tags naming it, its size and whether it is held. Read from the store itself, so a manifest no tag names is listed too; it reads only (ADR 0632 §3).", + "callers": "any caller whose role grants it", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the repository, /; every repository of the seat when not given" + } + } + }, + "answer": { + "type": "object", + "properties": { + "repositories": { + "type": "array", + "items": { + "$ref": "#/defs/Repository" + }, + "description": "each repository named, in order of its name" + } + }, + "required": [ + "repositories" + ] + }, + "optional": true + } + ], + "events": [ + { + "name": "held", + "body": { + "$ref": "#/defs/Hold" + } + }, + { + "name": "released", + "body": { + "$ref": "#/defs/Hold" + } + } + ], + "family": { + "name": "oci-artifact-registry.events", + "max-age-days": 7 + }, + "defs": { + "Hold": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the name" + }, + "digest": { + "type": "string", + "description": "the digest" + }, + "reason": { + "type": "string", + "description": "why" + }, + "holder": { + "type": "string", + "description": "the module holding the seat" + }, + "by": { + "type": "string", + "description": "the caller the holder proved" + }, + "at": { + "type": "string", + "description": "when, RFC 3339" + } + }, + "required": [ + "name", + "digest", + "holder" + ], + "description": "the body of held and released" + }, + "Repository": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the repository" + }, + "manifests": { + "type": "array", + "items": { + "$ref": "#/defs/Listed" + }, + "description": "each manifest it holds, by digest" + } + }, + "required": [ + "name", + "manifests" + ] + }, + "Listed": { + "type": "object", + "properties": { + "digest": { + "type": "string", + "description": "the manifest's digest, sha256:" + }, + "index": { + "type": "boolean", + "description": "it is an index, naming a manifest per platform" + }, + "platforms": { + "type": "array", + "items": { + "type": "string" + }, + "description": "for an index, the digests of the manifests it names" + }, + "artifact": { + "type": "string", + "description": "for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the tags naming it, none for an untagged one" + }, + "size": { + "type": "integer", + "description": "its bytes: its own content and every blob it names" + }, + "held": { + "type": "boolean", + "description": "a hold keeps it from retention" + } + }, + "required": [ + "digest", + "index", + "size", + "held" + ] + } + }, + "states": [ + { + "name": "holds", + "template": "state", + "history": 1, + "description": "the bundles, archives and copied bases kept from retention's removal", + "key-forms": [ + { + "key": "..", + "value": { + "type": "object", + "description": "one hold: an untagged bundle, archive or copied base kept from retention's removal", + "properties": { + "seat": { + "type": "string" + }, + "name": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "digest": { + "type": "string" + }, + "by": { + "type": "string" + }, + "why": { + "type": "string" + }, + "since": { + "type": "string", + "description": "when (RFC 3339 time)" + } + }, + "required": [ + "seat", + "name", + "kind", + "digest", + "by", + "since" + ] + }, + "description": "one hold" + } + ] + }, + { + "name": "windows", + "template": "state", + "history": 1, + "description": "the direct-write windows, one per caller", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "description": "the direct-write window of one caller: open while a node holds it open", + "properties": { + "name": { + "type": "string", + "description": "the caller whose window it is" + }, + "open": { + "type": "boolean" + }, + "holding": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the nodes holding it open" + }, + "since": { + "type": "object", + "additionalProperties": { + "type": "string", + "description": "when (RFC 3339 time)" + }, + "description": "node to when it began to hold" + }, + "closed": { + "type": "string", + "description": "when it last closed (RFC 3339 time)" + }, + "last_holding": { + "type": "array", + "items": { + "type": "string" + } + }, + "dropped": { + "type": "array", + "items": { + "type": "object", + "properties": { + "node": { + "type": "string" + }, + "by": { + "type": "string" + }, + "why": { + "type": "string" + }, + "at": { + "type": "string", + "description": "when (RFC 3339 time)" + } + }, + "required": [ + "node" + ] + }, + "description": "retired nodes dropped by hand" + }, + "why": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "of each node holding the window, why it holds it" + } + }, + "required": [ + "name" + ] + }, + "description": "one caller's window" + } + ] + } + ] +} diff --git a/contracts/ts/seats/containerimageregistry/package-lock.json b/contracts/ts/seats/containerimageregistry/package-lock.json new file mode 100644 index 00000000..91b0e989 --- /dev/null +++ b/contracts/ts/seats/containerimageregistry/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-delivery-contracts-container-image-registry", + "packages": { + "": { + "name": "@novox/mesh-delivery-contracts-container-image-registry", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/contracts/ts/seats/containerimageregistry/package.json b/contracts/ts/seats/containerimageregistry/package.json new file mode 100644 index 00000000..0cbf3724 --- /dev/null +++ b/contracts/ts/seats/containerimageregistry/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat container-image-registry, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-delivery-contracts-container-image-registry", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/contracts/ts/seats/containerimageregistry/src/index.ts b/contracts/ts/seats/containerimageregistry/src/index.ts new file mode 100644 index 00000000..f38d9103 --- /dev/null +++ b/contracts/ts/seats/containerimageregistry/src/index.ts @@ -0,0 +1,271 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "container-image-registry", defined whole (seats/container-image-registry/seat.json): The mesh's container images, in the one artifact store: the OCI distribution protocol's read side, an image's tags and mirroring an outside image. + +import type * as registry from "./parts/registry.js"; + +export const seat = "container-image-registry"; + +export const verbs = { + publish: "publish", + publishPart: "publish-part", + versions: "versions", + remove: "remove", + mirror: "mirror", + tags: "tags", + hold: "hold", + release: "release", + list: "list", +} as const; + +export const events = { + published: "published", + refused: "refused", + removed: "removed", + kept: "kept", + held: "held", + released: "released", +} as const; + +export const family = { name: "container-image-registry.events", maxAgeDays: 7 } as const; + +export const publishedSubject = "mesh.seat.container-image-registry.event.published"; + +export const refusedSubject = "mesh.seat.container-image-registry.event.refused"; + +export const removedSubject = "mesh.seat.container-image-registry.event.removed"; + +export const keptSubject = "mesh.seat.container-image-registry.event.kept"; + +export const heldSubject = "mesh.seat.container-image-registry.event.held"; + +export const releasedSubject = "mesh.seat.container-image-registry.event.released"; + +export const states = { + holds: "holds", + windows: "windows", +} as const; + +/** The key of one value of the state holds under the form "..". */ +export function holdsBySeatNameDigestKey(seat: string, name: string, digest: string): string { + return seat + "." + name + "." + digest; +} + +/** The parts of a key of the form "..", or undefined for a key of another form. */ +export function parseHoldsBySeatNameDigestKey(key: string): { seat: string; name: string; digest: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const seat = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + const at2 = rest.indexOf("."); + if (at2 <= 0) return undefined; + const name = rest.slice(0, at2); + rest = rest.slice(at2 + 1); + if (rest === "") return undefined; + const digest = rest; + rest = ""; + if (rest !== "") return undefined; + return { seat, name, digest }; +} + +/** The key of one value of the state windows under the form "". */ +export function windowsByCallerKey(caller: string): string { + return caller; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseWindowsByCallerKey(key: string): { caller: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const caller = rest; + rest = ""; + if (rest !== "") return undefined; + return { caller }; +} + +export type Artifact = registry.Artifact; +export type KeptEvent = registry.KeptEvent; +export type Outcome = registry.Outcome; +export type Part = registry.Part; +export type PublishAnswer = registry.PublishAnswer; +export type PublishInput = registry.PublishInput; +export type PublishPartAnswer = registry.PublishPartAnswer; +export type PublishPartInput = registry.PublishPartInput; +export type PublishedEvent = registry.PublishedEvent; +export type RefusedEvent = registry.RefusedEvent; +export type RemoveAnswer = registry.RemoveAnswer; +export type RemoveInput = registry.RemoveInput; +export type RemovedEvent = registry.RemovedEvent; +export type VersionsAnswer = registry.VersionsAnswer; +export type VersionsInput = registry.VersionsInput; + +/** the body of held and released */ +export interface Hold { + /** when, RFC 3339 */ + "at"?: string; + /** the caller the holder proved */ + "by"?: string; + /** the digest */ + "digest": string; + /** the module holding the seat */ + "holder": string; + /** the name */ + "name": string; + /** why */ + "reason"?: string; +} + +export interface Listed { + /** for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by */ + "artifact"?: string; + /** the manifest's digest, sha256: */ + "digest": string; + /** a hold keeps it from retention */ + "held": boolean; + /** it is an index, naming a manifest per platform */ + "index": boolean; + /** for an index, the digests of the manifests it names */ + "platforms"?: string[]; + /** its bytes: its own content and every blob it names */ + "size": number; + /** the tags naming it, none for an untagged one */ + "tags"?: string[]; +} + +export interface Repository { + /** each manifest it holds, by digest */ + "manifests": Listed[]; + /** the repository */ + "name": string; +} + +/** is what mirror takes. */ +export interface MirrorInput { + /** its pinned digest, sha256: */ + "digest": string; + /** the name it is kept under */ + "name": string; + /** the outside image's reference */ + "source": string; +} + +/** is what mirror answers. */ +export interface MirrorAnswer { + /** the digest stored */ + "digest": string; + /** the reference the seat answers */ + "reference": string; +} + +/** is what tags takes. */ +export interface TagsInput { + /** the image */ + "name": string; +} + +export interface TagsAnswerTagsItem { + /** its digest */ + "digest": string; + /** the tag */ + "tag": string; +} + +/** is what tags answers. */ +export interface TagsAnswer { + /** the image */ + "name": string; + /** every tag */ + "tags": TagsAnswerTagsItem[]; +} + +/** is what hold takes. */ +export interface HoldInput { + /** the digest */ + "digest": string; + /** the name */ + "name": string; + /** which of the sweep's rules keeps it */ + "reason": string; +} + +/** is what hold answers. */ +export interface HoldAnswer { + /** it is held */ + "held": boolean; + /** when the hold began, RFC 3339 */ + "since"?: string; +} + +/** is what release takes. */ +export interface ReleaseInput { + /** the digest */ + "digest": string; + /** the name */ + "name": string; + /** why it is let go */ + "reason": string; +} + +/** is what release answers. */ +export interface ReleaseAnswer { + /** a hold was let go; false when there was none */ + "released": boolean; +} + +/** is what list takes. */ +export interface ListInput { + /** the repository, /; every repository of the seat when not given */ + "name"?: string; +} + +/** is what list answers. */ +export interface ListAnswer { + /** each repository named, in order of its name */ + "repositories": Repository[]; +} + +/** is the body of the event held. */ +export type HeldEvent = Hold; + +/** is the body of the event released. */ +export type ReleasedEvent = Hold; + +/** is one value of the state holds under the key ... */ +export interface HoldsBySeatNameDigestValue { + "by": string; + "digest": string; + "kind": string; + "name": string; + "seat": string; + /** when (RFC 3339 time) */ + "since": string; + "why"?: string; +} + +export interface WindowsByCallerValueDroppedItem { + /** when (RFC 3339 time) */ + "at"?: string; + "by"?: string; + "node": string; + "why"?: string; +} + +/** is one value of the state windows under the key . */ +export interface WindowsByCallerValue { + /** when it last closed (RFC 3339 time) */ + "closed"?: string; + /** retired nodes dropped by hand */ + "dropped"?: WindowsByCallerValueDroppedItem[]; + /** the nodes holding it open */ + "holding"?: string[]; + "last_holding"?: string[]; + /** the caller whose window it is */ + "name": string; + "open"?: boolean; + /** node to when it began to hold */ + "since"?: Record; + /** of each node holding the window, why it holds it */ + "why"?: Record; +} + diff --git a/contracts/ts/seats/containerimageregistry/src/parts/registry.ts b/contracts/ts/seats/containerimageregistry/src/parts/registry.ts new file mode 100644 index 00000000..17a8de46 --- /dev/null +++ b/contracts/ts/seats/containerimageregistry/src/parts/registry.ts @@ -0,0 +1,187 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The part "registry" several seats include whole: The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved. + +export const verbs = { + publish: "publish", + publishPart: "publish-part", + versions: "versions", + remove: "remove", +} as const; + +export const events = { + published: "published", + refused: "refused", + removed: "removed", + kept: "kept", +} as const; + +/** one published thing, as every registry event and answer names it */ +export interface Artifact { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ + "name": string; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** the body of published, refused and removed */ +export interface Outcome { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** the module holding the seat that said it */ + "holder": string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact */ + "module"?: string; + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ + "name": string; + /** why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) */ + "reason"?: string; + /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ + "reference"?: string; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** for kept: the other tags still naming its digest */ + "tags"?: string[]; + /** the transfer it ends, when it ended one */ + "transfer"?: string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** one part of a transfer */ +export interface Part { + /** the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob */ + "digest": string; + /** its size in bytes */ + "size": number; +} + +/** is what publish takes. */ +export interface PublishInput { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ + "name": string; + /** every part's digest and size; none when the digest is already held */ + "parts"?: Part[]; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the transfer's name, the caller's own; the same name opens the same transfer again */ + "transfer": string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** is what publish answers. */ +export interface PublishAnswer { + /** the digest is held already: nothing to send; the outcome follows as an event */ + "held"?: boolean; + /** the digests of the parts the holder lacks, to send by publish-part */ + "lacks"?: string[]; + /** the transfer */ + "transfer": string; +} + +/** is what publish-part takes. */ +export interface PublishPartInput { + /** the chunk's bytes, base64 (at most 760 KiB before encoding) */ + "data": string; + /** this chunk is the part's last */ + "last": boolean; + /** the part's digest */ + "part": string; + /** the chunk's place in its part, from 0 */ + "sequence": number; + /** the transfer */ + "transfer": string; +} + +/** is what publish-part answers. */ +export interface PublishPartAnswer { + /** the part is whole and its bytes match its digest */ + "complete"?: boolean; + /** the part */ + "part": string; + /** the chunks of the part held so far */ + "received"?: number; + /** the transfer */ + "transfer": string; +} + +/** is what versions takes. */ +export interface VersionsInput { + /** narrowed to one kind (optional) */ + "kind"?: string; + /** the name */ + "name": string; +} + +/** is what versions answers. */ +export interface VersionsAnswer { + /** the name */ + "name": string; + /** newest first */ + "versions": Artifact[]; +} + +/** is what remove takes. */ +export interface RemoveInput { + /** the digest (for a bundle, archive or copied base) */ + "digest"?: string; + /** the name */ + "name": string; + /** why it goes */ + "reason": "tag-gone" | "retention"; + /** the tag (for a tagged version) */ + "tag"?: string; + /** the version */ + "version"?: string; +} + +/** is what remove answers. */ +export interface RemoveAnswer { + "artifact"?: Artifact; + /** it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) */ + "kept"?: boolean; + /** why it was kept: shared-digest, another tag names its digest */ + "reason"?: "shared-digest"; + /** it is gone */ + "removed": boolean; + /** the other tags still naming its digest, when kept */ + "tags"?: string[]; +} + +/** is the body of the event published. */ +export type PublishedEvent = Outcome; + +/** is the body of the event refused. */ +export type RefusedEvent = Outcome; + +/** is the body of the event removed. */ +export type RemovedEvent = Outcome; + +/** is the body of the event kept. */ +export type KeptEvent = Outcome; + diff --git a/contracts/ts/seats/containerimageregistry/tsconfig.json b/contracts/ts/seats/containerimageregistry/tsconfig.json new file mode 100644 index 00000000..60274c6a --- /dev/null +++ b/contracts/ts/seats/containerimageregistry/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/contracts/ts/seats/gopackageregistry/package-lock.json b/contracts/ts/seats/gopackageregistry/package-lock.json new file mode 100644 index 00000000..345a71a1 --- /dev/null +++ b/contracts/ts/seats/gopackageregistry/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-delivery-contracts-go-package-registry", + "packages": { + "": { + "name": "@novox/mesh-delivery-contracts-go-package-registry", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/contracts/ts/seats/gopackageregistry/package.json b/contracts/ts/seats/gopackageregistry/package.json new file mode 100644 index 00000000..1ea96fb4 --- /dev/null +++ b/contracts/ts/seats/gopackageregistry/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat go-package-registry, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-delivery-contracts-go-package-registry", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/contracts/ts/seats/gopackageregistry/src/index.ts b/contracts/ts/seats/gopackageregistry/src/index.ts new file mode 100644 index 00000000..f0720909 --- /dev/null +++ b/contracts/ts/seats/gopackageregistry/src/index.ts @@ -0,0 +1,54 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "go-package-registry", defined whole (seats/go-package-registry/seat.json): The mesh's Go modules: Go's module proxy protocol's read side, and publishing over the bus. + +import type * as registry from "./parts/registry.js"; + +export const seat = "go-package-registry"; + +export const verbs = { + publish: "publish", + publishPart: "publish-part", + versions: "versions", + remove: "remove", +} as const; + +export const events = { + published: "published", + refused: "refused", + removed: "removed", + kept: "kept", +} as const; + +export const family = { name: "go-package-registry.events", maxAgeDays: 7 } as const; + +export const publishedSubject = "mesh.seat.go-package-registry.event.published"; + +export const refusedSubject = "mesh.seat.go-package-registry.event.refused"; + +export const removedSubject = "mesh.seat.go-package-registry.event.removed"; + +export const keptSubject = "mesh.seat.go-package-registry.event.kept"; + +export type Artifact = registry.Artifact; +export type KeptEvent = registry.KeptEvent; +export type Outcome = registry.Outcome; +export type Part = registry.Part; +export type PublishAnswer = registry.PublishAnswer; +export type PublishInput = registry.PublishInput; +export type PublishPartAnswer = registry.PublishPartAnswer; +export type PublishPartInput = registry.PublishPartInput; +export type PublishedEvent = registry.PublishedEvent; +export type RefusedEvent = registry.RefusedEvent; +export type RemoveAnswer = registry.RemoveAnswer; +export type RemoveInput = registry.RemoveInput; +export type RemovedEvent = registry.RemovedEvent; +export type VersionsAnswer = registry.VersionsAnswer; +export type VersionsInput = registry.VersionsInput; + +/** is the form of the provision go-package-registry. */ +export interface GoPackageRegistryForm { + /** the read side's address */ + "url": string; +} + diff --git a/contracts/ts/seats/gopackageregistry/src/parts/registry.ts b/contracts/ts/seats/gopackageregistry/src/parts/registry.ts new file mode 100644 index 00000000..17a8de46 --- /dev/null +++ b/contracts/ts/seats/gopackageregistry/src/parts/registry.ts @@ -0,0 +1,187 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The part "registry" several seats include whole: The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved. + +export const verbs = { + publish: "publish", + publishPart: "publish-part", + versions: "versions", + remove: "remove", +} as const; + +export const events = { + published: "published", + refused: "refused", + removed: "removed", + kept: "kept", +} as const; + +/** one published thing, as every registry event and answer names it */ +export interface Artifact { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ + "name": string; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** the body of published, refused and removed */ +export interface Outcome { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** the module holding the seat that said it */ + "holder": string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact */ + "module"?: string; + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ + "name": string; + /** why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) */ + "reason"?: string; + /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ + "reference"?: string; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** for kept: the other tags still naming its digest */ + "tags"?: string[]; + /** the transfer it ends, when it ended one */ + "transfer"?: string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** one part of a transfer */ +export interface Part { + /** the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob */ + "digest": string; + /** its size in bytes */ + "size": number; +} + +/** is what publish takes. */ +export interface PublishInput { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ + "name": string; + /** every part's digest and size; none when the digest is already held */ + "parts"?: Part[]; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the transfer's name, the caller's own; the same name opens the same transfer again */ + "transfer": string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** is what publish answers. */ +export interface PublishAnswer { + /** the digest is held already: nothing to send; the outcome follows as an event */ + "held"?: boolean; + /** the digests of the parts the holder lacks, to send by publish-part */ + "lacks"?: string[]; + /** the transfer */ + "transfer": string; +} + +/** is what publish-part takes. */ +export interface PublishPartInput { + /** the chunk's bytes, base64 (at most 760 KiB before encoding) */ + "data": string; + /** this chunk is the part's last */ + "last": boolean; + /** the part's digest */ + "part": string; + /** the chunk's place in its part, from 0 */ + "sequence": number; + /** the transfer */ + "transfer": string; +} + +/** is what publish-part answers. */ +export interface PublishPartAnswer { + /** the part is whole and its bytes match its digest */ + "complete"?: boolean; + /** the part */ + "part": string; + /** the chunks of the part held so far */ + "received"?: number; + /** the transfer */ + "transfer": string; +} + +/** is what versions takes. */ +export interface VersionsInput { + /** narrowed to one kind (optional) */ + "kind"?: string; + /** the name */ + "name": string; +} + +/** is what versions answers. */ +export interface VersionsAnswer { + /** the name */ + "name": string; + /** newest first */ + "versions": Artifact[]; +} + +/** is what remove takes. */ +export interface RemoveInput { + /** the digest (for a bundle, archive or copied base) */ + "digest"?: string; + /** the name */ + "name": string; + /** why it goes */ + "reason": "tag-gone" | "retention"; + /** the tag (for a tagged version) */ + "tag"?: string; + /** the version */ + "version"?: string; +} + +/** is what remove answers. */ +export interface RemoveAnswer { + "artifact"?: Artifact; + /** it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) */ + "kept"?: boolean; + /** why it was kept: shared-digest, another tag names its digest */ + "reason"?: "shared-digest"; + /** it is gone */ + "removed": boolean; + /** the other tags still naming its digest, when kept */ + "tags"?: string[]; +} + +/** is the body of the event published. */ +export type PublishedEvent = Outcome; + +/** is the body of the event refused. */ +export type RefusedEvent = Outcome; + +/** is the body of the event removed. */ +export type RemovedEvent = Outcome; + +/** is the body of the event kept. */ +export type KeptEvent = Outcome; + diff --git a/contracts/ts/seats/gopackageregistry/tsconfig.json b/contracts/ts/seats/gopackageregistry/tsconfig.json new file mode 100644 index 00000000..60274c6a --- /dev/null +++ b/contracts/ts/seats/gopackageregistry/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/contracts/ts/seats/meshcatalog/package-lock.json b/contracts/ts/seats/meshcatalog/package-lock.json new file mode 100644 index 00000000..2562cdaf --- /dev/null +++ b/contracts/ts/seats/meshcatalog/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-delivery-contracts-mesh-catalog", + "packages": { + "": { + "name": "@novox/mesh-delivery-contracts-mesh-catalog", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/contracts/ts/seats/meshcatalog/package.json b/contracts/ts/seats/meshcatalog/package.json new file mode 100644 index 00000000..4ee7253e --- /dev/null +++ b/contracts/ts/seats/meshcatalog/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat mesh-catalog, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-delivery-contracts-mesh-catalog", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/contracts/ts/seats/meshcatalog/src/index.ts b/contracts/ts/seats/meshcatalog/src/index.ts new file mode 100644 index 00000000..10a89abb --- /dev/null +++ b/contracts/ts/seats/meshcatalog/src/index.ts @@ -0,0 +1,430 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-catalog", defined whole (seats/mesh-catalog/seat.json): The catalog of the mesh: every module the trunk holds with its manifest, fingerprint and verdict, the packages published, and the seat list (ADR 0636 §1, §4). + +export const seat = "mesh-catalog"; + +export const verbs = { + modules: "modules", + module: "module", + provides: "provides", + dependents: "dependents", +} as const; + +export const events = { + seatDefinitionChanged: "seat-definition-changed", + seatDefinitionRemoved: "seat-definition-removed", +} as const; + +export const family = { name: "mesh-catalog.events", maxAgeDays: 7 } as const; + +export const seatDefinitionChangedSubject = "mesh.seat.mesh-catalog.event.seat-definition-changed"; + +export const seatDefinitionRemovedSubject = "mesh.seat.mesh-catalog.event.seat-definition-removed"; + +export const states = { + manifests: "manifests", + modules: "modules", + candidates: "candidates", + verdicts: "verdicts", + predefined: "predefined", + configCandidates: "config-candidates", + packages: "packages", +} as const; + +/** The key of one value of the state manifests under the form "". */ +export function manifestsByDigestKey(digest: string): string { + return digest; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseManifestsByDigestKey(key: string): { digest: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const digest = rest; + rest = ""; + if (rest !== "") return undefined; + return { digest }; +} + +/** The key of one value of the state modules under the form "". */ +export function modulesByModuleKey(module: string): string { + return module; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseModulesByModuleKey(key: string): { module: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const module = rest; + rest = ""; + if (rest !== "") return undefined; + return { module }; +} + +/** The key of one value of the state candidates under the form "". */ +export function candidatesByGroupKey(group: string): string { + return group; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseCandidatesByGroupKey(key: string): { group: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const group = rest; + rest = ""; + if (rest !== "") return undefined; + return { group }; +} + +/** The key of one value of the state verdicts under the form "". */ +export function verdictsByDigestKey(digest: string): string { + return digest; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseVerdictsByDigestKey(key: string): { digest: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const digest = rest; + rest = ""; + if (rest !== "") return undefined; + return { digest }; +} + +/** The key of one value of the state predefined under the form "list". */ +export function predefinedByListKey(): string { + return "list"; +} + +/** The parts of a key of the form "list", or undefined for a key of another form. */ +export function parsePredefinedByListKey(key: string): { } | undefined { + let rest = key; + if (!rest.startsWith("list")) return undefined; + rest = rest.slice(4); + if (rest !== "") return undefined; + return { }; +} + +/** The key of one value of the state config-candidates under the form "". */ +export function configCandidatesByPullKey(pull: string): string { + return pull; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseConfigCandidatesByPullKey(key: string): { pull: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const pull = rest; + rest = ""; + if (rest !== "") return undefined; + return { pull }; +} + +/** The key of one value of the state packages under the form "". */ +export function packagesByModuleKey(module: string): string { + return module; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parsePackagesByModuleKey(key: string): { module: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const module = rest; + rest = ""; + if (rest !== "") return undefined; + return { module }; +} + +/** The key of one value of the state packages under the form ".". */ +export function packagesByPackageSeriesKey(packagePart: string, series: string): string { + return packagePart + "." + series; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parsePackagesByPackageSeriesKey(key: string): { packagePart: string; series: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const packagePart = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const series = rest; + rest = ""; + if (rest !== "") return undefined; + return { packagePart, series }; +} + +/** is what modules takes. */ +export interface ModulesInput { +} + +/** is what modules answers. */ +export type ModulesAnswer = Record; + +/** is what module takes. */ +export interface ModuleInput { + /** the module's name */ + "module": string; +} + +/** is what module answers. */ +export type ModuleAnswer = Record; + +/** is what provides takes. */ +export interface ProvidesInput { + /** the provision, such as postgres-database */ + "provision": string; +} + +/** is what provides answers. */ +export type ProvidesAnswer = Record; + +/** is what dependents takes. */ +export interface DependentsInput { + /** the module's name */ + "module": string; +} + +/** is what dependents answers. */ +export type DependentsAnswer = Record; + +/** is the body of the event seat-definition-changed. */ +export interface SeatDefinitionChangedEvent { + "digest": string; + "seat": string; + /** trunk */ + "source": string; +} + +/** is the body of the event seat-definition-removed. */ +export interface SeatDefinitionRemovedEvent { + "seat": string; +} + +/** is one value of the state manifests under the key . */ +export interface ManifestsByDigestValue { + /** the manifest, UTF-8 JSON text */ + "manifest": string; + "module": string; + /** the layout version */ + "v"?: number; +} + +export interface ModulesByModuleValueInputs2 { + "bases"?: string[]; + "builder"?: string; + "contexts"?: Record; + "import-form"?: string; + "imports"?: string[]; + "locks"?: Record; + "packages"?: string[]; + "paths"?: Record; + "tree": string; +} + +/** is one value of the state modules under the key . */ +export interface ModulesByModuleValue { + "commit": string; + /** the old-form fingerprint */ + "fp"?: string; + "fp-src"?: number; + /** src2: fingerprint */ + "fp2"?: string; + "inputs2"?: ModulesByModuleValueInputs2; + /** manifest digest */ + "manifest"?: string; + "needs"?: string[]; + "none"?: string; + /** why it has none */ + "none2"?: string; + "packages"?: Record; + "path"?: string; + "paths"?: Record; + "problems"?: string[]; + "repository": string; + /** the source revision */ + "src"?: number; + "toolchains"?: Record; + "unknown"?: string[]; + /** the layout version */ + "v"?: number; + "verdict"?: string; + "warnings"?: string[]; +} + +export interface CandidatesByGroupValuePullsItemFpsValue { + "fp2"?: string; + "manifest"?: string; + "none2"?: string; + "packages"?: Record; + "paths"?: Record; +} + +export interface CandidatesByGroupValuePullsItem { + "fps"?: Record; + "head": string; + "modules": string[]; + "number": number; + "repository": string; +} + +/** is one value of the state candidates under the key . */ +export interface CandidatesByGroupValue { + /** the source revision it is read on */ + "base": number; + "fpset"?: string; + "group": string; + "heads": string; + "pulls": CandidatesByGroupValuePullsItem[]; + /** the layout version */ + "v"?: number; +} + +/** is one value of the state verdicts under the key . */ +export interface VerdictsByDigestValue { + /** when (RFC 3339 time) */ + "at"?: string; + "judge"?: string; + "module": string; + "problems"?: string[]; + "state": string; + "unknown"?: string[]; + /** the layout version */ + "v"?: number; + "warnings"?: string[]; +} + +/** is one value of the state predefined under the key list. */ +export interface PredefinedByListValue { + /** when (RFC 3339 time) */ + "at"?: string; + "commit": string; + /** the foundation's predefined list as kept */ + "list": unknown; + /** the layout version */ + "v"?: number; +} + +export interface ConfigCandidatesByPullValueDeclaresValueValue { + /** the default */ + "default": unknown; + "description"?: string; + /** the allowed values */ + "enum"?: unknown; + "kind": string; + "maximum"?: number; + "minimum"?: number; + "pattern"?: string; + "predefined"?: string; + "required"?: boolean; + "runtime"?: boolean; + "trusted"?: boolean; + "type"?: string; + "why": string; +} + +/** is one value of the state config-candidates under the key . */ +export interface ConfigCandidatesByPullValue { + /** when (RFC 3339 time) */ + "at"?: string; + /** module to key to declaration */ + "declares": Record>; + "head": string; + "pull": string; + /** the layout version */ + "v"?: number; +} + +export interface PackagesByModuleValuePackagesItem { + "digest": string; + "package": string; +} + +/** is one value of the state packages under the key . */ +export interface PackagesByModuleValue { + /** when (RFC 3339 time) */ + "at"?: string; + "commit": string; + "dir"?: string; + /** the Go module path */ + "module": string; + "packages": PackagesByModuleValuePackagesItem[]; + "repository": string; + /** digest of go.mod and go.sum */ + "requires"?: string; + /** the layout version */ + "v"?: number; +} + +export interface PackagesByPackageSeriesValueNewestContentsPackagesItem { + "digest": string; + "package": string; +} + +export interface PackagesByPackageSeriesValueNewestContents { + /** when (RFC 3339 time) */ + "at"?: string; + "commit": string; + "dir"?: string; + /** the Go module path */ + "module": string; + "packages": PackagesByPackageSeriesValueNewestContentsPackagesItem[]; + "repository": string; + /** digest of go.mod and go.sum */ + "requires"?: string; + /** the layout version */ + "v"?: number; +} + +export interface PackagesByPackageSeriesValueNewest { + "commit"?: string; + "contents"?: PackagesByPackageSeriesValueNewestContents; + "digest"?: string; + "fingerprint"?: string; + "version": string; +} + +export interface PackagesByPackageSeriesValueNumberedContentsPackagesItem { + "digest": string; + "package": string; +} + +export interface PackagesByPackageSeriesValueNumberedContents { + /** when (RFC 3339 time) */ + "at"?: string; + "commit": string; + "dir"?: string; + /** the Go module path */ + "module": string; + "packages": PackagesByPackageSeriesValueNumberedContentsPackagesItem[]; + "repository": string; + /** digest of go.mod and go.sum */ + "requires"?: string; + /** the layout version */ + "v"?: number; +} + +export interface PackagesByPackageSeriesValueNumbered { + "commit"?: string; + "contents"?: PackagesByPackageSeriesValueNumberedContents; + "digest"?: string; + "fingerprint"?: string; + "version": string; +} + +/** is one value of the state packages under the key .. */ +export interface PackagesByPackageSeriesValue { + /** when (RFC 3339 time) */ + "at"?: string; + "go-module"?: string; + "name": string; + "newest"?: PackagesByPackageSeriesValueNewest; + "numbered"?: PackagesByPackageSeriesValueNumbered; + "package"?: string; + "seat"?: string; + "series": string; + /** the layout version */ + "v"?: number; +} + diff --git a/contracts/ts/seats/meshcatalog/tsconfig.json b/contracts/ts/seats/meshcatalog/tsconfig.json new file mode 100644 index 00000000..60274c6a --- /dev/null +++ b/contracts/ts/seats/meshcatalog/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/contracts/ts/seats/meshdelivery/package-lock.json b/contracts/ts/seats/meshdelivery/package-lock.json new file mode 100644 index 00000000..8bfb16f6 --- /dev/null +++ b/contracts/ts/seats/meshdelivery/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-delivery-contracts-mesh-delivery", + "packages": { + "": { + "name": "@novox/mesh-delivery-contracts-mesh-delivery", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/contracts/ts/seats/meshdelivery/package.json b/contracts/ts/seats/meshdelivery/package.json new file mode 100644 index 00000000..dda5c61b --- /dev/null +++ b/contracts/ts/seats/meshdelivery/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat mesh-delivery, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-delivery-contracts-mesh-delivery", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/contracts/ts/seats/meshdelivery/src/index.ts b/contracts/ts/seats/meshdelivery/src/index.ts new file mode 100644 index 00000000..ac1f7057 --- /dev/null +++ b/contracts/ts/seats/meshdelivery/src/index.ts @@ -0,0 +1,338 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-delivery", defined whole (seats/mesh-delivery/seat.json): The delivery gate: reads and steers deliveries, merges and the checks that decide them; the merge is made on the operator's Approve once both checks passed. + +export const seat = "mesh-delivery"; + +export const verbs = { + deliveries: "deliveries", + times: "times", + show: "show", + groups: "groups", + whatIf: "what-if", + checks: "checks", + table: "table", + stalled: "stalled", + recheck: "recheck", + release: "release", + stop: "stop", + close: "close", + retireHistory: "retire-history", + settle: "settle", +} as const; + +export const states = { + source: "source", + merges: "merges", + asks: "asks", + owed: "owed", + turns: "turns", +} as const; + +/** The key of one value of the state source under the form "main". */ +export function sourceByMainKey(): string { + return "main"; +} + +/** The parts of a key of the form "main", or undefined for a key of another form. */ +export function parseSourceByMainKey(key: string): { } | undefined { + let rest = key; + if (!rest.startsWith("main")) return undefined; + rest = rest.slice(4); + if (rest !== "") return undefined; + return { }; +} + +/** The key of one value of the state merges under the form "". */ +export function mergesByGroupKey(group: string): string { + return group; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseMergesByGroupKey(key: string): { group: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const group = rest; + rest = ""; + if (rest !== "") return undefined; + return { group }; +} + +/** The key of one value of the state asks under the form "merge..". */ +export function asksByMergeGroupHeadsKey(group: string, heads: string): string { + return "merge." + group + "." + heads; +} + +/** The parts of a key of the form "merge..", or undefined for a key of another form. */ +export function parseAsksByMergeGroupHeadsKey(key: string): { group: string; heads: string } | undefined { + let rest = key; + if (!rest.startsWith("merge.")) return undefined; + rest = rest.slice(6); + const at1 = rest.indexOf("."); + if (at1 <= 0) return undefined; + const group = rest.slice(0, at1); + rest = rest.slice(at1 + 1); + if (rest === "") return undefined; + const heads = rest; + rest = ""; + if (rest !== "") return undefined; + return { group, heads }; +} + +/** The key of one value of the state owed under the form "...". */ +export function owedByRepositoryNumberHeadActKey(repository: string, number: string, head: string, act: string): string { + return repository + "." + number + "." + head + "." + act; +} + +/** The parts of a key of the form "...", or undefined for a key of another form. */ +export function parseOwedByRepositoryNumberHeadActKey(key: string): { repository: string; number: string; head: string; act: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const repository = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + const at2 = rest.indexOf("."); + if (at2 <= 0) return undefined; + const number = rest.slice(0, at2); + rest = rest.slice(at2 + 1); + const at4 = rest.indexOf("."); + if (at4 <= 0) return undefined; + const head = rest.slice(0, at4); + rest = rest.slice(at4 + 1); + if (rest === "") return undefined; + const act = rest; + rest = ""; + if (rest !== "") return undefined; + return { repository, number, head, act }; +} + +/** The key of one value of the state turns under the form "". */ +export function turnsByRepositoryKey(repository: string): string { + return repository; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseTurnsByRepositoryKey(key: string): { repository: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const repository = rest; + rest = ""; + if (rest !== "") return undefined; + return { repository }; +} + +/** is what deliveries takes. */ +export interface DeliveriesInput { + /** "true": the final ones of the last thirty days too */ + "all"?: "true" | "false"; + /** a group's id (its branch name) */ + "group"?: string; + /** owner/repository */ + "repository"?: string; + /** one state, e.g. delivering or held */ + "state"?: string; +} + +/** is what deliveries answers. */ +export type DeliveriesAnswer = Record; + +/** is what times takes. */ +export interface TimesInput { +} + +/** is what times answers. */ +export type TimesAnswer = Record; + +/** is what show takes. */ +export interface ShowInput { + /** a delivery's id, or a group's */ + "id": string; +} + +/** is what show answers. */ +export type ShowAnswer = Record; + +/** is what groups takes. */ +export interface GroupsInput { + /** "true": the groups that ended too */ + "all"?: "true" | "false"; +} + +/** is what groups answers. */ +export type GroupsAnswer = Record; + +/** is what what-if takes. */ +export interface WhatIfInput { +} + +/** is what what-if answers. */ +export type WhatIfAnswer = Record; + +/** is what checks takes. */ +export interface ChecksInput { + /** a commit's sha, or the start of one, instead of a pull request */ + "commit"?: string; + /** a pull request's number: its head is read */ + "number"?: string; + /** owner/repository */ + "repository": string; +} + +/** is what checks answers. */ +export type ChecksAnswer = Record; + +/** is what table takes. */ +export interface TableInput { +} + +/** is what table answers. */ +export type TableAnswer = Record; + +/** is what stalled takes. */ +export interface StalledInput { +} + +/** is what stalled answers. */ +export type StalledAnswer = Record; + +/** is what recheck takes. */ +export interface RecheckInput { + /** the delivery's id */ + "id": string; + /** why, kept with the transition */ + "why": string; +} + +/** is what recheck answers. */ +export type RecheckAnswer = Record; + +/** is what release takes. */ +export interface ReleaseInput { +} + +/** is what release answers. */ +export type ReleaseAnswer = Record; + +/** is what stop takes. */ +export interface StopInput { +} + +/** is what stop answers. */ +export type StopAnswer = Record; + +/** is what close takes. */ +export interface CloseInput { + /** the delivery's id */ + "id": string; + /** the condition it answers */ + "why": string; +} + +/** is what close answers. */ +export type CloseAnswer = Record; + +/** is what retire-history takes. */ +export interface RetireHistoryInput { +} + +/** is what retire-history answers. */ +export type RetireHistoryAnswer = Record; + +/** is what settle takes. */ +export interface SettleInput { + /** the held deliveries' ids (owner/repository@commit) */ + "ids": string[]; + /** why they may be settled, shown to the operator */ + "why": string; +} + +/** is what settle answers. */ +export type SettleAnswer = Record; + +/** is one value of the state source under the key main. */ +export interface SourceByMainValue { + /** the group that moved it */ + "group": string; + /** repository to the merged head, a full commit */ + "heads": Record; + /** the layout version */ + "v"?: number; +} + +export interface MergesByGroupValuePullsItem { + "commit"?: string; + "head": string; + "number": number; + "repository": string; + /** merging, merged or refused */ + "state": string; + "words"?: string; +} + +/** is one value of the state merges under the key . */ +export interface MergesByGroupValue { + /** @ to the approved head */ + "approved"?: Record; + /** the ask key */ + "ask": string; + "group": string; + "pulls": MergesByGroupValuePullsItem[]; + /** the layout version */ + "v"?: number; +} + +export interface AsksByMergeGroupHeadsValueHeadsItem { + /** the full commit */ + "head": string; + "number": number; + /** / */ + "repository": string; +} + +/** is one value of the state asks under the key merge... */ +export interface AsksByMergeGroupHeadsValue { + /** the operator-channel ask as raised */ + "ask": unknown; + /** the number of times it was asked */ + "asking"?: number; + "group": string; + "heads": AsksByMergeGroupHeadsValueHeadsItem[]; + /** the layout version */ + "v"?: number; +} + +/** is one value of the state owed under the key .... */ +export interface OwedByRepositoryNumberHeadActValue { + /** status=, view, note or comment. */ + "act": string; + "context"?: string; + "ref"?: string; + "state"?: string; + "target"?: string; + "text"?: string; + /** the layout version */ + "v"?: number; +} + +export interface TurnsByRepositoryValueWaitingItem { + /** when (RFC 3339 time) */ + "approved": string; + "ask"?: string; + "group": string; + "heads": string; + /** when (RFC 3339 time) */ + "since": string; + "step"?: string; + "waits-for"?: string; + /** when (RFC 3339 time) */ + "waits-since"?: string; +} + +/** is one value of the state turns under the key . */ +export interface TurnsByRepositoryValue { + "repository": string; + /** the layout version */ + "v"?: number; + "waiting": TurnsByRepositoryValueWaitingItem[]; +} + diff --git a/contracts/ts/seats/meshdelivery/tsconfig.json b/contracts/ts/seats/meshdelivery/tsconfig.json new file mode 100644 index 00000000..60274c6a --- /dev/null +++ b/contracts/ts/seats/meshdelivery/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/contracts/ts/seats/meshdeliverychecker/package-lock.json b/contracts/ts/seats/meshdeliverychecker/package-lock.json new file mode 100644 index 00000000..c5d3fd93 --- /dev/null +++ b/contracts/ts/seats/meshdeliverychecker/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-delivery-contracts-mesh-delivery-checker", + "packages": { + "": { + "name": "@novox/mesh-delivery-contracts-mesh-delivery-checker", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/contracts/ts/seats/meshdeliverychecker/package.json b/contracts/ts/seats/meshdeliverychecker/package.json new file mode 100644 index 00000000..25896a5e --- /dev/null +++ b/contracts/ts/seats/meshdeliverychecker/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat mesh-delivery-checker, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-delivery-contracts-mesh-delivery-checker", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/contracts/ts/seats/meshdeliverychecker/src/index.ts b/contracts/ts/seats/meshdeliverychecker/src/index.ts new file mode 100644 index 00000000..42323dfa --- /dev/null +++ b/contracts/ts/seats/meshdeliverychecker/src/index.ts @@ -0,0 +1,341 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-delivery-checker", defined whole (seats/mesh-delivery-checker/seat.json): The merge check's tries: start one, read one, end one. The checker's own events stay under its own subjects (ADR 0620 §2). + +export const seat = "mesh-delivery-checker"; + +export const verbs = { + try: "try", + tryRead: "try-read", + tryEnd: "try-end", +} as const; + +export const holderEvents = { + verdict: "verdict", + tagWanted: "tag-wanted", + previewWritten: "preview.written", +} as const; + +export function verdictHolderSubject(holder: string): string { + return "mesh.stream." + holder + ".event.verdict"; +} + +export function tagWantedHolderSubject(holder: string): string { + return "mesh.stream." + holder + ".event.tag-wanted"; +} + +export function previewWrittenHolderSubject(holder: string): string { + return "mesh.stream." + holder + ".event.preview.written"; +} + +export const states = { + verdicts: "verdicts", + kept: "kept", + owed: "owed", + previews: "previews", +} as const; + +/** The key of one value of the state verdicts under the form ".". */ +export function verdictsByGroupHeadsKey(group: string, heads: string): string { + return group + "." + heads; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseVerdictsByGroupHeadsKey(key: string): { group: string; heads: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const group = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const heads = rest; + rest = ""; + if (rest !== "") return undefined; + return { group, heads }; +} + +/** The key of one value of the state kept under the form ".". */ +export function keptByLevelDigestKey(level: string, digest: string): string { + return level + "." + digest; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseKeptByLevelDigestKey(key: string): { level: string; digest: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const level = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const digest = rest; + rest = ""; + if (rest !== "") return undefined; + return { level, digest }; +} + +/** The key of one value of the state owed under the form "...". */ +export function owedByRepositoryNumberHeadActKey(repository: string, number: string, head: string, act: string): string { + return repository + "." + number + "." + head + "." + act; +} + +/** The parts of a key of the form "...", or undefined for a key of another form. */ +export function parseOwedByRepositoryNumberHeadActKey(key: string): { repository: string; number: string; head: string; act: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const repository = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + const at2 = rest.indexOf("."); + if (at2 <= 0) return undefined; + const number = rest.slice(0, at2); + rest = rest.slice(at2 + 1); + const at4 = rest.indexOf("."); + if (at4 <= 0) return undefined; + const head = rest.slice(0, at4); + rest = rest.slice(at4 + 1); + if (rest === "") return undefined; + const act = rest; + rest = ""; + if (rest !== "") return undefined; + return { repository, number, head, act }; +} + +/** The key of one value of the state previews under the form "..". */ +export function previewsByRepositoryHeadTrunkKey(repository: string, head: string, trunk: string): string { + return repository + "." + head + "." + trunk; +} + +/** The parts of a key of the form "..", or undefined for a key of another form. */ +export function parsePreviewsByRepositoryHeadTrunkKey(key: string): { repository: string; head: string; trunk: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const repository = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + const at2 = rest.indexOf("."); + if (at2 <= 0) return undefined; + const head = rest.slice(0, at2); + rest = rest.slice(at2 + 1); + if (rest === "") return undefined; + const trunk = rest; + rest = ""; + if (rest !== "") return undefined; + return { repository, head, trunk }; +} + +/** is what try takes. */ +export interface TryInput { + /** the full commit to try, 40 or 64 hex digits */ + "head": string; + /** unit or integration: only this level; both when absent */ + "level"?: string; + /** / */ + "repository": string; + /** a go test -run pattern: only those tests run, and nothing is kept */ + "tests"?: string; + /** the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent */ + "trunk"?: string; +} + +/** is what try answers. */ +export interface TryAnswer { + /** the try's id */ + "id": string; + /** its state */ + "state"?: string; +} + +/** is what try-read takes. */ +export interface TryReadInput { + /** the try's id, try- */ + "id": string; +} + +/** is what try-read answers. */ +export type TryReadAnswer = Record; + +/** is what try-end takes. */ +export interface TryEndInput { + /** the try's id, try- */ + "id": string; + /** why it is ended, in words */ + "why": string; +} + +/** is what try-end answers. */ +export interface TryEndAnswer { + /** it is ended */ + "ended": boolean; + /** the try */ + "id": string; +} + +export interface VerdictHolderEventSeriesItem { + /** its artifact, or the package's ecosystem name as the module's packages name it */ + "artifact": string; + /** the series on trunk */ + "from": string; + /** the module whose artifact's series changes */ + "module": string; + /** the series the checked head gives it */ + "to": string; +} + +/** is the body of the holder's event verdict. */ +export interface VerdictHolderEvent { + /** the commit checked */ + "head": string; + /** the generated files out of step with their seat definitions, each path (design 26) */ + "out-of-step"?: string[]; + /** the pull request's number, for a pull request's check */ + "pull"?: number; + /** owner/repo */ + "repository": string; + /** each change of an artifact's series the checked head makes against trunk's manifests, once each (design 32); absent when it changes none */ + "series"?: VerdictHolderEventSeriesItem[]; + /** pass or fail */ + "verdict": string; +} + +/** is the body of the holder's event tag-wanted. */ +export interface TagWantedHolderEvent { + /** the pull request's branch: its tag is removed when the branch ends */ + "branch": string; + /** its head */ + "head": string; + /** the group member */ + "module": string; + /** the package it declares */ + "package": string; + /** the pull request */ + "pull"?: number; + /** owner/repo */ + "repository": string; + /** the tag wanted */ + "tag"?: string; +} + +export interface VerdictsByGroupHeadsValueComposition { + "fpset"?: string; + "machines"?: string[]; + "sdk"?: string; + /** not-run, pass or fail */ + "state": string; + "words"?: string; +} + +export interface VerdictsByGroupHeadsValueFailed { + "machine"?: string; + "test"?: string; + "words"?: string; +} + +export interface VerdictsByGroupHeadsValueFailuresItem { + "machine"?: string; + "test"?: string; + "words"?: string; +} + +export interface VerdictsByGroupHeadsValueLevelsValue { + "kept": number; + "run": number; + "skipped"?: number; +} + +export interface VerdictsByGroupHeadsValueSeriesItem { + "artifact": string; + "from": string; + "module": string; + "to": string; +} + +/** milliseconds */ +export interface VerdictsByGroupHeadsValueTook { + "clone"?: number; + "composition"?: number; + "integration-run"?: number; + "integration-setup"?: number; + "lint"?: number; + "queue"?: number; + "unit"?: number; +} + +/** is one value of the state verdicts under the key .. */ +export interface VerdictsByGroupHeadsValue { + "beside"?: Record; + "carried-from"?: Record; + "composition"?: VerdictsByGroupHeadsValueComposition; + "failed"?: VerdictsByGroupHeadsValueFailed; + "failures"?: VerdictsByGroupHeadsValueFailuresItem[]; + "group": string; + /** 12 hex digits */ + "heads": string; + /** by level, unit or integration */ + "levels"?: Record; + "machines"?: string[]; + "over-bound"?: string[]; + "sdk"?: string; + "series"?: VerdictsByGroupHeadsValueSeriesItem[]; + /** running, pass or fail */ + "state": string; + /** milliseconds */ + "took"?: VerdictsByGroupHeadsValueTook; + /** repository to the trunk head merged onto */ + "trunk"?: Record; + /** the layout version */ + "v"?: number; +} + +/** is one value of the state kept under the key .. */ +export interface KeptByLevelDigestValue { + /** when (RFC 3339 time) */ + "at"?: string; + "level": string; + "package": string; + "repository": string; + /** pass: a failure is never kept */ + "state": string; + "tests"?: number; + "took"?: number; + "toolchain"?: string; + /** the layout version */ + "v"?: number; +} + +/** is one value of the state owed under the key .... */ +export interface OwedByRepositoryNumberHeadActValue { + /** status=, view, note or comment. */ + "act": string; + "context"?: string; + "ref"?: string; + "state"?: string; + "target"?: string; + "text"?: string; + /** the layout version */ + "v"?: number; +} + +export interface PreviewsByRepositoryHeadTrunkValueModulesValue { + "locks"?: Record; + /** manifest digest */ + "manifest": string; + "packages"?: string[]; + "paths"?: Record; + "tree": string; +} + +/** is one value of the state previews under the key ... */ +export interface PreviewsByRepositoryHeadTrunkValue { + /** files in conflict */ + "conflicts"?: string[]; + "head": string; + /** the merge commit */ + "merge"?: string; + /** by directory */ + "modules"?: Record; + "repository": string; + "trunk": string; + /** the layout version */ + "v"?: number; +} + diff --git a/contracts/ts/seats/meshdeliverychecker/tsconfig.json b/contracts/ts/seats/meshdeliverychecker/tsconfig.json new file mode 100644 index 00000000..60274c6a --- /dev/null +++ b/contracts/ts/seats/meshdeliverychecker/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/contracts/ts/seats/nodebuildgo/package-lock.json b/contracts/ts/seats/nodebuildgo/package-lock.json new file mode 100644 index 00000000..bc0af20e --- /dev/null +++ b/contracts/ts/seats/nodebuildgo/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-delivery-contracts-node-build-go", + "packages": { + "": { + "name": "@novox/mesh-delivery-contracts-node-build-go", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/contracts/ts/seats/nodebuildgo/package.json b/contracts/ts/seats/nodebuildgo/package.json new file mode 100644 index 00000000..074cc833 --- /dev/null +++ b/contracts/ts/seats/nodebuildgo/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat node-build-go, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-delivery-contracts-node-build-go", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/contracts/ts/seats/nodebuildgo/src/index.ts b/contracts/ts/seats/nodebuildgo/src/index.ts new file mode 100644 index 00000000..24fa95b4 --- /dev/null +++ b/contracts/ts/seats/nodebuildgo/src/index.ts @@ -0,0 +1,153 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "node-build-go", defined whole (seats/node-build-go/seat.json): The Go build module on a node: it takes the asks of the builder and the checker, runs them as steps and answers. + +import type * as buildStep from "./parts/build-step.js"; + +export const seat = "node-build-go"; + +export const verbs = { + current: "current", + pause: "pause", + resume: "resume", +} as const; + +export const accepts = { + delivery: "delivery", + check: "check", + recheck: "recheck", +} as const; + +export const deliverySubject = "mesh.seat.node-build-go.accept.delivery"; + +export const checkSubject = "mesh.seat.node-build-go.accept.check"; + +export const recheckSubject = "mesh.seat.node-build-go.accept.recheck"; + +export const states = { + steps: "steps", + pauses: "pauses", + toolchains: "toolchains", +} as const; + +/** The key of one value of the state steps under the form ".". */ +export function stepsByNodeIDKey(node: string, id: string): string { + return node + "." + id; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseStepsByNodeIDKey(key: string): { node: string; id: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const id = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, id }; +} + +/** The key of one value of the state pauses under the form "paused.". */ +export function pausesByPausedNodeKey(node: string): string { + return "paused." + node; +} + +/** The parts of a key of the form "paused.", or undefined for a key of another form. */ +export function parsePausesByPausedNodeKey(key: string): { node: string } | undefined { + let rest = key; + if (!rest.startsWith("paused.")) return undefined; + rest = rest.slice(7); + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { node }; +} + +/** The key of one value of the state pauses under the form "capacity.". */ +export function pausesByCapacityNodeKey(node: string): string { + return "capacity." + node; +} + +/** The parts of a key of the form "capacity.", or undefined for a key of another form. */ +export function parsePausesByCapacityNodeKey(key: string): { node: string } | undefined { + let rest = key; + if (!rest.startsWith("capacity.")) return undefined; + rest = rest.slice(9); + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { node }; +} + +/** The key of one value of the state toolchains under the form ".". */ +export function toolchainsByNodeLanguageKey(node: string, language: string): string { + return node + "." + language; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseToolchainsByNodeLanguageKey(key: string): { node: string; language: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const language = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, language }; +} + +export type BuildAnswer = buildStep.BuildAnswer; +export type BuildAnswerArtifactsItem = buildStep.BuildAnswerArtifactsItem; +export type BuildAnswerToolchain = buildStep.BuildAnswerToolchain; +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 Pause = buildStep.Pause; +export type PauseAnswer = buildStep.PauseAnswer; +export type PauseInput = buildStep.PauseInput; +export type ResumeAnswer = buildStep.ResumeAnswer; +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 Toolchain = buildStep.Toolchain; + +/** is the body of delivery. */ +export type DeliveryAccept = StepAsk; + +/** is the body of check. */ +export type CheckAccept = StepAsk; + +/** is the body of recheck. */ +export type RecheckAccept = StepAsk; + +/** is one value of the state steps under the key .. */ +export type StepsByNodeIDValue = Step; + +/** is one value of the state pauses under the key paused.. */ +export type PausesByPausedNodeValue = Pause; + +/** is one value of the state pauses under the key capacity.. */ +export type PausesByCapacityNodeValue = Capacity; + +/** is one value of the state toolchains under the key .. */ +export type ToolchainsByNodeLanguageValue = Toolchain; + diff --git a/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts b/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts new file mode 100644 index 00000000..fa5146dc --- /dev/null +++ b/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts @@ -0,0 +1,232 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The part "build-step" several seats include whole: What the three build seats share: the verbs that read and steer a node's builds, and the layouts of the asks they accept and the steps they keep. A seat lists its states against these (a part carries no states). + +export const verbs = { + current: "current", + pause: "pause", + resume: "resume", +} as const; + +export interface BuildAnswerArtifactsItem { + "artifact": string; + "digest": string; + "reference"?: string; +} + +export interface BuildAnswerToolchain { + "language": string; + "recipe": string; + "version": string; +} + +export interface BuildAnswer { + "artifacts": BuildAnswerArtifactsItem[]; + "imports"?: string[]; + "out-of-step"?: string[]; + "toolchain"?: BuildAnswerToolchain; +} + +/** how many steps the node runs at once */ +export interface Capacity { + /** when (RFC 3339 time) */ + "at"?: string; + "build-hours"?: string; + "build-hours-fault"?: string; + "by-cpu"?: number; + "configured": number; + "configured-from"?: string; + "cores-per-step"?: number; + "effective": number; + "ineligible"?: string; + "outside-hours"?: boolean; + "quota-cores"?: number; + "quota-from"?: string; + /** the layout version */ + "v"?: number; + "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[]; +} + +export interface ComposeAnswer { + "built": boolean; + "key"?: string; + /** the compose-candidate verdict */ + "verdict"?: unknown; +} + +/** whether the node takes no new ask */ +export interface Pause { + /** when (RFC 3339 time) */ + "at"?: string; + "by"?: string; + "paused": boolean; + /** the layout version */ + "v"?: number; +} + +/** one step a build module runs on a node */ +export interface Step { + /** when (RFC 3339 time) */ + "ended"?: string; + "id": string; + "node": string; + "owner"?: string; + "phase"?: string; + /** when (RFC 3339 time) */ + "phase-at"?: string; + "reason"?: string; + /** what the step answered */ + "result"?: unknown; + /** when (RFC 3339 time) */ + "started": string; + /** running, passed, failed or refused */ + "state": string; + "tool": string; + /** the layout version */ + "v"?: number; + "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; + "fp"?: string; + "module": string; + "path"?: string; + "publish"?: boolean; + "toolchain"?: StepAskBuildToolchain; +} + +export interface StepAskCheck { + /** what the build module fetches: the repository, commit and directory, as the indexer states it */ + "fetch": unknown; + "kept"?: boolean; + "levels": string[]; + "packages"?: string[]; + "run"?: string; + "runner"?: string; + "runner_when"?: string; +} + +export interface StepAskCompose { + "dir": string; + /** what the build module fetches: the repository, commit and directory, as the indexer states it */ + "fetch": unknown; + "package": string; + "reader"?: string; + /** the compose-candidate request */ + "request": unknown; +} + +/** one ask of a build module */ +export interface StepAsk { + "asker": string; + /** when (RFC 3339 time) */ + "at"?: string; + "build"?: StepAskBuild; + "check"?: StepAskCheck; + "compose"?: StepAskCompose; + "id": string; + /** build, check or compose */ + "kind": string; + /** go, typescript or image */ + "language": string; + "needs"?: string[]; + /** package to the tagged version a build pins */ + "packages"?: Record; + /** the layout version */ + "v"?: number; +} + +/** the toolchain a node reports */ +export interface Toolchain { + /** when (RFC 3339 time) */ + "at"?: string; + /** go or typescript */ + "language": string; + "recipe": string; + /** the layout version */ + "v"?: number; + "version": string; +} + +/** is what current takes. */ +export interface CurrentInput { +} + +/** is what current answers. */ +export type CurrentAnswer = Record; + +/** is what pause takes. */ +export interface PauseInput { +} + +/** is what pause answers. */ +export type PauseAnswer = Record; + +/** is what resume takes. */ +export interface ResumeInput { +} + +/** is what resume answers. */ +export type ResumeAnswer = Record; + diff --git a/contracts/ts/seats/nodebuildgo/tsconfig.json b/contracts/ts/seats/nodebuildgo/tsconfig.json new file mode 100644 index 00000000..60274c6a --- /dev/null +++ b/contracts/ts/seats/nodebuildgo/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/contracts/ts/seats/nodebuildimage/package-lock.json b/contracts/ts/seats/nodebuildimage/package-lock.json new file mode 100644 index 00000000..ca80ceb0 --- /dev/null +++ b/contracts/ts/seats/nodebuildimage/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-delivery-contracts-node-build-image", + "packages": { + "": { + "name": "@novox/mesh-delivery-contracts-node-build-image", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/contracts/ts/seats/nodebuildimage/package.json b/contracts/ts/seats/nodebuildimage/package.json new file mode 100644 index 00000000..8e2ca24e --- /dev/null +++ b/contracts/ts/seats/nodebuildimage/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat node-build-image, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-delivery-contracts-node-build-image", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/contracts/ts/seats/nodebuildimage/src/index.ts b/contracts/ts/seats/nodebuildimage/src/index.ts new file mode 100644 index 00000000..b289e8f7 --- /dev/null +++ b/contracts/ts/seats/nodebuildimage/src/index.ts @@ -0,0 +1,153 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "node-build-image", defined whole (seats/node-build-image/seat.json): The image build module on a node: it takes the asks of the builder and the checker, runs them as steps and answers. + +import type * as buildStep from "./parts/build-step.js"; + +export const seat = "node-build-image"; + +export const verbs = { + current: "current", + pause: "pause", + resume: "resume", +} as const; + +export const accepts = { + delivery: "delivery", + check: "check", + recheck: "recheck", +} as const; + +export const deliverySubject = "mesh.seat.node-build-image.accept.delivery"; + +export const checkSubject = "mesh.seat.node-build-image.accept.check"; + +export const recheckSubject = "mesh.seat.node-build-image.accept.recheck"; + +export const states = { + steps: "steps", + pauses: "pauses", + toolchains: "toolchains", +} as const; + +/** The key of one value of the state steps under the form ".". */ +export function stepsByNodeIDKey(node: string, id: string): string { + return node + "." + id; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseStepsByNodeIDKey(key: string): { node: string; id: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const id = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, id }; +} + +/** The key of one value of the state pauses under the form "paused.". */ +export function pausesByPausedNodeKey(node: string): string { + return "paused." + node; +} + +/** The parts of a key of the form "paused.", or undefined for a key of another form. */ +export function parsePausesByPausedNodeKey(key: string): { node: string } | undefined { + let rest = key; + if (!rest.startsWith("paused.")) return undefined; + rest = rest.slice(7); + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { node }; +} + +/** The key of one value of the state pauses under the form "capacity.". */ +export function pausesByCapacityNodeKey(node: string): string { + return "capacity." + node; +} + +/** The parts of a key of the form "capacity.", or undefined for a key of another form. */ +export function parsePausesByCapacityNodeKey(key: string): { node: string } | undefined { + let rest = key; + if (!rest.startsWith("capacity.")) return undefined; + rest = rest.slice(9); + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { node }; +} + +/** The key of one value of the state toolchains under the form ".". */ +export function toolchainsByNodeLanguageKey(node: string, language: string): string { + return node + "." + language; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseToolchainsByNodeLanguageKey(key: string): { node: string; language: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const language = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, language }; +} + +export type BuildAnswer = buildStep.BuildAnswer; +export type BuildAnswerArtifactsItem = buildStep.BuildAnswerArtifactsItem; +export type BuildAnswerToolchain = buildStep.BuildAnswerToolchain; +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 Pause = buildStep.Pause; +export type PauseAnswer = buildStep.PauseAnswer; +export type PauseInput = buildStep.PauseInput; +export type ResumeAnswer = buildStep.ResumeAnswer; +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 Toolchain = buildStep.Toolchain; + +/** is the body of delivery. */ +export type DeliveryAccept = StepAsk; + +/** is the body of check. */ +export type CheckAccept = StepAsk; + +/** is the body of recheck. */ +export type RecheckAccept = StepAsk; + +/** is one value of the state steps under the key .. */ +export type StepsByNodeIDValue = Step; + +/** is one value of the state pauses under the key paused.. */ +export type PausesByPausedNodeValue = Pause; + +/** is one value of the state pauses under the key capacity.. */ +export type PausesByCapacityNodeValue = Capacity; + +/** is one value of the state toolchains under the key .. */ +export type ToolchainsByNodeLanguageValue = Toolchain; + diff --git a/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts b/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts new file mode 100644 index 00000000..fa5146dc --- /dev/null +++ b/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts @@ -0,0 +1,232 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The part "build-step" several seats include whole: What the three build seats share: the verbs that read and steer a node's builds, and the layouts of the asks they accept and the steps they keep. A seat lists its states against these (a part carries no states). + +export const verbs = { + current: "current", + pause: "pause", + resume: "resume", +} as const; + +export interface BuildAnswerArtifactsItem { + "artifact": string; + "digest": string; + "reference"?: string; +} + +export interface BuildAnswerToolchain { + "language": string; + "recipe": string; + "version": string; +} + +export interface BuildAnswer { + "artifacts": BuildAnswerArtifactsItem[]; + "imports"?: string[]; + "out-of-step"?: string[]; + "toolchain"?: BuildAnswerToolchain; +} + +/** how many steps the node runs at once */ +export interface Capacity { + /** when (RFC 3339 time) */ + "at"?: string; + "build-hours"?: string; + "build-hours-fault"?: string; + "by-cpu"?: number; + "configured": number; + "configured-from"?: string; + "cores-per-step"?: number; + "effective": number; + "ineligible"?: string; + "outside-hours"?: boolean; + "quota-cores"?: number; + "quota-from"?: string; + /** the layout version */ + "v"?: number; + "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[]; +} + +export interface ComposeAnswer { + "built": boolean; + "key"?: string; + /** the compose-candidate verdict */ + "verdict"?: unknown; +} + +/** whether the node takes no new ask */ +export interface Pause { + /** when (RFC 3339 time) */ + "at"?: string; + "by"?: string; + "paused": boolean; + /** the layout version */ + "v"?: number; +} + +/** one step a build module runs on a node */ +export interface Step { + /** when (RFC 3339 time) */ + "ended"?: string; + "id": string; + "node": string; + "owner"?: string; + "phase"?: string; + /** when (RFC 3339 time) */ + "phase-at"?: string; + "reason"?: string; + /** what the step answered */ + "result"?: unknown; + /** when (RFC 3339 time) */ + "started": string; + /** running, passed, failed or refused */ + "state": string; + "tool": string; + /** the layout version */ + "v"?: number; + "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; + "fp"?: string; + "module": string; + "path"?: string; + "publish"?: boolean; + "toolchain"?: StepAskBuildToolchain; +} + +export interface StepAskCheck { + /** what the build module fetches: the repository, commit and directory, as the indexer states it */ + "fetch": unknown; + "kept"?: boolean; + "levels": string[]; + "packages"?: string[]; + "run"?: string; + "runner"?: string; + "runner_when"?: string; +} + +export interface StepAskCompose { + "dir": string; + /** what the build module fetches: the repository, commit and directory, as the indexer states it */ + "fetch": unknown; + "package": string; + "reader"?: string; + /** the compose-candidate request */ + "request": unknown; +} + +/** one ask of a build module */ +export interface StepAsk { + "asker": string; + /** when (RFC 3339 time) */ + "at"?: string; + "build"?: StepAskBuild; + "check"?: StepAskCheck; + "compose"?: StepAskCompose; + "id": string; + /** build, check or compose */ + "kind": string; + /** go, typescript or image */ + "language": string; + "needs"?: string[]; + /** package to the tagged version a build pins */ + "packages"?: Record; + /** the layout version */ + "v"?: number; +} + +/** the toolchain a node reports */ +export interface Toolchain { + /** when (RFC 3339 time) */ + "at"?: string; + /** go or typescript */ + "language": string; + "recipe": string; + /** the layout version */ + "v"?: number; + "version": string; +} + +/** is what current takes. */ +export interface CurrentInput { +} + +/** is what current answers. */ +export type CurrentAnswer = Record; + +/** is what pause takes. */ +export interface PauseInput { +} + +/** is what pause answers. */ +export type PauseAnswer = Record; + +/** is what resume takes. */ +export interface ResumeInput { +} + +/** is what resume answers. */ +export type ResumeAnswer = Record; + diff --git a/contracts/ts/seats/nodebuildimage/tsconfig.json b/contracts/ts/seats/nodebuildimage/tsconfig.json new file mode 100644 index 00000000..60274c6a --- /dev/null +++ b/contracts/ts/seats/nodebuildimage/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/contracts/ts/seats/nodebuildnode/package-lock.json b/contracts/ts/seats/nodebuildnode/package-lock.json new file mode 100644 index 00000000..4622abb2 --- /dev/null +++ b/contracts/ts/seats/nodebuildnode/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-delivery-contracts-node-build-node", + "packages": { + "": { + "name": "@novox/mesh-delivery-contracts-node-build-node", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/contracts/ts/seats/nodebuildnode/package.json b/contracts/ts/seats/nodebuildnode/package.json new file mode 100644 index 00000000..de82c7d0 --- /dev/null +++ b/contracts/ts/seats/nodebuildnode/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat node-build-node, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-delivery-contracts-node-build-node", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/contracts/ts/seats/nodebuildnode/src/index.ts b/contracts/ts/seats/nodebuildnode/src/index.ts new file mode 100644 index 00000000..d84ca0ae --- /dev/null +++ b/contracts/ts/seats/nodebuildnode/src/index.ts @@ -0,0 +1,153 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "node-build-node", defined whole (seats/node-build-node/seat.json): The Node build module on a node: it takes the asks of the builder and the checker, runs them as steps and answers. + +import type * as buildStep from "./parts/build-step.js"; + +export const seat = "node-build-node"; + +export const verbs = { + current: "current", + pause: "pause", + resume: "resume", +} as const; + +export const accepts = { + delivery: "delivery", + check: "check", + recheck: "recheck", +} as const; + +export const deliverySubject = "mesh.seat.node-build-node.accept.delivery"; + +export const checkSubject = "mesh.seat.node-build-node.accept.check"; + +export const recheckSubject = "mesh.seat.node-build-node.accept.recheck"; + +export const states = { + steps: "steps", + pauses: "pauses", + toolchains: "toolchains", +} as const; + +/** The key of one value of the state steps under the form ".". */ +export function stepsByNodeIDKey(node: string, id: string): string { + return node + "." + id; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseStepsByNodeIDKey(key: string): { node: string; id: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const id = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, id }; +} + +/** The key of one value of the state pauses under the form "paused.". */ +export function pausesByPausedNodeKey(node: string): string { + return "paused." + node; +} + +/** The parts of a key of the form "paused.", or undefined for a key of another form. */ +export function parsePausesByPausedNodeKey(key: string): { node: string } | undefined { + let rest = key; + if (!rest.startsWith("paused.")) return undefined; + rest = rest.slice(7); + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { node }; +} + +/** The key of one value of the state pauses under the form "capacity.". */ +export function pausesByCapacityNodeKey(node: string): string { + return "capacity." + node; +} + +/** The parts of a key of the form "capacity.", or undefined for a key of another form. */ +export function parsePausesByCapacityNodeKey(key: string): { node: string } | undefined { + let rest = key; + if (!rest.startsWith("capacity.")) return undefined; + rest = rest.slice(9); + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { node }; +} + +/** The key of one value of the state toolchains under the form ".". */ +export function toolchainsByNodeLanguageKey(node: string, language: string): string { + return node + "." + language; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseToolchainsByNodeLanguageKey(key: string): { node: string; language: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const language = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, language }; +} + +export type BuildAnswer = buildStep.BuildAnswer; +export type BuildAnswerArtifactsItem = buildStep.BuildAnswerArtifactsItem; +export type BuildAnswerToolchain = buildStep.BuildAnswerToolchain; +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 Pause = buildStep.Pause; +export type PauseAnswer = buildStep.PauseAnswer; +export type PauseInput = buildStep.PauseInput; +export type ResumeAnswer = buildStep.ResumeAnswer; +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 Toolchain = buildStep.Toolchain; + +/** is the body of delivery. */ +export type DeliveryAccept = StepAsk; + +/** is the body of check. */ +export type CheckAccept = StepAsk; + +/** is the body of recheck. */ +export type RecheckAccept = StepAsk; + +/** is one value of the state steps under the key .. */ +export type StepsByNodeIDValue = Step; + +/** is one value of the state pauses under the key paused.. */ +export type PausesByPausedNodeValue = Pause; + +/** is one value of the state pauses under the key capacity.. */ +export type PausesByCapacityNodeValue = Capacity; + +/** is one value of the state toolchains under the key .. */ +export type ToolchainsByNodeLanguageValue = Toolchain; + diff --git a/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts b/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts new file mode 100644 index 00000000..fa5146dc --- /dev/null +++ b/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts @@ -0,0 +1,232 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The part "build-step" several seats include whole: What the three build seats share: the verbs that read and steer a node's builds, and the layouts of the asks they accept and the steps they keep. A seat lists its states against these (a part carries no states). + +export const verbs = { + current: "current", + pause: "pause", + resume: "resume", +} as const; + +export interface BuildAnswerArtifactsItem { + "artifact": string; + "digest": string; + "reference"?: string; +} + +export interface BuildAnswerToolchain { + "language": string; + "recipe": string; + "version": string; +} + +export interface BuildAnswer { + "artifacts": BuildAnswerArtifactsItem[]; + "imports"?: string[]; + "out-of-step"?: string[]; + "toolchain"?: BuildAnswerToolchain; +} + +/** how many steps the node runs at once */ +export interface Capacity { + /** when (RFC 3339 time) */ + "at"?: string; + "build-hours"?: string; + "build-hours-fault"?: string; + "by-cpu"?: number; + "configured": number; + "configured-from"?: string; + "cores-per-step"?: number; + "effective": number; + "ineligible"?: string; + "outside-hours"?: boolean; + "quota-cores"?: number; + "quota-from"?: string; + /** the layout version */ + "v"?: number; + "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[]; +} + +export interface ComposeAnswer { + "built": boolean; + "key"?: string; + /** the compose-candidate verdict */ + "verdict"?: unknown; +} + +/** whether the node takes no new ask */ +export interface Pause { + /** when (RFC 3339 time) */ + "at"?: string; + "by"?: string; + "paused": boolean; + /** the layout version */ + "v"?: number; +} + +/** one step a build module runs on a node */ +export interface Step { + /** when (RFC 3339 time) */ + "ended"?: string; + "id": string; + "node": string; + "owner"?: string; + "phase"?: string; + /** when (RFC 3339 time) */ + "phase-at"?: string; + "reason"?: string; + /** what the step answered */ + "result"?: unknown; + /** when (RFC 3339 time) */ + "started": string; + /** running, passed, failed or refused */ + "state": string; + "tool": string; + /** the layout version */ + "v"?: number; + "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; + "fp"?: string; + "module": string; + "path"?: string; + "publish"?: boolean; + "toolchain"?: StepAskBuildToolchain; +} + +export interface StepAskCheck { + /** what the build module fetches: the repository, commit and directory, as the indexer states it */ + "fetch": unknown; + "kept"?: boolean; + "levels": string[]; + "packages"?: string[]; + "run"?: string; + "runner"?: string; + "runner_when"?: string; +} + +export interface StepAskCompose { + "dir": string; + /** what the build module fetches: the repository, commit and directory, as the indexer states it */ + "fetch": unknown; + "package": string; + "reader"?: string; + /** the compose-candidate request */ + "request": unknown; +} + +/** one ask of a build module */ +export interface StepAsk { + "asker": string; + /** when (RFC 3339 time) */ + "at"?: string; + "build"?: StepAskBuild; + "check"?: StepAskCheck; + "compose"?: StepAskCompose; + "id": string; + /** build, check or compose */ + "kind": string; + /** go, typescript or image */ + "language": string; + "needs"?: string[]; + /** package to the tagged version a build pins */ + "packages"?: Record; + /** the layout version */ + "v"?: number; +} + +/** the toolchain a node reports */ +export interface Toolchain { + /** when (RFC 3339 time) */ + "at"?: string; + /** go or typescript */ + "language": string; + "recipe": string; + /** the layout version */ + "v"?: number; + "version": string; +} + +/** is what current takes. */ +export interface CurrentInput { +} + +/** is what current answers. */ +export type CurrentAnswer = Record; + +/** is what pause takes. */ +export interface PauseInput { +} + +/** is what pause answers. */ +export type PauseAnswer = Record; + +/** is what resume takes. */ +export interface ResumeInput { +} + +/** is what resume answers. */ +export type ResumeAnswer = Record; + diff --git a/contracts/ts/seats/nodebuildnode/tsconfig.json b/contracts/ts/seats/nodebuildnode/tsconfig.json new file mode 100644 index 00000000..60274c6a --- /dev/null +++ b/contracts/ts/seats/nodebuildnode/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/contracts/ts/seats/npmpackageregistry/package-lock.json b/contracts/ts/seats/npmpackageregistry/package-lock.json new file mode 100644 index 00000000..321b0c94 --- /dev/null +++ b/contracts/ts/seats/npmpackageregistry/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-delivery-contracts-npm-package-registry", + "packages": { + "": { + "name": "@novox/mesh-delivery-contracts-npm-package-registry", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/contracts/ts/seats/npmpackageregistry/package.json b/contracts/ts/seats/npmpackageregistry/package.json new file mode 100644 index 00000000..ca10dfc9 --- /dev/null +++ b/contracts/ts/seats/npmpackageregistry/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat npm-package-registry, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-delivery-contracts-npm-package-registry", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/contracts/ts/seats/npmpackageregistry/src/index.ts b/contracts/ts/seats/npmpackageregistry/src/index.ts new file mode 100644 index 00000000..a704e8b6 --- /dev/null +++ b/contracts/ts/seats/npmpackageregistry/src/index.ts @@ -0,0 +1,54 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "npm-package-registry", defined whole (seats/npm-package-registry/seat.json): The mesh's npm packages: the npm registry protocol's read side, and publishing over the bus. + +import type * as registry from "./parts/registry.js"; + +export const seat = "npm-package-registry"; + +export const verbs = { + publish: "publish", + publishPart: "publish-part", + versions: "versions", + remove: "remove", +} as const; + +export const events = { + published: "published", + refused: "refused", + removed: "removed", + kept: "kept", +} as const; + +export const family = { name: "npm-package-registry.events", maxAgeDays: 7 } as const; + +export const publishedSubject = "mesh.seat.npm-package-registry.event.published"; + +export const refusedSubject = "mesh.seat.npm-package-registry.event.refused"; + +export const removedSubject = "mesh.seat.npm-package-registry.event.removed"; + +export const keptSubject = "mesh.seat.npm-package-registry.event.kept"; + +export type Artifact = registry.Artifact; +export type KeptEvent = registry.KeptEvent; +export type Outcome = registry.Outcome; +export type Part = registry.Part; +export type PublishAnswer = registry.PublishAnswer; +export type PublishInput = registry.PublishInput; +export type PublishPartAnswer = registry.PublishPartAnswer; +export type PublishPartInput = registry.PublishPartInput; +export type PublishedEvent = registry.PublishedEvent; +export type RefusedEvent = registry.RefusedEvent; +export type RemoveAnswer = registry.RemoveAnswer; +export type RemoveInput = registry.RemoveInput; +export type RemovedEvent = registry.RemovedEvent; +export type VersionsAnswer = registry.VersionsAnswer; +export type VersionsInput = registry.VersionsInput; + +/** is the form of the provision npm-package-registry. */ +export interface NpmPackageRegistryForm { + /** the read side's address */ + "url": string; +} + diff --git a/contracts/ts/seats/npmpackageregistry/src/parts/registry.ts b/contracts/ts/seats/npmpackageregistry/src/parts/registry.ts new file mode 100644 index 00000000..17a8de46 --- /dev/null +++ b/contracts/ts/seats/npmpackageregistry/src/parts/registry.ts @@ -0,0 +1,187 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The part "registry" several seats include whole: The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved. + +export const verbs = { + publish: "publish", + publishPart: "publish-part", + versions: "versions", + remove: "remove", +} as const; + +export const events = { + published: "published", + refused: "refused", + removed: "removed", + kept: "kept", +} as const; + +/** one published thing, as every registry event and answer names it */ +export interface Artifact { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ + "name": string; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** the body of published, refused and removed */ +export interface Outcome { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** the module holding the seat that said it */ + "holder": string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact */ + "module"?: string; + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ + "name": string; + /** why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) */ + "reason"?: string; + /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ + "reference"?: string; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** for kept: the other tags still naming its digest */ + "tags"?: string[]; + /** the transfer it ends, when it ended one */ + "transfer"?: string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** one part of a transfer */ +export interface Part { + /** the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob */ + "digest": string; + /** its size in bytes */ + "size": number; +} + +/** is what publish takes. */ +export interface PublishInput { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ + "name": string; + /** every part's digest and size; none when the digest is already held */ + "parts"?: Part[]; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the transfer's name, the caller's own; the same name opens the same transfer again */ + "transfer": string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** is what publish answers. */ +export interface PublishAnswer { + /** the digest is held already: nothing to send; the outcome follows as an event */ + "held"?: boolean; + /** the digests of the parts the holder lacks, to send by publish-part */ + "lacks"?: string[]; + /** the transfer */ + "transfer": string; +} + +/** is what publish-part takes. */ +export interface PublishPartInput { + /** the chunk's bytes, base64 (at most 760 KiB before encoding) */ + "data": string; + /** this chunk is the part's last */ + "last": boolean; + /** the part's digest */ + "part": string; + /** the chunk's place in its part, from 0 */ + "sequence": number; + /** the transfer */ + "transfer": string; +} + +/** is what publish-part answers. */ +export interface PublishPartAnswer { + /** the part is whole and its bytes match its digest */ + "complete"?: boolean; + /** the part */ + "part": string; + /** the chunks of the part held so far */ + "received"?: number; + /** the transfer */ + "transfer": string; +} + +/** is what versions takes. */ +export interface VersionsInput { + /** narrowed to one kind (optional) */ + "kind"?: string; + /** the name */ + "name": string; +} + +/** is what versions answers. */ +export interface VersionsAnswer { + /** the name */ + "name": string; + /** newest first */ + "versions": Artifact[]; +} + +/** is what remove takes. */ +export interface RemoveInput { + /** the digest (for a bundle, archive or copied base) */ + "digest"?: string; + /** the name */ + "name": string; + /** why it goes */ + "reason": "tag-gone" | "retention"; + /** the tag (for a tagged version) */ + "tag"?: string; + /** the version */ + "version"?: string; +} + +/** is what remove answers. */ +export interface RemoveAnswer { + "artifact"?: Artifact; + /** it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) */ + "kept"?: boolean; + /** why it was kept: shared-digest, another tag names its digest */ + "reason"?: "shared-digest"; + /** it is gone */ + "removed": boolean; + /** the other tags still naming its digest, when kept */ + "tags"?: string[]; +} + +/** is the body of the event published. */ +export type PublishedEvent = Outcome; + +/** is the body of the event refused. */ +export type RefusedEvent = Outcome; + +/** is the body of the event removed. */ +export type RemovedEvent = Outcome; + +/** is the body of the event kept. */ +export type KeptEvent = Outcome; + diff --git a/contracts/ts/seats/npmpackageregistry/tsconfig.json b/contracts/ts/seats/npmpackageregistry/tsconfig.json new file mode 100644 index 00000000..60274c6a --- /dev/null +++ b/contracts/ts/seats/npmpackageregistry/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/contracts/ts/seats/ociartifactregistry/package-lock.json b/contracts/ts/seats/ociartifactregistry/package-lock.json new file mode 100644 index 00000000..0ff49c30 --- /dev/null +++ b/contracts/ts/seats/ociartifactregistry/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-delivery-contracts-oci-artifact-registry", + "packages": { + "": { + "name": "@novox/mesh-delivery-contracts-oci-artifact-registry", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/contracts/ts/seats/ociartifactregistry/package.json b/contracts/ts/seats/ociartifactregistry/package.json new file mode 100644 index 00000000..7f05486f --- /dev/null +++ b/contracts/ts/seats/ociartifactregistry/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat oci-artifact-registry, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-delivery-contracts-oci-artifact-registry", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/contracts/ts/seats/ociartifactregistry/src/index.ts b/contracts/ts/seats/ociartifactregistry/src/index.ts new file mode 100644 index 00000000..aae83af7 --- /dev/null +++ b/contracts/ts/seats/ociartifactregistry/src/index.ts @@ -0,0 +1,230 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "oci-artifact-registry", defined whole (seats/oci-artifact-registry/seat.json): The mesh's bundles and archives, by digest, in the one artifact store: the OCI distribution protocol's read side. + +import type * as registry from "./parts/registry.js"; + +export const seat = "oci-artifact-registry"; + +export const verbs = { + publish: "publish", + publishPart: "publish-part", + versions: "versions", + remove: "remove", + hold: "hold", + release: "release", + list: "list", +} as const; + +export const events = { + published: "published", + refused: "refused", + removed: "removed", + kept: "kept", + held: "held", + released: "released", +} as const; + +export const family = { name: "oci-artifact-registry.events", maxAgeDays: 7 } as const; + +export const publishedSubject = "mesh.seat.oci-artifact-registry.event.published"; + +export const refusedSubject = "mesh.seat.oci-artifact-registry.event.refused"; + +export const removedSubject = "mesh.seat.oci-artifact-registry.event.removed"; + +export const keptSubject = "mesh.seat.oci-artifact-registry.event.kept"; + +export const heldSubject = "mesh.seat.oci-artifact-registry.event.held"; + +export const releasedSubject = "mesh.seat.oci-artifact-registry.event.released"; + +export const states = { + holds: "holds", + windows: "windows", +} as const; + +/** The key of one value of the state holds under the form "..". */ +export function holdsBySeatNameDigestKey(seat: string, name: string, digest: string): string { + return seat + "." + name + "." + digest; +} + +/** The parts of a key of the form "..", or undefined for a key of another form. */ +export function parseHoldsBySeatNameDigestKey(key: string): { seat: string; name: string; digest: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const seat = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + const at2 = rest.indexOf("."); + if (at2 <= 0) return undefined; + const name = rest.slice(0, at2); + rest = rest.slice(at2 + 1); + if (rest === "") return undefined; + const digest = rest; + rest = ""; + if (rest !== "") return undefined; + return { seat, name, digest }; +} + +/** The key of one value of the state windows under the form "". */ +export function windowsByCallerKey(caller: string): string { + return caller; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseWindowsByCallerKey(key: string): { caller: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const caller = rest; + rest = ""; + if (rest !== "") return undefined; + return { caller }; +} + +export type Artifact = registry.Artifact; +export type KeptEvent = registry.KeptEvent; +export type Outcome = registry.Outcome; +export type Part = registry.Part; +export type PublishAnswer = registry.PublishAnswer; +export type PublishInput = registry.PublishInput; +export type PublishPartAnswer = registry.PublishPartAnswer; +export type PublishPartInput = registry.PublishPartInput; +export type PublishedEvent = registry.PublishedEvent; +export type RefusedEvent = registry.RefusedEvent; +export type RemoveAnswer = registry.RemoveAnswer; +export type RemoveInput = registry.RemoveInput; +export type RemovedEvent = registry.RemovedEvent; +export type VersionsAnswer = registry.VersionsAnswer; +export type VersionsInput = registry.VersionsInput; + +/** the body of held and released */ +export interface Hold { + /** when, RFC 3339 */ + "at"?: string; + /** the caller the holder proved */ + "by"?: string; + /** the digest */ + "digest": string; + /** the module holding the seat */ + "holder": string; + /** the name */ + "name": string; + /** why */ + "reason"?: string; +} + +export interface Listed { + /** for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by */ + "artifact"?: string; + /** the manifest's digest, sha256: */ + "digest": string; + /** a hold keeps it from retention */ + "held": boolean; + /** it is an index, naming a manifest per platform */ + "index": boolean; + /** for an index, the digests of the manifests it names */ + "platforms"?: string[]; + /** its bytes: its own content and every blob it names */ + "size": number; + /** the tags naming it, none for an untagged one */ + "tags"?: string[]; +} + +export interface Repository { + /** each manifest it holds, by digest */ + "manifests": Listed[]; + /** the repository */ + "name": string; +} + +/** is what hold takes. */ +export interface HoldInput { + /** the digest */ + "digest": string; + /** the name */ + "name": string; + /** which of the sweep's rules keeps it */ + "reason": string; +} + +/** is what hold answers. */ +export interface HoldAnswer { + /** it is held */ + "held": boolean; + /** when the hold began, RFC 3339 */ + "since"?: string; +} + +/** is what release takes. */ +export interface ReleaseInput { + /** the digest */ + "digest": string; + /** the name */ + "name": string; + /** why it is let go */ + "reason": string; +} + +/** is what release answers. */ +export interface ReleaseAnswer { + /** a hold was let go; false when there was none */ + "released": boolean; +} + +/** is what list takes. */ +export interface ListInput { + /** the repository, /; every repository of the seat when not given */ + "name"?: string; +} + +/** is what list answers. */ +export interface ListAnswer { + /** each repository named, in order of its name */ + "repositories": Repository[]; +} + +/** is the body of the event held. */ +export type HeldEvent = Hold; + +/** is the body of the event released. */ +export type ReleasedEvent = Hold; + +/** is one value of the state holds under the key ... */ +export interface HoldsBySeatNameDigestValue { + "by": string; + "digest": string; + "kind": string; + "name": string; + "seat": string; + /** when (RFC 3339 time) */ + "since": string; + "why"?: string; +} + +export interface WindowsByCallerValueDroppedItem { + /** when (RFC 3339 time) */ + "at"?: string; + "by"?: string; + "node": string; + "why"?: string; +} + +/** is one value of the state windows under the key . */ +export interface WindowsByCallerValue { + /** when it last closed (RFC 3339 time) */ + "closed"?: string; + /** retired nodes dropped by hand */ + "dropped"?: WindowsByCallerValueDroppedItem[]; + /** the nodes holding it open */ + "holding"?: string[]; + "last_holding"?: string[]; + /** the caller whose window it is */ + "name": string; + "open"?: boolean; + /** node to when it began to hold */ + "since"?: Record; + /** of each node holding the window, why it holds it */ + "why"?: Record; +} + diff --git a/contracts/ts/seats/ociartifactregistry/src/parts/registry.ts b/contracts/ts/seats/ociartifactregistry/src/parts/registry.ts new file mode 100644 index 00000000..17a8de46 --- /dev/null +++ b/contracts/ts/seats/ociartifactregistry/src/parts/registry.ts @@ -0,0 +1,187 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The part "registry" several seats include whole: The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved. + +export const verbs = { + publish: "publish", + publishPart: "publish-part", + versions: "versions", + remove: "remove", +} as const; + +export const events = { + published: "published", + refused: "refused", + removed: "removed", + kept: "kept", +} as const; + +/** one published thing, as every registry event and answer names it */ +export interface Artifact { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ + "name": string; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** the body of published, refused and removed */ +export interface Outcome { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** the module holding the seat that said it */ + "holder": string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact */ + "module"?: string; + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ + "name": string; + /** why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) */ + "reason"?: string; + /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ + "reference"?: string; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** for kept: the other tags still naming its digest */ + "tags"?: string[]; + /** the transfer it ends, when it ended one */ + "transfer"?: string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** one part of a transfer */ +export interface Part { + /** the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob */ + "digest": string; + /** its size in bytes */ + "size": number; +} + +/** is what publish takes. */ +export interface PublishInput { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ + "name": string; + /** every part's digest and size; none when the digest is already held */ + "parts"?: Part[]; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the transfer's name, the caller's own; the same name opens the same transfer again */ + "transfer": string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** is what publish answers. */ +export interface PublishAnswer { + /** the digest is held already: nothing to send; the outcome follows as an event */ + "held"?: boolean; + /** the digests of the parts the holder lacks, to send by publish-part */ + "lacks"?: string[]; + /** the transfer */ + "transfer": string; +} + +/** is what publish-part takes. */ +export interface PublishPartInput { + /** the chunk's bytes, base64 (at most 760 KiB before encoding) */ + "data": string; + /** this chunk is the part's last */ + "last": boolean; + /** the part's digest */ + "part": string; + /** the chunk's place in its part, from 0 */ + "sequence": number; + /** the transfer */ + "transfer": string; +} + +/** is what publish-part answers. */ +export interface PublishPartAnswer { + /** the part is whole and its bytes match its digest */ + "complete"?: boolean; + /** the part */ + "part": string; + /** the chunks of the part held so far */ + "received"?: number; + /** the transfer */ + "transfer": string; +} + +/** is what versions takes. */ +export interface VersionsInput { + /** narrowed to one kind (optional) */ + "kind"?: string; + /** the name */ + "name": string; +} + +/** is what versions answers. */ +export interface VersionsAnswer { + /** the name */ + "name": string; + /** newest first */ + "versions": Artifact[]; +} + +/** is what remove takes. */ +export interface RemoveInput { + /** the digest (for a bundle, archive or copied base) */ + "digest"?: string; + /** the name */ + "name": string; + /** why it goes */ + "reason": "tag-gone" | "retention"; + /** the tag (for a tagged version) */ + "tag"?: string; + /** the version */ + "version"?: string; +} + +/** is what remove answers. */ +export interface RemoveAnswer { + "artifact"?: Artifact; + /** it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) */ + "kept"?: boolean; + /** why it was kept: shared-digest, another tag names its digest */ + "reason"?: "shared-digest"; + /** it is gone */ + "removed": boolean; + /** the other tags still naming its digest, when kept */ + "tags"?: string[]; +} + +/** is the body of the event published. */ +export type PublishedEvent = Outcome; + +/** is the body of the event refused. */ +export type RefusedEvent = Outcome; + +/** is the body of the event removed. */ +export type RemovedEvent = Outcome; + +/** is the body of the event kept. */ +export type KeptEvent = Outcome; + diff --git a/contracts/ts/seats/ociartifactregistry/tsconfig.json b/contracts/ts/seats/ociartifactregistry/tsconfig.json new file mode 100644 index 00000000..60274c6a --- /dev/null +++ b/contracts/ts/seats/ociartifactregistry/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/modules/mesh-delivery-forge-bridge/module.json b/modules/mesh-delivery-forge-bridge/module.json index 0eccd0c6..c53795a1 100644 --- a/modules/mesh-delivery-forge-bridge/module.json +++ b/modules/mesh-delivery-forge-bridge/module.json @@ -95,6 +95,15 @@ "loads": [ "mesh-delivery-forge-bridge-tools" ] + }, + { + "name": "forgemsg", + "kind": "package", + "language": "go", + "paths": [ + "forgemsg" + ], + "series": "0.1" } ] } diff --git a/sdk/buildrecord/buildrecord.go b/sdk/buildrecord/buildrecord.go new file mode 100644 index 00000000..0c9f7418 --- /dev/null +++ b/sdk/buildrecord/buildrecord.go @@ -0,0 +1,70 @@ +package buildrecord + +import "time" + +// BuildRecord is the layout the ended controller's chain wrote (mesh-controller internal/chain/records.go Build, line 91), +// the fields a reader outside it read (novox/hq ADR 0437 §4 §5, issue 1223). The controller's record carries more; a decode ignores +// what is not here. Path is the module's directory in its repository, as the controller writes it (Build.Path, records.go:100, +// set from the build request trimmed of slashes at internal/inventory/chain_requests.go:19); empty for a module whose +// manifest is the repository's root. SourceInputs has no json keys of its own: it is written under its Go field names, as the controller +// writes it. +type BuildRecord struct { + ID string `json:"id"` + State string `json:"state,omitempty"` + Failed string `json:"failed,omitempty"` + Repository string `json:"repository"` + Ref string `json:"ref,omitempty"` + Module string `json:"module,omitempty"` + Path string `json:"path,omitempty"` + Commit string `json:"commit,omitempty"` + At time.Time `json:"at,omitzero"` + SourceFingerprint string `json:"source-fingerprint,omitempty"` + SourceInputs *SourceInputs `json:"source-inputs,omitempty"` + // Made is what the build made (Build.Made, records.go:110); the digest is the tail of an artifact's Reference. + Made []Artifact `json:"made,omitempty"` +} + +// Artifact is one thing a build made: mesh-controller internal/chain/records.go Artifact, json keys name, kind and +// reference, nothing else (novox/hq ADR 0478 clause 8). +type Artifact struct { + Name string `json:"name"` + Kind string `json:"kind"` + Reference string `json:"reference"` +} + +// BuildKey is a build's record under its module, a build that knew none under the request's key: copied from +// mesh-controller internal/chain/keys.go:35-40 (BuildKey, with RequestKey at line 42). +func BuildKey(module, id string) string { + if module == "" { + return "req." + id + } + return module + ".build." + id +} + +// NewestKey is the key of a module's newest build, of any state: copied from mesh-controller internal/chain/keys.go:44. +// The key holds no record: its value is the newest build's id as raw bytes (internal/inventory/chain_builds.go:210 and +// :546), and the record lives at BuildKey(module, id). The pointer is a hint, checked against that record. +func NewestKey(module string) string { return module + ".newest" } + +// SourceInputs are what the ended controller's source fingerprint (src1) was of, as its build records hold them +// (BuildRecord), read and never computed again (novox/hq ADR 0437 §5): the module, the git tree of +// its directory, its bases (references, of which only the digest counts), the root tree of each repository its +// artifacts read (by artifact), each artifact's toolchain line, and the reason the source does not pin the build +// when it does not (a build that resolves packages when built). A context is the value the builder records for it: +// a repository's root tree, or for an image that compiles a Go program the `narrow:` hash of the build source it was +// handed (mesh-controller internal/builder/builder.go:1146-1156). +type SourceInputs struct { + Module string + Tree string + Bases []string + Contexts map[string]string + Toolchains map[string]string + Unpinned string +} + +// IsTrunkBuild is whether the record is a successful build of the trunk that recorded its source inputs: an outcome +// (the controller writes no state on one; state asked is a request, records.go:57-59), not failed, and ref "main" or none. A build a delivery plan asked after a merge, or rebuild or build --behind, follows the branch +// without naming it, so it carries no ref; a build of any other branch or of a commit names it (issue 1281). +func (r BuildRecord) IsTrunkBuild() bool { + return r.State == "" && r.Failed == "" && r.SourceInputs != nil && (r.Ref == "" || r.Ref == "main") +} diff --git a/sdk/buildrecord/buildrecord_test.go b/sdk/buildrecord/buildrecord_test.go new file mode 100644 index 00000000..8d992096 --- /dev/null +++ b/sdk/buildrecord/buildrecord_test.go @@ -0,0 +1,179 @@ +package buildrecord + +import ( + "encoding/json" + "os" + "testing" + "time" +) + +// The fixture's path is what the controller writes for a build request: strings.Trim(path, "/") at mesh-controller +// internal/inventory/chain_requests.go:19, the module's directory in its repository (Build.Path, internal/chain/records.go:100). +// The fixture started as a record the controller's own encoder wrote (json.Marshal of a chain.Build, mesh-controller +// internal/chain, a throwaway test run once and deleted) with build-agent's build-1792395447088936520 values, plus a +// manifest-digest and one made artifact (shape: mesh-controller internal/chain/records.go:91-117 Build, json tags, at commit +// 1f708e2d8d72e2c27dbeaba02f14c128d700eada; an outcome has no state key and a build asked after a merge no ref, records.go:57-59), which a BuildRecord does not carry. It was then edited by hand +// to the controller's layout for an outcome: state and ref removed, the repository a clone URL (issue 1281). +func loadBuildFixture(t *testing.T) []byte { + t.Helper() + raw, err := os.ReadFile("testdata/build-record-build-agent.json") + if err != nil { + t.Fatal(err) + } + return raw +} + +func TestBuildRecordDecodes(t *testing.T) { // 1223/AC1 + var r BuildRecord + if err := json.Unmarshal(loadBuildFixture(t), &r); err != nil { + t.Fatal(err) + } + for _, c := range []struct{ name, got, want string }{ + {"id", r.ID, "build-1792395447088936520"}, + {"module", r.Module, "build-agent"}, + {"repository", r.Repository, "http://novox.internal:20000/novox/mesh-controller.git"}, + {"ref", r.Ref, ""}, + {"commit", r.Commit, "7a74cee83f88e6585a5511c3cc53bdf0f5303d20"}, + {"state", r.State, ""}, + {"failed", r.Failed, ""}, + {"source-fingerprint", r.SourceFingerprint, "src1:e846efe73716b78e80860baaac96153de2cb76551325753838ca245d2160a3d7"}, + } { + if c.got != c.want { + t.Errorf("%s = %q, want %q", c.name, c.got, c.want) + } + } + if want := time.Date(2026, 10, 19, 9, 38, 0, 0, time.UTC); !r.At.Equal(want) { + t.Errorf("at = %v, want %v", r.At, want) + } + if !r.IsTrunkBuild() { // 1281/AC7 + t.Error("the fixture, a record as the controller writes it, is not a trunk build") + } + if r.SourceInputs == nil { + t.Fatal("source-inputs missing") + } +} + +func TestBuildRecordPath(t *testing.T) { // 1233/AC1 + var r BuildRecord + if err := json.Unmarshal(loadBuildFixture(t), &r); err != nil { + t.Fatal(err) + } + if r.Path != "modules/build-agent" { + t.Errorf("path = %q, want modules/build-agent", r.Path) + } + // mesh-controller's manifest is its repository's root: the controller writes no path (omitempty), so the path is "". + var m map[string]json.RawMessage + if err := json.Unmarshal(loadBuildFixture(t), &m); err != nil { + t.Fatal(err) + } + delete(m, "path") + raw, _ := json.Marshal(m) + var root BuildRecord + if err := json.Unmarshal(raw, &root); err != nil { + t.Fatal(err) + } + if root.Path != "" { + t.Errorf("path of a root module = %q, want empty", root.Path) + } + // Every field 1223/AC1 checks is still equal in both. + if root.ID != r.ID || root.Module != r.Module || root.Repository != r.Repository || root.Ref != r.Ref || + root.Commit != r.Commit || root.State != r.State || root.SourceFingerprint != r.SourceFingerprint || !root.At.Equal(r.At) { + t.Errorf("fields differ: %+v vs %+v", root, r) + } + // And the path survives a round trip through the SDK's own type. + back, _ := json.Marshal(r) + var again BuildRecord + if err := json.Unmarshal(back, &again); err != nil || again.Path != r.Path { + t.Errorf("round trip path = %q (%v)", again.Path, err) + } +} + +func TestBuildRecordWithoutInputs(t *testing.T) { // 1223/AC2 + var m map[string]json.RawMessage + if err := json.Unmarshal(loadBuildFixture(t), &m); err != nil { + t.Fatal(err) + } + delete(m, "source-inputs") + raw, _ := json.Marshal(m) + var r BuildRecord + if err := json.Unmarshal(raw, &r); err != nil { + t.Fatal(err) + } + if r.SourceInputs != nil { + t.Errorf("source-inputs = %+v, want nil", r.SourceInputs) + } + if r.Module != "build-agent" { + t.Errorf("module = %q, want build-agent", r.Module) + } +} + +func TestBuildKeys(t *testing.T) { // 1223/AC3 + // Literal values of mesh-controller internal/broker/controller_buckets.go:101 (BucketName(ControllerSeat, ChainBuilds) + // is "mesh-controller_builds") and internal/chain/keys.go:35-44. + if got := BuildKey("build-agent", "build-1"); got != "build-agent.build.build-1" { + t.Errorf("BuildKey = %q", got) + } + if got := BuildKey("", "build-1"); got != "req.build-1" { + t.Errorf("BuildKey without a module = %q", got) + } + if got := NewestKey("build-agent"); got != "build-agent.newest" { + t.Errorf("NewestKey = %q", got) + } +} + +// The fixture's made artifacts are the controller's Artifact (mesh-controller internal/chain/records.go, json tags name, +// kind, reference; the controller writes no digest key, the digest is the tail of the reference). The kinds and references +// are those of the live record mesh-controller.builds printed for build-agent build-1792540091397811913 (2026-10-21 01:49). +func TestBuildRecordMade(t *testing.T) { // 1588/AC2 + var r BuildRecord + if err := json.Unmarshal(loadBuildFixture(t), &r); err != nil { + t.Fatal(err) + } + want := []Artifact{ + {Name: "server", Kind: "image", Reference: "artifact-store://build-agent/server@sha256:a79c13f1c727ff94959e80824cc704ea58b80d80ecdd00fd447698acebb35090"}, + {Name: "files", Kind: "archive", Reference: "artifact-store://build-agent/files/blobs/sha256:bccd3f47851436d1b45a3063cc2d7e34c5c109b2d030739bfbb4b9781703f9a9"}, + } + if len(r.Made) != len(want) { + t.Fatalf("made = %+v, want %+v", r.Made, want) + } + for i := range want { + if r.Made[i] != want[i] { + t.Errorf("made[%d] = %+v, want %+v", i, r.Made[i], want[i]) + } + } + var none BuildRecord + if err := json.Unmarshal([]byte(`{"id":"b","repository":"a/b"}`), &none); err != nil || none.Made != nil { + t.Errorf("a record without made: %+v, %v", none, err) + } +} + +func TestIsTrunkBuild(t *testing.T) { // 1281/AC2 + inputs := &SourceInputs{Module: "zsh", Tree: "t"} + for _, c := range []struct { + name string + r BuildRecord + want bool + }{ + {"built on main", BuildRecord{Ref: "main", SourceInputs: inputs}, true}, + // a build a delivery plan asked after a merge, or rebuild / build --behind, carries no ref (live builds, + // 2026-10-19: zsh build-1792417485811374131, mesh-delivery build-1792435539606203626) + {"built with no ref", BuildRecord{SourceInputs: inputs}, true}, + {"another branch", BuildRecord{Ref: "feat/x", SourceInputs: inputs}, false}, + {"a commit", BuildRecord{Ref: "9efdd87a", SourceInputs: inputs}, false}, + {"asked", BuildRecord{State: "asked", SourceInputs: inputs}, false}, + {"answered, state answered, no source inputs", BuildRecord{State: "answered"}, false}, + {"failed", BuildRecord{Failed: "boom", Ref: "main", SourceInputs: inputs}, false}, + {"no source inputs", BuildRecord{Ref: "main"}, false}, + {"no ref and no source inputs", BuildRecord{}, false}, + } { + if got := c.r.IsTrunkBuild(); got != c.want { + t.Errorf("%s: IsTrunkBuild = %v, want %v", c.name, got, c.want) + } + } + // decoded as the controller writes it: the key "ref" is absent + var r BuildRecord + raw := `{"id":"build-1792417485811374131","repository":"http://novox.internal:20000/novox/mesh-catalog.git","module":"zsh","source-inputs":{"Module":"zsh","Tree":"t"}}` + if err := json.Unmarshal([]byte(raw), &r); err != nil || !r.IsTrunkBuild() { + t.Errorf("a decoded record with no ref: trunk = %v, err %v", r.IsTrunkBuild(), err) + } +} diff --git a/sdk/buildrecord/testdata/build-record-build-agent.json b/sdk/buildrecord/testdata/build-record-build-agent.json new file mode 100644 index 00000000..0e756e00 --- /dev/null +++ b/sdk/buildrecord/testdata/build-record-build-agent.json @@ -0,0 +1 @@ +{"id":"build-1792395447088936520","repository":"http://novox.internal:20000/novox/mesh-controller.git","module":"build-agent","commit":"7a74cee83f88e6585a5511c3cc53bdf0f5303d20","path":"modules/build-agent","manifest-digest":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","source-fingerprint":"src1:e846efe73716b78e80860baaac96153de2cb76551325753838ca245d2160a3d7","source-inputs":{"Module":"build-agent","Tree":"11c0b4f90cae0d6e4ef9cc48cbfdf9f6dbe52bca","Bases":["sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc","sha256:8ac98ca534ac3f51e1f420a1dd2c15e74c75cfa0f23f3ad27eb5d7236c349a0c"],"Contexts":{"server":"narrow:d51ca744fd11af59d616c858f76c27b3e58e16e11fbdc0c720eb9d7d19d55fac"},"Toolchains":null,"Unpinned":""},"made":[{"name":"server","kind":"image","reference":"artifact-store://build-agent/server@sha256:a79c13f1c727ff94959e80824cc704ea58b80d80ecdd00fd447698acebb35090"},{"name":"files","kind":"archive","reference":"artifact-store://build-agent/files/blobs/sha256:bccd3f47851436d1b45a3063cc2d7e34c5c109b2d030739bfbb4b9781703f9a9"}],"at":"2026-10-19T09:38:00Z"} diff --git a/sdk/buildstep/capacity.go b/sdk/buildstep/capacity.go new file mode 100644 index 00000000..098f6570 --- /dev/null +++ b/sdk/buildstep/capacity.go @@ -0,0 +1,7 @@ +package buildstep + +// CapacitySetting is the build modules' setting of the node's build capacity (novox/hq ADR 0525 §5, ADR 0567 §12): how +// many asks the node takes at once, an integer config value set on each node's layer of mesh-config. No repository names +// a node: each build module declares it with the default 0, which pulls nothing, and the controller (or mesh-config) +// lays the node's value into the module's settings file. +const CapacitySetting = "capacity" diff --git a/sdk/buildstep/doc.go b/sdk/buildstep/doc.go new file mode 100644 index 00000000..93c1feb7 --- /dev/null +++ b/sdk/buildstep/doc.go @@ -0,0 +1,5 @@ +// Package buildstep is the contract of a build step every delivery part reads (novox/hq ADR 0593 §1): a step's id, the +// fetch request, a step a previous process left running and its end, a failure's words, a module tree's shape, the +// capacity setting and the launchers' names. The runner the build modules share is the Go build module's own package +// (mesh-catalog modules/mesh-build-go/buildstep), which imports this one. +package buildstep diff --git a/sdk/buildstep/failure.go b/sdk/buildstep/failure.go new file mode 100644 index 00000000..bdea5581 --- /dev/null +++ b/sdk/buildstep/failure.go @@ -0,0 +1,141 @@ +package buildstep + +import ( + "fmt" + "strings" + + "git.novox.be/novox/mesh-sdk/go/sdkversion" +) + +// Failure is one test that failed, or a package or file that failed with no test to name (a build that failed, a test +// program that panicked, a test file that did not load). The JSON names are the ones mesh-build-go and the checker have +// always used; File is new and an older reader ignores it. +type Failure struct { + // Package is the Go package's import path, or the TypeScript package's directory in the workspace. + Package string `json:"package"` + // Test names the failing test, or the tests that were running when the program stopped; "" when none can be named. + Test string `json:"test,omitempty"` + // File is where the failing test is (a Go test's file and line is in its Cause; node names the test's file:line:col). + File string `json:"file,omitempty"` + // Cause is the one line that says why: the assertion's or the error's first line, a "panic:" line, a build's first + // error, or words saying how the program stopped. **Never empty, and never a passing line** (PassingLine). + Cause string `json:"cause,omitempty"` + // Output is the failing test's own output, its last lines up to OutputBytes, Cause first: an older checker reads + // only its first line. + Output string `json:"output,omitempty"` +} + +// OutputBytes is the most a failure keeps of its output, and LineBytes the most of one line: four kilobytes holds a long +// assertion message and stays far under a bus message's or a verdict's bound. +const ( + OutputBytes = 4096 + LineBytes = 1024 +) + +// By names a build module and the SDK it was built with, for a result's "by": the checker names it when a failure hides +// its cause ("the build module hid the failure ()"). +func By(module string) string { + return module + " with mesh-sdk " + sdkversion.Version() +} + +// PassingLine says a line is a test runner's word for a test or a package that passed: node's '✔', TAP's "ok N" (at any +// indent), go test's "ok \t", "PASS" and "--- PASS". A line of a test's own that starts "ok " is none of these. Such a line is never a failure's cause, nor part of a failure's output. +func PassingLine(l string) bool { + if strings.HasPrefix(l, "ok \t") { // go test's package line, at column 0 + return true + } + t := strings.TrimSpace(l) + switch { + case t == "PASS", strings.HasPrefix(t, "✔"), strings.HasPrefix(t, "--- PASS"): + return true + } + return tapOk(t) +} + +// Hidden says how a failure hides why it failed, or "" when it does not: it has no cause, or its cause is a passing line. +// The checker says it loudly (issue 1901): a build module that sends such a failure does not keep the contract. +func Hidden(f Failure) string { + c := strings.TrimSpace(f.Cause) + switch { + case c == "": + return "it sent no cause" + case PassingLine(c): + return fmt.Sprintf("its cause is a passing line (%q)", CutLine(c)) + } + return "" +} + +// Clip joins the last lines of a failure's output that fit OutputBytes, each cut to LineBytes, and says how many earlier +// lines it left out. **Its first line is the cause**, put ahead when the kept lines do not start with it, or, with no +// cause and lines left out, the first non-empty line, then the marker: an older checker reads only the first line. +// Passing lines are dropped. +func Clip(lines []string, cause string) string { + own := make([]string, 0, len(lines)) + for _, l := range lines { + if !PassingLine(l) { + own = append(own, l) + } + } + lines = own + size, from := 0, len(lines) + for from > 0 { + n := len(CutLine(lines[from-1])) + 1 + if size+n > OutputBytes && from < len(lines) { + break + } + size += n + from-- + } + kept := make([]string, 0, len(lines)-from+2) + head := strings.TrimSpace(cause) + for i := 0; head == "" && i < from; i++ { + head = strings.TrimSpace(lines[i]) + } + if head != "" && (from > 0 || len(lines) == 0 || strings.TrimSpace(lines[from]) != head) { + kept = append(kept, CutLine(head)) + } + if from > 0 { + kept = append(kept, fmt.Sprintf("... (%d earlier lines)", from)) + } + for _, l := range lines[from:] { + kept = append(kept, CutLine(l)) + } + return strings.Join(kept, "\n") +} + +// CutLine cuts a line to LineBytes, on a character boundary. +func CutLine(l string) string { + if len(l) <= LineBytes { + return l + } + return strings.ToValidUTF8(l[:LineBytes], "") + "..." +} + +// ConsoleFailure is the failure of a command that is no test runner (go vet, gofmt, tsc, a build script): its cause is the +// first line that names an error (tsc's "error TS", go vet's "file.go:12:3: ..."), else the first line that is not a +// heading ("# ", npm's "> script"), and its output the command's lines without passing ones. +func ConsoleFailure(pkg, test, out string) Failure { + var lines []string + for _, l := range strings.Split(strings.ReplaceAll(stripANSI(out), "\r\n", "\n"), "\n") { + if t := strings.TrimSpace(l); t != "" && !PassingLine(t) { + lines = append(lines, strings.TrimRight(l, " \t")) + } + } + cause := "" + for _, l := range lines { + if t := strings.TrimSpace(l); strings.Contains(t, "error") || strings.Contains(t, "Error") || goPosition.MatchString(t) { + cause = t + break + } + } + for _, l := range lines { + if t := strings.TrimSpace(l); cause == "" && !strings.HasPrefix(t, "# ") && !strings.HasPrefix(t, "> ") && !strings.HasPrefix(t, "npm ") { + cause = t + } + } + if cause == "" { + cause = test + " failed and wrote nothing that says why" + } + cause = CutLine(cause) + return Failure{Package: pkg, Test: test, Cause: cause, Output: Clip(lines, cause)} +} diff --git a/sdk/buildstep/helpers.go b/sdk/buildstep/helpers.go new file mode 100644 index 00000000..d9f0b98d --- /dev/null +++ b/sdk/buildstep/helpers.go @@ -0,0 +1,19 @@ +package buildstep + +import ( + "regexp" + "strings" +) + +// goPosition matches a line that starts with a Go file's position, as go vet and the compiler write an error. +var goPosition = regexp.MustCompile(`^\S+\.go:\d+(:\d+)?: `) + +var ansiEscape = regexp.MustCompile("\x1b\\[[0-9;]*m") + +func stripANSI(s string) string { return ansiEscape.ReplaceAllString(s, "") } + +// tapOk says a line is a passing test's "ok N - name", indented or not. +func tapOk(l string) bool { + rest, ok := strings.CutPrefix(strings.TrimSpace(l), "ok ") + return ok && rest != "" && rest[0] >= '0' && rest[0] <= '9' +} diff --git a/sdk/buildstep/launchers.go b/sdk/buildstep/launchers.go new file mode 100644 index 00000000..975f5603 --- /dev/null +++ b/sdk/buildstep/launchers.go @@ -0,0 +1,14 @@ +package buildstep + +import "git.novox.be/novox/mesh-sdk/go/catalogue" + +// LauncherSuffix is what a TypeScript entrypoint's launcher is called beside it: index.js is started as +// index.serve.mjs (novox/hq ADR 0193), an ES module by its own extension whatever the bundle's package.json says. +const LauncherSuffix = catalogue.LauncherSuffix + +// Launchers are the launchers a bundle's build writes, by entrypoint, relative to the bundle's root +// (catalogue.Launchers): the one rule mesh-build-node, build-agent, the controller and a node taking in a build share +// (R4e, issue 1126), so the name a build writes is the name a reader expects. +func Launchers(language string, entrypoints []string) map[string]string { + return catalogue.Launchers(language, entrypoints) +} diff --git a/sdk/buildstep/launchers_test.go b/sdk/buildstep/launchers_test.go new file mode 100644 index 00000000..ddea9a0d --- /dev/null +++ b/sdk/buildstep/launchers_test.go @@ -0,0 +1,22 @@ +package buildstep + +import ( + "reflect" + "testing" +) + +// A TypeScript bundle's entrypoint x.js is started as x.serve.mjs beside it (novox/hq ADR 0193), the rule mesh-build-node +// and build-agent both apply; an entrypoint that is not .js gets none, and another language gets none. +func TestATypeScriptEntrypointIsStartedByTheLauncherBesideIt(t *testing.T) { + got := Launchers("typescript", []string{"index.js", "tools/x.js", "data.json"}) + want := map[string]string{"index.js": "index.serve.mjs", "tools/x.js": "tools/x.serve.mjs"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("Launchers(typescript) = %v, want %v", got, want) + } + if got := Launchers("go", []string{"index.js"}); len(got) != 0 { + t.Fatalf("a Go bundle got launchers: %v", got) + } + if got := Launchers("typescript", nil); len(got) != 0 { + t.Fatalf("a bundle with no entrypoint got launchers: %v", got) + } +} diff --git a/sdk/buildstep/request.go b/sdk/buildstep/request.go new file mode 100644 index 00000000..9cc961ff --- /dev/null +++ b/sdk/buildstep/request.go @@ -0,0 +1,161 @@ +package buildstep + +import ( + "fmt" + "maps" + "path" + "regexp" + "strings" +) + +var ( + requestCommitOK = regexp.MustCompile(`^[0-9a-f]{40}$`) + requestRepositoryOK = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$`) +) + +// FetchRequest 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, and, for a build of a +// module whose every artifact names its paths, that module's directory and its named paths (novox/hq ADR 0499 clause 4). +// +// **The paths come from the builder, not from a manifest** (ADR 0495 clause 1; the lead's decision L8 of R4e): the +// builder fills Dir and Paths from the indexer's Module value (Path, and KeptInputs2.Paths' keys), and the build module +// gives the build and test steps a workspace holding those paths, the manifest and its lock files. A request without +// them builds from the module's directory, as before. +type FetchRequest struct { + Repository string `json:"repository"` + Commit string `json:"commit"` + Ref string `json:"ref,omitempty"` + Base string `json:"base,omitempty"` + Dir string `json:"dir,omitempty"` + Paths []string `json:"paths,omitempty"` + // Packages are the version of each mesh package the build or the check builds against (Ask.Packages, + // novox/hq ADR 0573 §2, §4): the build module writes each into the workspace's go.mod and go.sum, whatever version + // the repository names, checksum-verified, and names them in its log. + Packages PackageVersions `json:"packages,omitempty"` +} + +// FetchRequestProperties are the JSON schema properties of FetchRequest's dir and paths, for a fetch tool's Input. +func FetchRequestProperties() map[string]any { + return map[string]any{ + "dir": map[string]any{"type": "string", "description": "the module's directory in the repository (\".\" for its root), with paths"}, + "paths": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, + "description": "the paths the module names, relative to dir, from the indexer's kept inputs: the build sees only these, the manifest and its lock files"}, + "packages": map[string]any{"type": "object", "additionalProperties": map[string]any{"type": "string"}, + "description": "optional: the version (a full commit) of each mesh package to build against, by its module path, whatever version go.mod names (novox/hq ADR 0573 §2); the builder and the checker fill it"}, + } +} + +// Args is r as a tool call's arguments. +func (r FetchRequest) Args() map[string]any { + args := map[string]any{"repository": r.Repository, "commit": r.Commit} + for k, v := range map[string]string{"ref": r.Ref, "base": r.Base, "dir": r.Dir} { + if v != "" { + args[k] = v + } + } + if len(r.Paths) > 0 { + args["paths"] = append([]string(nil), r.Paths...) + } + if len(r.Packages) > 0 { + args["packages"] = map[string]any{} + for p, v := range r.Packages { + args["packages"].(map[string]any)[p] = v + } + } + return args +} + +// ParseFetchRequest reads a fetch tool's arguments; arguments it does not know (a build module's own) are left to the +// caller. It refuses a repository that is not /, a commit or a base that is not 40 hex digits, paths with no +// module directory or a directory with no path, a directory leaving the repository and a path leaving the directory, +// naming each. +func ParseFetchRequest(args map[string]any) (FetchRequest, error) { + var r FetchRequest + r.Repository, _ = args["repository"].(string) + r.Commit, _ = args["commit"].(string) + r.Ref, _ = args["ref"].(string) + r.Base, _ = args["base"].(string) + r.Dir, _ = args["dir"].(string) + switch v := args["paths"].(type) { + case nil: + case []string: + r.Paths = append([]string(nil), v...) + case []any: + for _, p := range v { + s, ok := p.(string) + if !ok { + return FetchRequest{}, fmt.Errorf("paths is a list of strings") + } + r.Paths = append(r.Paths, s) + } + default: + return FetchRequest{}, fmt.Errorf("paths is a list of strings") + } + packages, err := PackagesArg(args["packages"]) + if err != nil { + return FetchRequest{}, err + } + r.Packages = packages + return r, r.Check() +} + +// PackagesArg reads a tool call's packages argument, an object of strings (as JSON gives it) or the map an ask gives; +// nil for none. +func PackagesArg(v any) (PackageVersions, error) { + out := PackageVersions{} + switch v := v.(type) { + case nil: + return nil, nil + case PackageVersions: + maps.Copy(out, v) + case map[string]string: + maps.Copy(out, v) + case map[string]any: + for p, version := range v { + s, ok := version.(string) + if !ok { + return nil, fmt.Errorf("packages is an object of versions (strings), by package") + } + out[p] = s + } + default: + return nil, fmt.Errorf("packages is an object of versions (strings), by package") + } + if len(out) == 0 { + return nil, nil + } + return out, out.Check() +} + +// Check is what ParseFetchRequest refuses. +func (r FetchRequest) Check() error { + if !requestRepositoryOK.MatchString(r.Repository) || strings.Contains(r.Repository, "..") { + return fmt.Errorf("%q is not a repository, /", r.Repository) + } + if !requestCommitOK.MatchString(r.Commit) { + return fmt.Errorf("%q is not a full commit (40 hex digits)", r.Commit) + } + if r.Base != "" && !requestCommitOK.MatchString(r.Base) { + return fmt.Errorf("%q is not a trunk commit (40 hex digits)", r.Base) + } + switch { + case len(r.Paths) > 0 && r.Dir == "": + return fmt.Errorf("the request names paths and names no module directory (dir) they are relative to") + case len(r.Paths) == 0 && r.Dir != "": + return fmt.Errorf("the request names a module directory and no path") + } + if r.Dir != "" && (outside(r.Dir) || strings.HasPrefix(r.Dir, "/")) { + return fmt.Errorf("the module directory %q leaves the repository", r.Dir) + } + for _, p := range r.Paths { + if p == "" || strings.HasPrefix(p, "/") || outside(p) || path.Clean(p) == "." { + return fmt.Errorf("the path %q leaves the module's directory or is not a path in it", p) + } + } + return nil +} + +func outside(p string) bool { + c := path.Clean(p) + return c == ".." || strings.HasPrefix(c, "../") +} diff --git a/sdk/buildstep/request_test.go b/sdk/buildstep/request_test.go new file mode 100644 index 00000000..7732329f --- /dev/null +++ b/sdk/buildstep/request_test.go @@ -0,0 +1,64 @@ +package buildstep + +import ( + "reflect" + "strings" + "testing" +) + +// A build's fetch is handed the paths of a module that names them (novox/hq ADR 0499 clause 4): the builder fills them +// from the indexer's kept inputs and the build module reads them from the call, never from the manifest (ADR 0495 +// clause 1). The request survives the trip through a tool call's arguments. +func TestAFetchRequestCarriesTheNamedPathsThroughTheToolCall(t *testing.T) { + // And the versions of the mesh packages to build against (novox/hq ADR 0573 §2; issue 1585 S4). + req := FetchRequest{Repository: "novox/mesh-sdk", Commit: strings.Repeat("a", 40), Ref: "refs/pull/7/head", + Dir: ".", Paths: []string{"package.json", "src"}, Packages: map[string]string{"git.novox.be/novox/mesh-sdk/go": strings.Repeat("d", 40)}} + args := req.Args() + // A tool call's arguments arrive as JSON: lists of any, objects of any. + args["paths"] = []any{"package.json", "src"} + if p, ok := args["packages"].(map[string]any); !ok || p["git.novox.be/novox/mesh-sdk/go"] != strings.Repeat("d", 40) { + t.Fatalf("the packages are not in the call's arguments: %v", args["packages"]) + } + got, err := ParseFetchRequest(args) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, req) { + t.Fatalf("got %+v, want %+v", got, req) + } + plain, err := ParseFetchRequest(map[string]any{"repository": "novox/x", "commit": strings.Repeat("b", 40), + "base": strings.Repeat("c", 40), "beside": []any{}}) + if err != nil || plain.Paths != nil || plain.Base != strings.Repeat("c", 40) { + t.Fatalf("a request without paths: %+v, %v", plain, err) + } + + ok := map[string]any{"repository": "novox/x", "commit": strings.Repeat("b", 40)} + for _, c := range []struct { + set map[string]any + says string + }{ + {map[string]any{"repository": "novox"}, "is not a repository"}, + {map[string]any{"repository": "novox/../x"}, "is not a repository"}, + {map[string]any{"commit": "abc"}, "is not a full commit"}, + {map[string]any{"base": "abc"}, "is not a trunk commit"}, + {map[string]any{"paths": []any{"src"}}, "names no module directory"}, + {map[string]any{"dir": "modules/a", "paths": []any{"../b"}}, "leaves the module's directory"}, + {map[string]any{"dir": "../a", "paths": []any{"src"}}, "leaves the repository"}, + {map[string]any{"dir": "modules/a", "paths": []any{3}}, "paths is a list of strings"}, + {map[string]any{"dir": "modules/a"}, "names a module directory and no path"}, + {map[string]any{"packages": map[string]any{"git.novox.be/novox/mesh-sdk/go": "main"}}, "neither a full commit nor a Go semantic version"}, + {map[string]any{"packages": map[string]any{"a b": strings.Repeat("d", 40)}}, "no module path"}, + {map[string]any{"packages": []any{"x"}}, "packages is an object"}, + } { + args := map[string]any{} + for k, v := range ok { + args[k] = v + } + for k, v := range c.set { + args[k] = v + } + if _, err := ParseFetchRequest(args); err == nil || !strings.Contains(err.Error(), c.says) { + t.Errorf("%v: %v, wanted a refusal saying %q", c.set, err, c.says) + } + } +} diff --git a/sdk/buildstep/stale.go b/sdk/buildstep/stale.go new file mode 100644 index 00000000..7e0de98e --- /dev/null +++ b/sdk/buildstep/stale.go @@ -0,0 +1,329 @@ +package buildstep + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "strconv" + "strings" + "time" + + stdio "git.novox.be/novox/mesh-sdk/go" +) + +// RestartReason is why a step a previous process left running is ended. +const RestartReason = "the build module restarted while it ran" + +// Boot is when this process began. A step with no owner (written by an older build) started at or after it is taken +// for this process's, which may still run it, so EndOne refuses it. +var Boot = time.Now() + +// Owner is this process as a step's owner (Step.Owner): `..`, the node's boot id +// (/proc/sys/kernel/random/boot_id), the pid and Boot in milliseconds, base 36. The start keeps two processes apart +// that a reboot or a pid namespace gave the same pid. A running step whose owner is another is a previous process's. +var Owner = ownerOf(bootID, os.Getpid(), Boot) + +// bootID is the node's boot id as Owner holds it; noBootID when it could not be read, which EndStaleAtStart says once. +var bootID = readBootID() + +const noBootID = "no-boot-id" + +func ownerOf(bootID string, pid int, start time.Time) string { + return bootID + "." + strconv.Itoa(pid) + "." + strconv.FormatInt(start.UnixMilli(), 36) +} + +// readBootID is the node's boot id, or noBootID where it cannot be read (the pid and the start then tell the processes +// apart). +func readBootID() string { + b, err := os.ReadFile("/proc/sys/kernel/random/boot_id") + if id := strings.TrimSpace(string(b)); err == nil && id != "" { + return id + } + return noBootID +} + +// Scan reads and writes a steps state with its revisions: its keys, a key's stored value and revision (nil and 0 for +// none), a write that lands only while the key is still at the revision read (KeptState.Update), and a delete of a key +// still at the revision read; both fail with stdio.ErrChanged when another writer was first. +type Scan struct { + Keys func() ([]string, error) + Get func(key string) (json.RawMessage, uint64, error) + Update func(key string, value any, revision uint64) (uint64, error) + Delete func(key string, revision uint64) error +} + +// ScanOf is the Scan of a state the process keeps. +// +// Its Delete reads the key again and deletes it only while it is still at the revision read: the runtime's +// mesh/state.delete takes no revision yet, so a write landing between that read and the delete (a few milliseconds) is +// deleted with it. For a step key that is a redelivered ask begun on this node in that moment: its running key leaves +// the steps state until its run writes its end, which its asker still reads (a watch takes puts only). +func ScanOf(s stdio.KeptState) Scan { + get := func(key string) (json.RawMessage, uint64, error) { + e, err := s.Get(key) + if err != nil || e == nil { + return nil, 0, err + } + return e.Value, e.Revision, nil + } + return Scan{Keys: s.Keys, Update: s.Update, Get: get, Delete: func(key string, revision uint64) error { + _, rev, err := get(key) + if err != nil { + return err + } + if rev != revision { + return stdio.ErrChanged + } + return s.Delete(key) + }} +} + +// isAsk says a step is a pulled ask's: its key is . and its tool is " ask" (a hand-started step +// has the tool's name). An ask's key is the stream's to settle, never a restart's: the puller leaves an ask it was +// running unacknowledged so the stream redelivers it, and the asker's wait takes the first end it reads on the key. +// A key a gone process owns is deleted, never ended (EndStale); one with no owner is ended only once the asker's bound +// has passed (StepBound), when it is nobody's. +func isAsk(key string, s Step) bool { + _, _, err := ParseAskStepKey(key) + return err == nil && strings.HasSuffix(s.Tool, " ask") +} + +// AskWhat says what an ask is, in one line: its kind, and its repository, head and levels, or its module and +// fingerprint. +func AskWhat(a Ask) string { + short := func(s string) string { + if len(s) > 12 { + return s[:12] + } + return s + } + str := func(m map[string]any, k string) string { v, _ := m[k].(string); return v } + parts := []string{a.Kind + " ask"} + switch { + case a.Check != nil: + parts = append(parts, str(a.Check.Fetch, "repository")+"@"+short(str(a.Check.Fetch, "commit")), "levels "+strings.Join(a.Check.Levels, ",")) + if a.Check.Run != "" { + parts = append(parts, "run "+a.Check.Run) + } + case a.Build != nil: + parts = append(parts, "module "+a.Build.Module) + if r := str(a.Build.Fetch, "repository"); r != "" { + parts = append(parts, r+"@"+short(str(a.Build.Fetch, "commit"))) + } + if a.Build.FP != "" { + parts = append(parts, "fp "+short(a.Build.FP)) + } + case a.Compose != nil: + parts = append(parts, "compose", str(a.Compose.Fetch, "repository")+"@"+short(str(a.Compose.Fetch, "commit"))) + } + return strings.Join(parts, ", ") +} + +// endRunning writes step ended failed with why, only while its key is still at revision. +func endRunning(scan Scan, key string, step Step, revision uint64, why string) error { + step.State, step.Reason, step.Ended, step.Result = StepFailed, why, now().UTC(), nil + raw, err := EncodeStep(step) + if err != nil { + return err + } + _, err = scan.Update(key, json.RawMessage(raw), revision) + return err +} + +// EndStale ends this node's running steps that no process of this module runs any more, and answers their ids. Only keys +// of node are read; a value that is no step is left alone. +// - A hand-started step with an owner (Step.Owner) that is not this process (Owner) is ended failed with +// RestartReason at once, whatever its age: its process is gone and nothing redelivers it, so nothing else would +// write its end (issue 1890, note 21). +// - A pulled ask's step with an owner that is not this process is deleted, not ended (novox/hq ADR 0525 §6): the +// stream redelivers an ask whose node stopped (the puller neither ends nor acknowledges one at its stop), and its +// asker takes the first end it reads, so an end written here would fail a check the redelivery would still run. Its +// asker's watch takes puts only and keeps waiting for the redelivery's key, its bound the backstop; the stale running +// key leaves the steps state, and with it the delivery view's and the checker's count of what runs (note 21: it +// held one of a build node's two slots). +// - A step owned by this process is left: it may run now. A redelivered ask begun on this node is owned by it. +// - A step with no owner (written by an older build) keeps the earlier rule: a hand-started step started before +// `before` (the start of this process) is ended; a pulled ask is left to the stream's redelivery (isAsk) until its +// start is older than StepBound, and then ended "past the step bound: its asker stopped waiting at