Merge b2/registry (b5a75693) into b7/registry

This commit is contained in:
2026-11-05 15:08:25 +01:00
15 changed files with 1045 additions and 127 deletions
@@ -0,0 +1,38 @@
package meshcatalogclient
import (
"crypto/sha256"
"encoding/hex"
"sort"
"strings"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
)
// FingerprintSet is the `fpset` of a candidate's pull requests: the first 12 hex digits of the sha256 over the lines
// `<module>=<fp2>` or `<module>=none:<reason>` of every module with an entry in a pull's fps, sorted and joined by a newline,
// so it is the same for every order and changes with any fingerprint or reason. A manifest digest does not enter: the
// fingerprint already covers the manifest. It is the empty string when no pull request carries a fingerprint. The checker
// keys its verdict by it.
func FingerprintSet(pulls []seat.CandidatesByGroupValuePullsItem) string {
lines := map[string]bool{}
for _, p := range pulls {
for module, m := range p.Fps {
if m.Fp2 != "" {
lines[module+"="+m.Fp2] = true
} else {
lines[module+"=none:"+m.None2] = true
}
}
}
if len(lines) == 0 {
return ""
}
all := make([]string, 0, len(lines))
for l := range lines {
all = append(all, l)
}
sort.Strings(all)
sum := sha256.Sum256([]byte(strings.Join(all, "\n")))
return hex.EncodeToString(sum[:])[:12]
}
@@ -0,0 +1,151 @@
package meshcatalogclient
import (
"errors"
"strings"
"testing"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys"
)
func hex64(c string) string { return strings.Repeat(c, 64) }
// The digest and the candidate's fingerprint set equal what the trunk's go/delivery computed for the same input (the
// expected strings are its output), so a key or a verdict written before is read after.
func TestDigestAndFingerprintSetKeepTheirForms(t *testing.T) {
if got := ManifestDigest([]byte(`{"module":"nats"}`)); got != "19a7450f557153c318558acac91cd6972321a192f6f95c3698a0e4c57edd4359" {
t.Errorf("ManifestDigest = %s", got)
}
pull := func(fps map[string]seat.CandidatesByGroupValuePullsItemFpsValue) []seat.CandidatesByGroupValuePullsItem {
return []seat.CandidatesByGroupValuePullsItem{{Fps: fps}}
}
fps := map[string]seat.CandidatesByGroupValuePullsItemFpsValue{"nats": {Fp2: SourcePrefix2 + hex64("a")}, "zsh": {None2: "no manifest"}}
set := FingerprintSet(pull(fps))
if set != "50a3f57cc4e2" {
t.Errorf("FingerprintSet = %s", set)
}
fps["zsh"] = seat.CandidatesByGroupValuePullsItemFpsValue{None2: "another reason"}
if FingerprintSet(pull(fps)) == set {
t.Error("a changed reason leaves the set as it was")
}
if FingerprintSet(pull(nil)) != "" {
t.Error("no fingerprint gives a set")
}
}
// A manifest and a module are read back as written, and each rule of the old values is kept: a refusal names its reason.
func TestManifestAndModuleValuesKeepTheirRules(t *testing.T) {
if _, err := ManifestsKey("abc"); err == nil || !strings.Contains(err.Error(), `"abc"`) {
t.Errorf("ManifestsKey(abc) = %v", err)
}
if k, err := ParseManifestsKey(hex64("b")); err != nil || k != hex64("b") {
t.Errorf("ParseManifestsKey = %q, %v", k, err)
}
raw, err := EncodeManifest(seat.ManifestsByDigestValue{Module: "nats", Manifest: `{"module":"nats"}`})
if err != nil {
t.Fatal(err)
}
if m, err := DecodeManifest(raw); err != nil || m.Module != "nats" || m.V != keys.Version {
t.Errorf("DecodeManifest = %+v, %v", m, err)
}
for _, bad := range []seat.ManifestsByDigestValue{{Module: "a.b", Manifest: "{}"}, {Module: "nats"}, {Module: "nats", Manifest: "not json"}, {Module: "nats", Manifest: "{\"a\":\"\xff\"}"}} {
if _, err := EncodeManifest(bad); err == nil {
t.Errorf("EncodeManifest(%+v) is accepted", bad)
}
}
if _, err := DecodeManifest([]byte(`{"module":"nats","manifest":"{}"}`)); !errors.Is(err, keys.ErrUnknownVersion) {
t.Errorf("a manifest with no version: %v", err)
}
good := seat.ModulesByModuleValue{Fp2: SourcePrefix2 + hex64("a"), Repository: "novox/mesh-catalog", Commit: "c", Manifest: hex64("b"), Path: RootPath}
raw, err = EncodeModule(good)
if err != nil {
t.Fatal(err)
}
if m, err := DecodeModule(raw); err != nil || m.Fp2 != good.Fp2 || m.Path != RootPath {
t.Errorf("DecodeModule = %+v, %v", m, err)
}
refused := good
refused.Verdict, refused.Problems = VerdictRefused, []string{"p"}
if _, err := EncodeModule(refused); err != nil {
t.Errorf("a refused module: %v", err)
}
for name, mutate := range map[string]func(m *seat.ModulesByModuleValue){
"neither a fingerprint nor a reason": func(m *seat.ModulesByModuleValue) { m.Fp2 = "" },
"both fp2 and none2": func(m *seat.ModulesByModuleValue) { m.None2 = "x" },
"an old fingerprint of another form": func(m *seat.ModulesByModuleValue) { m.Fp = "src2:" + hex64("a") },
"a fingerprint of the old key form": func(m *seat.ModulesByModuleValue) { m.Fp2 = "src1:" + hex64("a") },
"inputs and no manifest": func(m *seat.ModulesByModuleValue) {
m.Manifest = ""
m.Inputs2 = &seat.ModulesByModuleValueInputs2{Tree: "t"}
},
"a refusal with no problem": func(m *seat.ModulesByModuleValue) { m.Verdict = VerdictRefused },
"problems with no refusal": func(m *seat.ModulesByModuleValue) { m.Problems = []string{"p"} },
"unknown fields with no refusal": func(m *seat.ModulesByModuleValue) { m.Unknown = []string{"series"} },
"a path outside the directory": func(m *seat.ModulesByModuleValue) { m.Paths = map[string][]string{"tools": {"../x"}} },
"a toolchain with no recipe": func(m *seat.ModulesByModuleValue) {
m.Toolchains = map[string]seat.ToolchainUsed{"go": {Language: "go", Version: "1.26"}}
},
"an import form with no imports": func(m *seat.ModulesByModuleValue) {
m.Inputs2 = &seat.ModulesByModuleValueInputs2{Tree: "t", ImportForm: SourcePrefix2 + hex64("a")}
},
} {
m := good
mutate(&m)
if _, err := EncodeModule(m); err == nil {
t.Errorf("%s: EncodeModule accepts %+v", name, m)
}
}
}
// A verdict is taken as the readers have always taken it: a missing one accepted, a refusal held with its problems, a wait
// held with the fields it waits on, and a module's own verdict field gives the same.
func TestVerdictsAreTakenAsTheReadersTookThem(t *testing.T) {
if refused, problems := TakeVerdict(nil); refused || problems != nil {
t.Errorf("no verdict: %v %v", refused, problems)
}
waits := seat.VerdictsByDigestValue{Module: "nats", State: VerdictManifestWaits, Unknown: []string{"series"}, Judge: "j"}
refused, problems := TakeVerdict(&waits)
if !refused || len(problems) != 1 || problems[0] != "it carries series, which the indexer's judge does not know yet" {
t.Errorf("a wait: %v %v", refused, problems)
}
if got := VerdictWhy(waits); got != "the indexer's judge (j) holds the manifest of nats: "+problems[0] {
t.Errorf("VerdictWhy(wait) = %q", got)
}
if as := VerdictAsRefused(waits); as.State != VerdictRefused || len(as.Unknown) != 0 || as.Problems[0] != problems[0] {
t.Errorf("VerdictAsRefused = %+v", as)
}
no := seat.VerdictsByDigestValue{Module: "nats", State: VerdictRefused, Problems: []string{"a", "b"}, Judge: "j"}
if got := VerdictWhy(no); got != "the indexer's judge (j) refused the manifest of nats: a; b" {
t.Errorf("VerdictWhy(refused) = %q", got)
}
if !SameVerdict(no, seat.VerdictsByDigestValue{Module: "nats", State: VerdictRefused, Problems: []string{"a", "b"}, Judge: "k"}) {
t.Error("another judge is another verdict")
}
fromModule := ModuleVerdict("nats", seat.ModulesByModuleValue{Verdict: VerdictRefused, Unknown: []string{"series"}, Problems: []string{"x"}})
if fromModule == nil || fromModule.State != VerdictManifestWaits || fromModule.Unknown[0] != "series" {
t.Errorf("a refusal naming unknown fields is the wait it was written as: %+v", fromModule)
}
if ModuleVerdict("nats", seat.ModulesByModuleValue{}) != nil {
t.Error("a module with no verdict carries one")
}
for _, bad := range []seat.VerdictsByDigestValue{{Module: "nats", State: "accepted", Problems: []string{"p"}, Judge: "j"}, {Module: "nats", State: "refused", Judge: "j"}, {Module: "nats", State: "accepted"}, {Module: "nats", State: "odd", Judge: "j"}} {
if _, err := EncodeVerdict(bad); err == nil {
t.Errorf("EncodeVerdict(%+v) is accepted", bad)
}
}
raw, err := EncodeVerdict(no)
if err != nil {
t.Fatal(err)
}
if v, err := DecodeVerdict(raw); err != nil || !SameVerdict(v, no) {
t.Errorf("DecodeVerdict = %+v, %v", v, err)
}
if k, err := VerdictsKey(hex64("c")); err != nil || k != hex64("c") {
t.Errorf("VerdictsKey = %q %v", k, err)
}
if _, err := ModulesKey("a.b"); err == nil {
t.Error("ModulesKey accepts a dotted name")
}
}
@@ -2,7 +2,10 @@ module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef
require (
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog 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
@@ -0,0 +1,79 @@
package meshcatalogclient
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"unicode/utf8"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// ManifestDigest is the hex sha256 of a manifest's bytes exactly as read at the commit: the key of a `manifests` value and
// the digest a `modules` value and a candidate's module carry (novox/hq ADR 0429 clause 16).
func ManifestDigest(raw []byte) string {
sum := sha256.Sum256(raw)
return hex.EncodeToString(sum[:])
}
// ManifestsKey is the key of a `manifests` value: the manifest's digest, 64 lowercase hex digits. The indexer writes it
// once, by a create that fails when the key exists.
func ManifestsKey(digest string) (string, error) {
if !keys.IsHex64(digest) {
return "", fmt.Errorf("meshcatalogclient: %q is not a manifest digest, 64 lowercase hex digits", digest)
}
return seat.ManifestsByDigestKey(digest), nil
}
// ParseManifestsKey is the inverse of ManifestsKey.
func ParseManifestsKey(key string) (string, error) {
digest, err := ManifestsKey(key)
if err != nil {
return "", fmt.Errorf("meshcatalogclient: %q is not a manifests key: %w", key, err)
}
return digest, nil
}
func checkManifest(m seat.ManifestsByDigestValue) error {
if err := keys.CheckName("module", m.Module); err != nil {
return err
}
if m.Manifest == "" {
return fmt.Errorf("meshcatalogclient: the manifest of %s is empty", m.Module)
}
if !utf8.ValidString(m.Manifest) {
return fmt.Errorf("meshcatalogclient: the manifest of %s is not UTF-8 text, which a value cannot hold byte for byte", m.Module)
}
if !json.Valid([]byte(m.Manifest)) {
return fmt.Errorf("meshcatalogclient: the manifest of %s is not JSON", m.Module)
}
return nil
}
// EncodeManifest is m as JSON at the kept values' version; it refuses a module that is not a name and a manifest that is
// empty, not UTF-8 or not JSON.
func EncodeManifest(m seat.ManifestsByDigestValue) ([]byte, error) {
if err := checkManifest(m); err != nil {
return nil, err
}
m.V = keys.Version
return json.Marshal(m)
}
// DecodeManifest reads a `manifests` value; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeManifest
// refuses.
func DecodeManifest(data []byte) (seat.ManifestsByDigestValue, error) {
if err := keys.CheckVersion(data); err != nil {
return seat.ManifestsByDigestValue{}, err
}
var m seat.ManifestsByDigestValue
if err := json.Unmarshal(data, &m); err != nil {
return seat.ManifestsByDigestValue{}, fmt.Errorf("meshcatalogclient: value does not fit its shape: %w", err)
}
if err := checkManifest(m); err != nil {
return seat.ManifestsByDigestValue{}, err
}
return m, nil
}
@@ -0,0 +1,133 @@
package meshcatalogclient
import (
"encoding/json"
"fmt"
"path"
"strings"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// RootPath is a `modules` value's path when the module's manifest is its repository's root.
const RootPath = "."
// SourcePrefix1 opens the fingerprint of the old form a `modules` value may still carry in `fp`.
const SourcePrefix1 = "src1:"
// ModulesKey is the key of a `modules` value: the module's name.
func ModulesKey(module string) (string, error) {
if err := keys.CheckName("module", module); err != nil {
return "", err
}
return seat.ModulesByModuleKey(module), nil
}
// ParseModulesKey is the inverse of ModulesKey.
func ParseModulesKey(key string) (string, error) {
module, err := ModulesKey(key)
if err != nil {
return "", fmt.Errorf("meshcatalogclient: %q is not a modules key: %w", key, err)
}
return module, nil
}
// CheckModule refuses a `modules` value that contradicts itself: both or neither of a fingerprint and the reason for none,
// of either form, an old fingerprint not of the form `src1:`, kept inputs without a manifest digest, a verdict the indexer
// does not write, problems without a refusal (or a refusal without problems), a path that is not clean inside the module's
// directory, and a fingerprint or a digest not of its form.
func CheckModule(m seat.ModulesByModuleValue) error {
switch {
case m.Fp != "" && m.None != "":
return fmt.Errorf("meshcatalogclient: a module has both a fingerprint and a reason for none")
case m.Fp == "" && m.None == "" && m.Fp2 == "" && m.None2 == "":
return fmt.Errorf("meshcatalogclient: a module has neither a fingerprint nor a reason for none, of either form")
case m.Fp != "" && !strings.HasPrefix(m.Fp, SourcePrefix1):
return fmt.Errorf("meshcatalogclient: the module's fingerprint %q does not start with %s", m.Fp, SourcePrefix1)
case m.Fp2 != "" && m.None2 != "":
return fmt.Errorf("meshcatalogclient: a module has both fp2 and none2")
}
for language, t := range m.Toolchains {
if t.Version == "" || t.Recipe == "" {
return fmt.Errorf("meshcatalogclient: the module's toolchain of %q needs its version and its recipe version", language)
}
}
if m.Inputs2 != nil && m.Manifest == "" {
return fmt.Errorf("meshcatalogclient: a module keeps new-form inputs but has no manifest digest")
}
for artifact, paths := range m.Paths {
if artifact == "" || len(paths) == 0 {
return fmt.Errorf("meshcatalogclient: the module's named paths of the artifact %q are empty or name no artifact", artifact)
}
for _, p := range paths {
if !cleanInside(p) {
return fmt.Errorf("meshcatalogclient: the artifact %q names the path %q, not a clean path inside the module's directory", artifact, p)
}
}
}
if in := m.Inputs2; in != nil {
if f := in.ImportForm; f != "" && (len(in.Imports) == 0 || !strings.HasPrefix(f, SourcePrefix2) || !keys.IsHex64(f[len(SourcePrefix2):])) {
return fmt.Errorf("meshcatalogclient: the kept import form %q is set only with imports, and is %s and 64 hex digits", f, SourcePrefix2)
}
for p, object := range in.Paths {
if !cleanInside(p) {
return fmt.Errorf("meshcatalogclient: the named path %q is not a clean path inside the module's directory", p)
}
if object == "" {
return fmt.Errorf("meshcatalogclient: the named path %q names no git object", p)
}
}
}
switch {
case m.Verdict != "" && m.Verdict != VerdictAccepted && m.Verdict != VerdictRefused:
return fmt.Errorf("meshcatalogclient: the module's verdict %q is neither %s nor %s", m.Verdict, VerdictAccepted, VerdictRefused)
case len(m.Unknown) > 0 && m.Verdict != VerdictRefused:
return fmt.Errorf("meshcatalogclient: a module's unknown fields are named only with the refusal a wait is written as")
case m.Verdict != "" && m.Manifest == "":
return fmt.Errorf("meshcatalogclient: a module carries a verdict and no manifest digest")
case (m.Verdict == VerdictRefused) != (len(m.Problems) > 0):
return fmt.Errorf("meshcatalogclient: a module's problems are named only with a refusal, and a refusal names them")
case m.Verdict == VerdictRefused && len(m.Warnings) > 0:
return fmt.Errorf("meshcatalogclient: a refused module warns: a refusal names problems")
}
if m.Fp2 != "" {
if _, err := KeyPart(m.Fp2); err != nil {
return err
}
}
if m.Manifest != "" {
if _, err := ManifestsKey(m.Manifest); err != nil {
return err
}
}
return nil
}
func cleanInside(p string) bool {
return p != "" && p == path.Clean(p) && !strings.HasPrefix(p, "/") && p != ".." && !strings.HasPrefix(p, "../")
}
// EncodeModule is m as JSON at the kept values' version; it refuses what CheckModule refuses.
func EncodeModule(m seat.ModulesByModuleValue) ([]byte, error) {
if err := CheckModule(m); err != nil {
return nil, err
}
m.V = keys.Version
return json.Marshal(m)
}
// DecodeModule reads a `modules` value; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeModule refuses.
func DecodeModule(data []byte) (seat.ModulesByModuleValue, error) {
if err := keys.CheckVersion(data); err != nil {
return seat.ModulesByModuleValue{}, err
}
var m seat.ModulesByModuleValue
if err := json.Unmarshal(data, &m); err != nil {
return seat.ModulesByModuleValue{}, fmt.Errorf("meshcatalogclient: value does not fit its shape: %w", err)
}
if err := CheckModule(m); err != nil {
return seat.ModulesByModuleValue{}, err
}
return m, nil
}
@@ -0,0 +1,168 @@
package meshcatalogclient
import (
"encoding/json"
"fmt"
"slices"
"strings"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The states of a verdict on a manifest (novox/hq ADR 0578 §1): the indexer judges every manifest it takes in once and
// every other part takes the verdict from the seat's state `verdicts`.
const (
// VerdictAccepted is the state of a manifest the judge found no problem in.
VerdictAccepted = "accepted"
// VerdictRefused is the state of a manifest the judge found a problem in; the verdict's problems say each.
VerdictRefused = "refused"
// VerdictManifestWaits is the state of a manifest that carries a field the judge does not know: it is not refused,
// because a newer judge may know the field; the verdict's unknown names each. A reader holds the module as a refused
// one and keeps what runs. In a `modules` value it is written as a refusal naming the fields.
VerdictManifestWaits = "waits"
)
// VerdictsKey is the key of a `verdicts` value: the digest of the manifest it judges.
func VerdictsKey(digest string) (string, error) {
if !keys.IsHex64(digest) {
return "", fmt.Errorf("meshcatalogclient: %q is not a manifest digest, 64 lowercase hex digits", digest)
}
return seat.VerdictsByDigestKey(digest), nil
}
// VerdictRefuses says whether v refuses its manifest.
func VerdictRefuses(v seat.VerdictsByDigestValue) bool { return v.State == VerdictRefused }
// VerdictWaits says whether v says the manifest waits for a judge that knows its fields.
func VerdictWaits(v seat.VerdictsByDigestValue) bool { return v.State == VerdictManifestWaits }
// VerdictHeld says whether a reader holds the module as refused: the manifest is refused or waits.
func VerdictHeld(v seat.VerdictsByDigestValue) bool { return VerdictRefuses(v) || VerdictWaits(v) }
// WaitsOn says, in words, what a waiting manifest waits on.
func WaitsOn(v seat.VerdictsByDigestValue) string {
return "it carries " + strings.Join(v.Unknown, ", ") + ", which the indexer's judge does not know yet"
}
// TakeVerdict is how a reader takes the verdict on a manifest (ADR 0578 §3): held, with its problems, when the indexer
// wrote a refusal, or, for a manifest that waits, with the words that say which fields it waits on. A missing verdict (nil)
// is taken as accepted: every manifest the indexer holds before it has judged it was taken in under the judges that ran
// until then. A reader holds a refused module and names it with VerdictWhy, and reads the rest.
func TakeVerdict(v *seat.VerdictsByDigestValue) (refused bool, problems []string) {
if v == nil || !VerdictHeld(*v) {
return false, nil
}
if VerdictWaits(*v) {
return true, []string{WaitsOn(*v)}
}
return true, v.Problems
}
// VerdictWhy is a refusal in a reader's words: the module, that the indexer's judge refused its manifest, and each problem.
func VerdictWhy(v seat.VerdictsByDigestValue) string {
if !VerdictHeld(v) {
return ""
}
judge := "the indexer's judge"
if v.Judge != "" {
judge += " (" + v.Judge + ")"
}
if VerdictWaits(v) {
return fmt.Sprintf("%s holds the manifest of %s: %s", judge, v.Module, WaitsOn(v))
}
why := fmt.Sprintf("%s refused the manifest of %s:", judge, v.Module)
for _, p := range v.Problems {
why += " " + p + ";"
}
return why[:len(why)-1]
}
// VerdictAsRefused is v as a reader that does not know the waiting state takes it: a waiting verdict is a refusal naming what
// it waits on, any other verdict is v.
func VerdictAsRefused(v seat.VerdictsByDigestValue) seat.VerdictsByDigestValue {
if !VerdictWaits(v) {
return v
}
v.State, v.Problems, v.Unknown = VerdictRefused, []string{WaitsOn(v)}, nil
return v
}
// SameVerdict says whether v and o say the same of a manifest: the module, the state, the problems, the unknown fields and
// the warnings, whoever judged and when. The indexer writes a verdict only when it is not the same as the one it holds.
func SameVerdict(v, o seat.VerdictsByDigestValue) bool {
return v.Module == o.Module && v.State == o.State && slices.Equal(v.Problems, o.Problems) && slices.Equal(v.Unknown, o.Unknown) && slices.Equal(v.Warnings, o.Warnings)
}
func checkVerdict(v seat.VerdictsByDigestValue) error {
if err := keys.CheckName("module", v.Module); err != nil {
return err
}
if v.State != VerdictManifestWaits && len(v.Unknown) > 0 {
return fmt.Errorf("meshcatalogclient: the verdict on %s names unknown fields and does not wait", v.Module)
}
switch v.State {
case VerdictAccepted:
if len(v.Problems) > 0 {
return fmt.Errorf("meshcatalogclient: the verdict on %s accepts its manifest and names problems", v.Module)
}
case VerdictRefused:
if len(v.Problems) == 0 {
return fmt.Errorf("meshcatalogclient: the verdict on %s refuses its manifest and names no problem", v.Module)
}
if len(v.Warnings) > 0 {
return fmt.Errorf("meshcatalogclient: the verdict on %s refuses its manifest and warns: a refusal names problems", v.Module)
}
case VerdictManifestWaits:
if len(v.Unknown) == 0 || len(v.Problems) > 0 || len(v.Warnings) > 0 {
return fmt.Errorf("meshcatalogclient: the verdict on %s waits and names no unknown field, or names problems or warnings: a wait names the fields", v.Module)
}
default:
return fmt.Errorf("meshcatalogclient: the verdict on %s is %q, none of %s, %s and %s", v.Module, v.State, VerdictAccepted, VerdictRefused, VerdictManifestWaits)
}
if v.Judge == "" {
return fmt.Errorf("meshcatalogclient: the verdict on %s names no judge", v.Module)
}
return nil
}
// EncodeVerdict is v as JSON at the kept values' version; it refuses a verdict without a module or a judge, a state other
// than accepted, refused or waits, a refusal naming no problem and an acceptance naming one.
func EncodeVerdict(v seat.VerdictsByDigestValue) ([]byte, error) {
if err := checkVerdict(v); err != nil {
return nil, err
}
v.V = keys.Version
return json.Marshal(v)
}
// DecodeVerdict reads a `verdicts` value; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeVerdict
// refuses.
func DecodeVerdict(data []byte) (seat.VerdictsByDigestValue, error) {
if err := keys.CheckVersion(data); err != nil {
return seat.VerdictsByDigestValue{}, err
}
var v seat.VerdictsByDigestValue
if err := json.Unmarshal(data, &v); err != nil {
return seat.VerdictsByDigestValue{}, fmt.Errorf("meshcatalogclient: value does not fit its shape: %w", err)
}
if err := checkVerdict(v); err != nil {
return seat.VerdictsByDigestValue{}, err
}
return v, nil
}
// ModuleVerdict is the verdict a `modules` value carries, or nil when it carries none (taken as accepted, TakeVerdict). A
// refusal naming unknown fields is the wait it was written as.
func ModuleVerdict(module string, m seat.ModulesByModuleValue) *seat.VerdictsByDigestValue {
if m.Verdict == "" && len(m.Warnings) == 0 {
return nil
}
if m.Verdict == "" {
return &seat.VerdictsByDigestValue{Module: module, State: VerdictAccepted, Warnings: m.Warnings}
}
if m.Verdict == VerdictRefused && len(m.Unknown) > 0 {
return &seat.VerdictsByDigestValue{Module: module, State: VerdictManifestWaits, Unknown: m.Unknown}
}
return &seat.VerdictsByDigestValue{Module: module, State: m.Verdict, Problems: m.Problems}
}
+69 -24
View File
@@ -8,6 +8,7 @@ import (
"encoding/json"
"errors"
"strings"
"time"
)
// Seat is the seat's name.
@@ -310,12 +311,37 @@ func DecodePackagesByPackageSeriesValue(raw []byte) (PackagesByPackageSeriesValu
return v, nil
}
// ToolchainUsed is a toolchain a fingerprint was computed with (ADR 0405 clause 7).
type ToolchainUsed struct {
Language string `json:"language"`
// the build module's recipe version
Recipe string `json:"recipe"`
// the installed toolchain's version
Version string `json:"version"`
}
// ModulesInput is what modules takes.
type ModulesInput struct {
}
// ModulesAnswerModulesItem is a type of the contract.
type ModulesAnswerModulesItem struct {
// the merge commit it was last changed at
Commit string `json:"commit"`
// its fingerprint (src2: and 64 hex digits); absent when it has none
Fp string `json:"fp,omitempty"`
Module string `json:"module"`
// why it has none
None string `json:"none,omitempty"`
// the module's directory in its repository, '.' for a root
Path string `json:"path,omitempty"`
Repository string `json:"repository"`
}
// ModulesAnswer is what modules answers.
type ModulesAnswer = map[string]any
type ModulesAnswer struct {
Modules []ModulesAnswerModulesItem `json:"modules"`
}
// ModuleInput is what module takes.
type ModuleInput struct {
@@ -323,8 +349,26 @@ type ModuleInput struct {
Module string `json:"module"`
}
// ModuleAnswerModule is a type of the contract.
type ModuleAnswerModule struct {
// the merge commit it was last changed at
Commit string `json:"commit"`
// its fingerprint (src2: and 64 hex digits); absent when it has none
Fp string `json:"fp,omitempty"`
// the module's manifest as the trunk holds it
Manifest map[string]any `json:"manifest,omitempty"`
Module string `json:"module"`
// why it has none
None string `json:"none,omitempty"`
// the module's directory in its repository, '.' for a root
Path string `json:"path,omitempty"`
Repository string `json:"repository"`
}
// ModuleAnswer is what module answers.
type ModuleAnswer = map[string]any
type ModuleAnswer struct {
Module ModuleAnswerModule `json:"module"`
}
// ProvidesInput is what provides takes.
type ProvidesInput struct {
@@ -435,9 +479,10 @@ type ModulesByModuleValue struct {
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"`
Src int64 `json:"src,omitempty"`
// by language: the toolchain FP2 was computed with
Toolchains map[string]ToolchainUsed `json:"toolchains,omitempty"`
Unknown []string `json:"unknown,omitempty"`
// the layout version
V int64 `json:"v,omitempty"`
Verdict string `json:"verdict,omitempty"`
@@ -477,12 +522,12 @@ type CandidatesByGroupValue struct {
// VerdictsByDigestValue is one value of the state verdicts under the key <digest>.
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"`
At *time.Time `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"`
@@ -491,8 +536,8 @@ type VerdictsByDigestValue struct {
// 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"`
At *time.Time `json:"at,omitempty"`
Commit string `json:"commit"`
// the foundation's predefined list as kept
List any `json:"list"`
// the layout version
@@ -521,7 +566,7 @@ type ConfigCandidatesByPullValueDeclaresValueValue struct {
// ConfigCandidatesByPullValue is one value of the state config-candidates under the key <pull>.
type ConfigCandidatesByPullValue struct {
// when (RFC 3339 time)
At string `json:"at,omitempty"`
At *time.Time `json:"at,omitempty"`
// module to key to declaration
Declares map[string]map[string]ConfigCandidatesByPullValueDeclaresValueValue `json:"declares"`
Head string `json:"head"`
@@ -539,9 +584,9 @@ type PackagesByModuleValuePackagesItem struct {
// PackagesByModuleValue is one value of the state packages under the key <module>.
type PackagesByModuleValue struct {
// when (RFC 3339 time)
At string `json:"at,omitempty"`
Commit string `json:"commit"`
Dir string `json:"dir,omitempty"`
At *time.Time `json:"at,omitempty"`
Commit string `json:"commit"`
Dir string `json:"dir,omitempty"`
// the Go module path
Module string `json:"module"`
Packages []PackagesByModuleValuePackagesItem `json:"packages"`
@@ -561,9 +606,9 @@ type PackagesByPackageSeriesValueNewestContentsPackagesItem struct {
// 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"`
At *time.Time `json:"at,omitempty"`
Commit string `json:"commit"`
Dir string `json:"dir,omitempty"`
// the Go module path
Module string `json:"module"`
Packages []PackagesByPackageSeriesValueNewestContentsPackagesItem `json:"packages"`
@@ -592,9 +637,9 @@ type PackagesByPackageSeriesValueNumberedContentsPackagesItem struct {
// 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"`
At *time.Time `json:"at,omitempty"`
Commit string `json:"commit"`
Dir string `json:"dir,omitempty"`
// the Go module path
Module string `json:"module"`
Packages []PackagesByPackageSeriesValueNumberedContentsPackagesItem `json:"packages"`
@@ -617,7 +662,7 @@ type PackagesByPackageSeriesValueNumbered struct {
// PackagesByPackageSeriesValue is one value of the state packages under the key <package>.<series>.
type PackagesByPackageSeriesValue struct {
// when (RFC 3339 time)
At string `json:"at,omitempty"`
At *time.Time `json:"at,omitempty"`
GoModule string `json:"go-module,omitempty"`
Name string `json:"name"`
Newest *PackagesByPackageSeriesValueNewest `json:"newest,omitempty"`
@@ -17,13 +17,13 @@ func TestFixtures(t *testing.T) {
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}",
{name: "a manifest (xclip, read through mesh-catalog.module)", subject: "", given: "{\"manifest\":\"{\\\"build\\\":{\\\"artifacts\\\":[{\\\"binary\\\":\\\"xclip-tools\\\",\\\"from\\\":\\\"cmd/xclip-tools\\\",\\\"kind\\\":\\\"bundle\\\",\\\"language\\\":\\\"go\\\",\\\"loads\\\":[\\\"xclip-tools\\\"],\\\"name\\\":\\\"tools\\\",\\\"system\\\":\\\"arch\\\"}]},\\\"capabilities\\\":[\\\"package-manager\\\"],\\\"module\\\":\\\"xclip\\\",\\\"resources\\\":[{\\\"id\\\":\\\"package\\\",\\\"package\\\":\\\"xclip\\\",\\\"type\\\":\\\"package\\\"}],\\\"tools\\\":[\\\"xclip_copy\\\",\\\"xclip_paste\\\",\\\"xclip_targets\\\",\\\"xclip_session\\\"],\\\"version\\\":\\\"1\\\"}\",\"module\":\"xclip\",\"v\":1}", wire: "{\"manifest\":\"{\\\"build\\\":{\\\"artifacts\\\":[{\\\"binary\\\":\\\"xclip-tools\\\",\\\"from\\\":\\\"cmd/xclip-tools\\\",\\\"kind\\\":\\\"bundle\\\",\\\"language\\\":\\\"go\\\",\\\"loads\\\":[\\\"xclip-tools\\\"],\\\"name\\\":\\\"tools\\\",\\\"system\\\":\\\"arch\\\"}]},\\\"capabilities\\\":[\\\"package-manager\\\"],\\\"module\\\":\\\"xclip\\\",\\\"resources\\\":[{\\\"id\\\":\\\"package\\\",\\\"package\\\":\\\"xclip\\\",\\\"type\\\":\\\"package\\\"}],\\\"tools\\\":[\\\"xclip_copy\\\",\\\"xclip_paste\\\",\\\"xclip_targets\\\",\\\"xclip_session\\\"],\\\"version\\\":\\\"1\\\"}\",\"module\":\"xclip\",\"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}",
{name: "a module (xclip, read through mesh-catalog.modules; the inputs and toolchains no tool reads are not here)", subject: "", given: "{\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp2\":\"src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56\",\"path\":\"modules/xclip\",\"repository\":\"novox/mesh-catalog\",\"v\":1}", wire: "{\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp2\":\"src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56\",\"path\":\"modules/xclip\",\"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}",
{name: "a refused verdict (nats, read through the indexer's verdicts tool)", subject: "", given: "{\"at\":\"2026-11-04T18:51:12.614583047+01:00\",\"judge\":\"exe:661e6fb424d7ffea\",\"module\":\"nats\",\"problems\":[\"this is not a module manifest: a state is either a name or {name, history, ttl-seconds, per-machine, key-ttl}: json: unknown field \\\"read-only\\\" (read without state[2].read-only)\"],\"state\":\"refused\"}", wire: "{\"at\":\"2026-11-04T18:51:12.614583047+01:00\",\"judge\":\"exe:661e6fb424d7ffea\",\"module\":\"nats\",\"problems\":[\"this is not a module manifest: a state is either a name or {name, history, ttl-seconds, per-machine, key-ttl}: json: unknown field \\\"read-only\\\" (read without state[2].read-only)\"],\"state\":\"refused\"}",
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},
@@ -41,6 +41,12 @@ func TestFixtures(t *testing.T) {
err := json.Unmarshal(b, &v)
return v, err
}, subjectIs: nil},
{name: "a module with its toolchain", subject: "", given: "{\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp2\":\"src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56\",\"repository\":\"novox/mesh-catalog\",\"toolchains\":{\"go\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"v\":1}", wire: "{\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp2\":\"src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56\",\"repository\":\"novox/mesh-catalog\",\"toolchains\":{\"go\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"v\":1}",
decode: func(b []byte) (any, error) { var v ModulesByModuleValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "the modules verb (captured: the first three modules)", subject: "", given: "{\"modules\":[{\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp\":\"src2:5e4a1b4ad4ea6175c05e4655ce58aeb957547149d4c9805eb27636efd50c1c87\",\"module\":\"adwaita\",\"path\":\"modules/adwaita\",\"repository\":\"novox/mesh-catalog\"},{\"commit\":\"b112adcecc41d18f54190cbd2fc864199b70c518\",\"fp\":\"src2:7dd0db8e98224f46121a5ae05c0b9c5a430eff531a5120aa40b1a59237853a3f\",\"module\":\"amqp-email-forwarder\",\"path\":\".\",\"repository\":\"jschoubben/amqp-email-forwarder\"},{\"commit\":\"dd458cb155f5f5fa3ed824a23728e444be5b156a\",\"fp\":\"src2:a98f8152f0a837dd5ba883a15efaf110ba88100193157e4bdb1a93ba55209371\",\"module\":\"artifact-store-tools\",\"path\":\"modules/artifact-store-tools\",\"repository\":\"novox/mesh-catalog\"}]}", wire: "{\"modules\":[{\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp\":\"src2:5e4a1b4ad4ea6175c05e4655ce58aeb957547149d4c9805eb27636efd50c1c87\",\"module\":\"adwaita\",\"path\":\"modules/adwaita\",\"repository\":\"novox/mesh-catalog\"},{\"commit\":\"b112adcecc41d18f54190cbd2fc864199b70c518\",\"fp\":\"src2:7dd0db8e98224f46121a5ae05c0b9c5a430eff531a5120aa40b1a59237853a3f\",\"module\":\"amqp-email-forwarder\",\"path\":\".\",\"repository\":\"jschoubben/amqp-email-forwarder\"},{\"commit\":\"dd458cb155f5f5fa3ed824a23728e444be5b156a\",\"fp\":\"src2:a98f8152f0a837dd5ba883a15efaf110ba88100193157e4bdb1a93ba55209371\",\"module\":\"artifact-store-tools\",\"path\":\"modules/artifact-store-tools\",\"repository\":\"novox/mesh-catalog\"}]}",
decode: func(b []byte) (any, error) { var v ModulesAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "the module verb (captured: xclip)", subject: "", given: "{\"module\":{\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp\":\"src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56\",\"manifest\":{\"build\":{\"artifacts\":[{\"binary\":\"xclip-tools\",\"from\":\"cmd/xclip-tools\",\"kind\":\"bundle\",\"language\":\"go\",\"loads\":[\"xclip-tools\"],\"name\":\"tools\",\"system\":\"arch\"}]},\"capabilities\":[\"package-manager\"],\"module\":\"xclip\",\"resources\":[{\"id\":\"package\",\"package\":\"xclip\",\"type\":\"package\"}],\"tools\":[\"xclip_copy\",\"xclip_paste\",\"xclip_targets\",\"xclip_session\"],\"version\":\"1\"},\"module\":\"xclip\",\"path\":\"modules/xclip\",\"repository\":\"novox/mesh-catalog\"}}", wire: "{\"module\":{\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp\":\"src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56\",\"manifest\":{\"build\":{\"artifacts\":[{\"binary\":\"xclip-tools\",\"from\":\"cmd/xclip-tools\",\"kind\":\"bundle\",\"language\":\"go\",\"loads\":[\"xclip-tools\"],\"name\":\"tools\",\"system\":\"arch\"}]},\"capabilities\":[\"package-manager\"],\"module\":\"xclip\",\"resources\":[{\"id\":\"package\",\"package\":\"xclip\",\"type\":\"package\"}],\"tools\":[\"xclip_copy\",\"xclip_paste\",\"xclip_targets\",\"xclip_session\"],\"version\":\"1\"},\"module\":\"xclip\",\"path\":\"modules/xclip\",\"repository\":\"novox/mesh-catalog\"}}",
decode: func(b []byte) (any, error) { var v ModuleAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
}
for _, c := range cases {
v, err := c.decode([]byte(c.given))
+25 -24
View File
@@ -8,6 +8,7 @@ import (
"encoding/json"
"errors"
"strings"
"time"
)
// Seat is the seat's name.
@@ -213,18 +214,18 @@ type BuiltArtifact struct {
// 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"`
At *time.Time `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"`
@@ -318,9 +319,9 @@ type LevelAnswer struct {
// 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"`
At *time.Time `json:"at,omitempty"`
By string `json:"by,omitempty"`
Paused bool `json:"paused"`
// the layout version
V int64 `json:"v,omitempty"`
}
@@ -328,17 +329,17 @@ type Pause struct {
// 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"`
Ended *time.Time `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"`
PhaseAt *time.Time `json:"phase-at,omitempty"`
Reason string `json:"reason,omitempty"`
Result *StepResult `json:"result,omitempty"`
// when (RFC 3339 time)
Started string `json:"started"`
Started time.Time `json:"started"`
// running, passed, failed or refused
State string `json:"state"`
Tool string `json:"tool"`
@@ -383,7 +384,7 @@ type StepAskCompose struct {
type StepAsk struct {
Asker string `json:"asker"`
// when (RFC 3339 time)
At string `json:"at,omitempty"`
At *time.Time `json:"at,omitempty"`
Build *StepAskBuild `json:"build,omitempty"`
Check *StepAskCheck `json:"check,omitempty"`
Compose *StepAskCompose `json:"compose,omitempty"`
@@ -422,7 +423,7 @@ type StepResult struct {
// Toolchain is the toolchain a node reports.
type Toolchain struct {
// when (RFC 3339 time)
At string `json:"at,omitempty"`
At *time.Time `json:"at,omitempty"`
// go or typescript
Language string `json:"language"`
Recipe string `json:"recipe"`
+25 -24
View File
@@ -8,6 +8,7 @@ import (
"encoding/json"
"errors"
"strings"
"time"
)
// Seat is the seat's name.
@@ -213,18 +214,18 @@ type BuiltArtifact struct {
// 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"`
At *time.Time `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"`
@@ -318,9 +319,9 @@ type LevelAnswer struct {
// 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"`
At *time.Time `json:"at,omitempty"`
By string `json:"by,omitempty"`
Paused bool `json:"paused"`
// the layout version
V int64 `json:"v,omitempty"`
}
@@ -328,17 +329,17 @@ type Pause struct {
// 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"`
Ended *time.Time `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"`
PhaseAt *time.Time `json:"phase-at,omitempty"`
Reason string `json:"reason,omitempty"`
Result *StepResult `json:"result,omitempty"`
// when (RFC 3339 time)
Started string `json:"started"`
Started time.Time `json:"started"`
// running, passed, failed or refused
State string `json:"state"`
Tool string `json:"tool"`
@@ -383,7 +384,7 @@ type StepAskCompose struct {
type StepAsk struct {
Asker string `json:"asker"`
// when (RFC 3339 time)
At string `json:"at,omitempty"`
At *time.Time `json:"at,omitempty"`
Build *StepAskBuild `json:"build,omitempty"`
Check *StepAskCheck `json:"check,omitempty"`
Compose *StepAskCompose `json:"compose,omitempty"`
@@ -422,7 +423,7 @@ type StepResult struct {
// Toolchain is the toolchain a node reports.
type Toolchain struct {
// when (RFC 3339 time)
At string `json:"at,omitempty"`
At *time.Time `json:"at,omitempty"`
// go or typescript
Language string `json:"language"`
Recipe string `json:"recipe"`
+25 -24
View File
@@ -8,6 +8,7 @@ import (
"encoding/json"
"errors"
"strings"
"time"
)
// Seat is the seat's name.
@@ -213,18 +214,18 @@ type BuiltArtifact struct {
// 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"`
At *time.Time `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"`
@@ -318,9 +319,9 @@ type LevelAnswer struct {
// 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"`
At *time.Time `json:"at,omitempty"`
By string `json:"by,omitempty"`
Paused bool `json:"paused"`
// the layout version
V int64 `json:"v,omitempty"`
}
@@ -328,17 +329,17 @@ type Pause struct {
// 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"`
Ended *time.Time `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"`
PhaseAt *time.Time `json:"phase-at,omitempty"`
Reason string `json:"reason,omitempty"`
Result *StepResult `json:"result,omitempty"`
// when (RFC 3339 time)
Started string `json:"started"`
Started time.Time `json:"started"`
// running, passed, failed or refused
State string `json:"state"`
Tool string `json:"tool"`
@@ -383,7 +384,7 @@ type StepAskCompose struct {
type StepAsk struct {
Asker string `json:"asker"`
// when (RFC 3339 time)
At string `json:"at,omitempty"`
At *time.Time `json:"at,omitempty"`
Build *StepAskBuild `json:"build,omitempty"`
Check *StepAskCheck `json:"check,omitempty"`
Compose *StepAskCompose `json:"compose,omitempty"`
@@ -422,7 +423,7 @@ type StepResult struct {
// Toolchain is the toolchain a node reports.
type Toolchain struct {
// when (RFC 3339 time)
At string `json:"at,omitempty"`
At *time.Time `json:"at,omitempty"`
// go or typescript
Language string `json:"language"`
Recipe string `json:"recipe"`
+108 -10
View File
@@ -1,21 +1,22 @@
[
{
"name": "a manifest",
"name": "a manifest (xclip, read through mesh-catalog.module)",
"of": "state:manifests/<digest>",
"given": {
"v": 1,
"module": "nats",
"manifest": "{\"module\":\"nats\"}"
"module": "xclip",
"manifest": "{\"build\":{\"artifacts\":[{\"binary\":\"xclip-tools\",\"from\":\"cmd/xclip-tools\",\"kind\":\"bundle\",\"language\":\"go\",\"loads\":[\"xclip-tools\"],\"name\":\"tools\",\"system\":\"arch\"}]},\"capabilities\":[\"package-manager\"],\"module\":\"xclip\",\"resources\":[{\"id\":\"package\",\"package\":\"xclip\",\"type\":\"package\"}],\"tools\":[\"xclip_copy\",\"xclip_paste\",\"xclip_targets\",\"xclip_session\"],\"version\":\"1\"}"
}
},
{
"name": "a module",
"name": "a module (xclip, read through mesh-catalog.modules; the inputs and toolchains no tool reads are not here)",
"of": "state:modules/<module>",
"given": {
"v": 1,
"repository": "novox/mesh-catalog",
"commit": "cccccccccccccccccccccccccccccccccccccccc",
"fp2": "src2:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
"commit": "f469eb387a3015569d9b16c66ed418e9dd098f64",
"path": "modules/xclip",
"fp2": "src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56"
}
},
{
@@ -39,13 +40,16 @@
}
},
{
"name": "a verdict",
"name": "a refused verdict (nats, read through the indexer's verdicts tool)",
"of": "state:verdicts/<digest>",
"given": {
"v": 1,
"module": "nats",
"state": "accepted",
"judge": "trunk"
"state": "refused",
"problems": [
"this is not a module manifest: a state is either a name or {name, history, ttl-seconds, per-machine, key-ttl}: json: unknown field \"read-only\" (read without state[2].read-only)"
],
"judge": "exe:661e6fb424d7ffea",
"at": "2026-11-04T18:51:12.614583047+01:00"
}
},
{
@@ -101,5 +105,99 @@
"name": "go-keys",
"series": "0.1"
}
},
{
"name": "a module with its toolchain",
"of": "state:modules/<module>",
"given": {
"v": 1,
"repository": "novox/mesh-catalog",
"commit": "f469eb387a3015569d9b16c66ed418e9dd098f64",
"fp2": "src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56",
"toolchains": {
"go": {
"language": "go",
"version": "1.26.0",
"recipe": "1"
}
}
}
},
{
"name": "the modules verb (captured: the first three modules)",
"of": "verb:modules.answer",
"given": {
"modules": [
{
"commit": "f469eb387a3015569d9b16c66ed418e9dd098f64",
"fp": "src2:5e4a1b4ad4ea6175c05e4655ce58aeb957547149d4c9805eb27636efd50c1c87",
"module": "adwaita",
"path": "modules/adwaita",
"repository": "novox/mesh-catalog"
},
{
"commit": "b112adcecc41d18f54190cbd2fc864199b70c518",
"fp": "src2:7dd0db8e98224f46121a5ae05c0b9c5a430eff531a5120aa40b1a59237853a3f",
"module": "amqp-email-forwarder",
"path": ".",
"repository": "jschoubben/amqp-email-forwarder"
},
{
"commit": "dd458cb155f5f5fa3ed824a23728e444be5b156a",
"fp": "src2:a98f8152f0a837dd5ba883a15efaf110ba88100193157e4bdb1a93ba55209371",
"module": "artifact-store-tools",
"path": "modules/artifact-store-tools",
"repository": "novox/mesh-catalog"
}
]
}
},
{
"name": "the module verb (captured: xclip)",
"of": "verb:module.answer",
"given": {
"module": {
"commit": "f469eb387a3015569d9b16c66ed418e9dd098f64",
"fp": "src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56",
"manifest": {
"build": {
"artifacts": [
{
"binary": "xclip-tools",
"from": "cmd/xclip-tools",
"kind": "bundle",
"language": "go",
"loads": [
"xclip-tools"
],
"name": "tools",
"system": "arch"
}
]
},
"capabilities": [
"package-manager"
],
"module": "xclip",
"resources": [
{
"id": "package",
"package": "xclip",
"type": "package"
}
],
"tools": [
"xclip_copy",
"xclip_paste",
"xclip_targets",
"xclip_session"
],
"version": "1"
},
"module": "xclip",
"path": "modules/xclip",
"repository": "novox/mesh-catalog"
}
}
}
]
+127 -14
View File
@@ -16,7 +16,47 @@
},
"answer": {
"type": "object",
"description": "what modules answers, as its description says"
"description": "every module the trunk holds, by name",
"properties": {
"modules": {
"type": "array",
"items": {
"type": "object",
"properties": {
"module": {
"type": "string"
},
"repository": {
"type": "string"
},
"path": {
"type": "string",
"description": "the module's directory in its repository, '.' for a root"
},
"commit": {
"type": "string",
"description": "the merge commit it was last changed at"
},
"fp": {
"type": "string",
"description": "its fingerprint (src2: and 64 hex digits); absent when it has none"
},
"none": {
"type": "string",
"description": "why it has none"
}
},
"required": [
"module",
"repository",
"commit"
]
}
}
},
"required": [
"modules"
]
}
},
{
@@ -37,7 +77,48 @@
},
"answer": {
"type": "object",
"description": "what module answers, as its description says"
"description": "the module whole",
"properties": {
"module": {
"type": "object",
"properties": {
"module": {
"type": "string"
},
"repository": {
"type": "string"
},
"path": {
"type": "string",
"description": "the module's directory in its repository, '.' for a root"
},
"commit": {
"type": "string",
"description": "the merge commit it was last changed at"
},
"fp": {
"type": "string",
"description": "its fingerprint (src2: and 64 hex digits); absent when it has none"
},
"none": {
"type": "string",
"description": "why it has none"
},
"manifest": {
"type": "object",
"description": "the module's manifest as the trunk holds it"
}
},
"required": [
"module",
"repository",
"commit"
]
}
},
"required": [
"module"
]
}
},
{
@@ -84,7 +165,7 @@
},
{
"name": "seat-definitions",
"description": "The seat list (ADR 0636 §4): with no seat, every trunk seat with its digest and the revision of the list; with a seat, that trunk definition resolved with the parts it includes. Reads only.",
"description": "The seat list (ADR 0636 \u00a74): with no seat, every trunk seat with its digest and the revision of the list; with a seat, that trunk definition resolved with the parts it includes. Reads only.",
"optional": true,
"input": {
"properties": {
@@ -163,7 +244,7 @@
}
}
],
"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).",
"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 \u00a71, \u00a74).",
"family": {
"name": "mesh-catalog.events",
"max-age-days": 7
@@ -344,8 +425,9 @@
"toolchains": {
"type": "object",
"additionalProperties": {
"description": "toolchain used"
}
"$ref": "#/defs/ToolchainUsed"
},
"description": "by language: the toolchain FP2 was computed with"
},
"paths": {
"type": "object",
@@ -543,7 +625,8 @@
},
"at": {
"type": "string",
"description": "when (RFC 3339 time)"
"description": "when (RFC 3339 time)",
"format": "date-time"
}
},
"required": [
@@ -576,7 +659,8 @@
},
"at": {
"type": "string",
"description": "when (RFC 3339 time)"
"description": "when (RFC 3339 time)",
"format": "date-time"
},
"list": {
"description": "the foundation's predefined list as kept"
@@ -670,7 +754,8 @@
},
"at": {
"type": "string",
"description": "when (RFC 3339 time)"
"description": "when (RFC 3339 time)",
"format": "date-time"
}
},
"required": [
@@ -735,7 +820,8 @@
},
"at": {
"type": "string",
"description": "when (RFC 3339 time)"
"description": "when (RFC 3339 time)",
"format": "date-time"
}
},
"required": [
@@ -830,7 +916,8 @@
},
"at": {
"type": "string",
"description": "when (RFC 3339 time)"
"description": "when (RFC 3339 time)",
"format": "date-time"
}
},
"required": [
@@ -904,7 +991,8 @@
},
"at": {
"type": "string",
"description": "when (RFC 3339 time)"
"description": "when (RFC 3339 time)",
"format": "date-time"
}
},
"required": [
@@ -921,7 +1009,8 @@
},
"at": {
"type": "string",
"description": "when (RFC 3339 time)"
"description": "when (RFC 3339 time)",
"format": "date-time"
}
},
"required": [
@@ -935,5 +1024,29 @@
"description": "the published packages",
"history": 5
}
]
],
"defs": {
"ToolchainUsed": {
"type": "object",
"description": "a toolchain a fingerprint was computed with (ADR 0405 clause 7)",
"properties": {
"language": {
"type": "string"
},
"version": {
"type": "string",
"description": "the installed toolchain's version"
},
"recipe": {
"type": "string",
"description": "the build module's recipe version"
}
},
"required": [
"language",
"version",
"recipe"
]
}
}
}
+45 -3
View File
@@ -156,12 +156,36 @@ export function parsePackagesByPackageSeriesKey(key: string): { packagePart: str
return { packagePart, series };
}
/** a toolchain a fingerprint was computed with (ADR 0405 clause 7) */
export interface ToolchainUsed {
"language": string;
/** the build module's recipe version */
"recipe": string;
/** the installed toolchain's version */
"version": string;
}
/** is what modules takes. */
export interface ModulesInput {
}
export interface ModulesAnswerModulesItem {
/** the merge commit it was last changed at */
"commit": string;
/** its fingerprint (src2: and 64 hex digits); absent when it has none */
"fp"?: string;
"module": string;
/** why it has none */
"none"?: string;
/** the module's directory in its repository, '.' for a root */
"path"?: string;
"repository": string;
}
/** is what modules answers. */
export type ModulesAnswer = Record<string, unknown>;
export interface ModulesAnswer {
"modules": ModulesAnswerModulesItem[];
}
/** is what module takes. */
export interface ModuleInput {
@@ -169,8 +193,25 @@ export interface ModuleInput {
"module": string;
}
export interface ModuleAnswerModule {
/** the merge commit it was last changed at */
"commit": string;
/** its fingerprint (src2: and 64 hex digits); absent when it has none */
"fp"?: string;
/** the module's manifest as the trunk holds it */
"manifest"?: Record<string, unknown>;
"module": string;
/** why it has none */
"none"?: string;
/** the module's directory in its repository, '.' for a root */
"path"?: string;
"repository": string;
}
/** is what module answers. */
export type ModuleAnswer = Record<string, unknown>;
export interface ModuleAnswer {
"module": ModuleAnswerModule;
}
/** is what provides takes. */
export interface ProvidesInput {
@@ -279,7 +320,8 @@ export interface ModulesByModuleValue {
"repository": string;
/** the source revision */
"src"?: number;
"toolchains"?: Record<string, unknown>;
/** by language: the toolchain FP2 was computed with */
"toolchains"?: Record<string, ToolchainUsed>;
"unknown"?: string[];
/** the layout version */
"v"?: number;
+39
View File
@@ -15,6 +15,7 @@ import (
// TestGraphEdges: the module graph's cycle, dependents and providers, each from the edges alone (ADR 0603 §2, ADR 0604 §1).
func TestGraphEdges(t *testing.T) {
t.Run("a node's value", nodeValues)
t.Run("a reader of the seat list waits for its holder", readersOfTheSeatList)
fp := "src2:" + strings.Repeat("b", 64)
node := func(repository, path string, edit func(*Node)) Node {
n := Node{FP: fp, Repository: repository, Path: path}
@@ -252,3 +253,41 @@ func nodeValues(t *testing.T) {
}
}
}
// readersOfTheSeatList (D-31): a module whose manifest invokes the seat mesh-catalog's seat-definitions waits, by the verb
// edge, for the module that claims the seat and serves the verb, so the delivery plan puts the holder first (ADR 0614 §2); a
// foundation part the holder itself invokes lies on a cycle with it and the edge is left out, so the part is carried by the
// bootstrapped three until the holder's new build runs (ADR 0636 §1).
func readersOfTheSeatList(t *testing.T) {
const list = "seat:mesh-catalog.seat-definitions"
fp := "src2:" + strings.Repeat("c", 64)
node := func(edit func(*Node)) Node {
n := Node{FP: fp, Repository: "novox/mesh-delivery", Path: "modules/x"}
edit(&n)
return n
}
g := Graph{
"indexer": node(func(n *Node) {
n.Serves = []string{list, "seat:mesh-catalog.modules"}
n.Invokes = []string{"seat:node-engine.composed"}
}),
"checker": node(func(n *Node) { n.Invokes = []string{list} }),
"node-agent": node(func(n *Node) { n.Invokes = []string{list}; n.Serves = []string{"seat:node-engine.composed"} }),
}
if got := g.Servers(list); !reflect.DeepEqual(got, []string{"indexer"}) {
t.Errorf("servers of the seat list = %v", got)
}
if got := g.Before("checker"); !reflect.DeepEqual(got, []string{"indexer"}) {
t.Errorf("the checker waits for %v, want the indexer", got)
}
g.LeaveOut()
if got := g.Before("checker"); !reflect.DeepEqual(got, []string{"indexer"}) {
t.Errorf("after the cycles are left out the checker waits for %v, want the indexer", got)
}
if got := g["node-agent"].LeftOut; !reflect.DeepEqual(got, []string{list}) {
t.Errorf("the node-agent leaves out %v, want the seat list (a cycle with the holder)", got)
}
if got := g.Before("node-agent"); len(got) != 0 {
t.Errorf("the node-agent waits for %v, want nothing: the bootstrap carries it", got)
}
}