diff --git a/.gitignore b/.gitignore index 420d8ac7..d90883df 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,9 @@ vendor/ # A compiled program never enters a commit (issue 1577): `go build .` in a command directory leaves one there. modules/*/cmd/*/mesh-* !modules/*/cmd/*/mesh-*.go +# `go build .` at a module's root leaves its program there too (the view's, met a second time). +modules/*/mesh-* +# The delivery SDK's commands (design 47 G §464), built in their own directory or at the SDK's root. +/modules/mesh-delivery-sdk/cmd/seatgen/seatgen +/modules/mesh-delivery-sdk/seatgen/seatgen +/modules/mesh-delivery-sdk/mesh-pack diff --git a/contracts/go/clients/meshcatalogclient/fingerprint_test.go b/contracts/go/clients/meshcatalogclient/fingerprint_test.go deleted file mode 100644 index ec575e0a..00000000 --- a/contracts/go/clients/meshcatalogclient/fingerprint_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package meshcatalogclient - -import ( - "strings" - "testing" -) - -// The key part holds only bus key characters and parsing gives the fingerprint back; a src1 or malformed value is refused, -// naming it. -func TestFingerprintKeyPart(t *testing.T) { - fp := SourcePrefix2 + strings.Repeat("ab", 32) - part, err := KeyPart(fp) - if err != nil { - t.Fatal(err) - } - if len(part) != 64 || strings.Trim(part, "0123456789abcdef") != "" { - t.Errorf("key part %q is not 64 lowercase hex characters", part) - } - back, err := ParseKeyPart(part) - if err != nil || back != fp { - t.Errorf("parse(%q) = %q, %v; want %q", part, back, err, fp) - } - src1 := "src1:" + strings.Repeat("c", 64) - for _, bad := range []string{src1, "src2:", "src2:xyz", "", "src2:" + strings.Repeat("A", 64), "src2:" + strings.Repeat("a", 63)} { - if _, err := KeyPart(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") { - t.Errorf("KeyPart(%q) = %v, want a refusal naming it", bad, err) - } - } - for _, bad := range []string{src1, "src2:" + strings.Repeat("a", 64), strings.Repeat("a", 63), strings.Repeat("A", 64), "", "zz"} { - if _, err := ParseKeyPart(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") { - t.Errorf("ParseKeyPart(%q) = %v, want a refusal naming it", bad, err) - } - } -} diff --git a/contracts/go/clients/meshcatalogclient/go.mod b/contracts/go/clients/meshcatalogclient/go.mod deleted file mode 100644 index 450b54a2..00000000 --- a/contracts/go/clients/meshcatalogclient/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient - -go 1.26.0 - -require ( - git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000 - git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 -) diff --git a/contracts/go/clients/meshdeliverycheckerclient/go.mod b/contracts/go/clients/meshdeliverycheckerclient/go.mod deleted file mode 100644 index ba6062bf..00000000 --- a/contracts/go/clients/meshdeliverycheckerclient/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient - -go 1.26.0 - -require ( - git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker v0.0.0-00010101000000-000000000000 - git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 -) diff --git a/contracts/go/clients/meshdeliveryclient/go.mod b/contracts/go/clients/meshdeliveryclient/go.mod deleted file mode 100644 index 7b85ffc7..00000000 --- a/contracts/go/clients/meshdeliveryclient/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient - -go 1.26.0 - -require ( - git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000 - git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 -) diff --git a/contracts/go/clients/meshdeliveryclient/removed.go b/contracts/go/clients/meshdeliveryclient/removed.go deleted file mode 100644 index 8b390133..00000000 --- a/contracts/go/clients/meshdeliveryclient/removed.go +++ /dev/null @@ -1,18 +0,0 @@ -package meshdeliveryclient - -// RemovedPrefix opens the description of a verb release 6 removed from the seat mesh-delivery (novox/hq ADR 0539 clause 17): -// the definition keeps it as an optional row with no input, so the holder of before and the holder of release 6 both hold -// the seat, and a caller is answered by name with its replacement. -const RemovedPrefix = "Removed in R6 (ADR 0539): refused by name, naming its replacement: " - -// RemovedVerbs are the seat's verbs release 6 removed, each with its replacement in words: with no plan the gate keeps the -// merge's states and derives nothing after the merge. A new map each call. -func RemovedVerbs() map[string]string { - return map[string]string{ - "times": "the delivery view (nox-delivery): each machine's want and have, and since when", - "what-if": "the merge check's composition, which composes every machine a change touches", - "release": "the assigner's asks (hold, restore, backup, record), answered on the phone or in the delivery view", - "stop": "a bad fingerprint is held and its canary set back by the assigner (ADR 0429 §6, ADR 0539 clause 18)", - "retire-history": "nothing to retire: the gate keeps no delivery after its merge", - } -} diff --git a/contracts/go/clients/meshwantsclient/go.mod b/contracts/go/clients/meshwantsclient/go.mod deleted file mode 100644 index 470f8994..00000000 --- a/contracts/go/clients/meshwantsclient/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient - -go 1.26.0 - -require ( - git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants v0.0.0-00010101000000-000000000000 - git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 -) diff --git a/contracts/go/seats/containerimageregistry/go.mod b/contracts/go/seats/containerimageregistry/go.mod deleted file mode 100644 index 26ccf02f..00000000 --- a/contracts/go/seats/containerimageregistry/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry - -go 1.26.0 diff --git a/contracts/go/seats/gopackageregistry/go.mod b/contracts/go/seats/gopackageregistry/go.mod deleted file mode 100644 index 065f758b..00000000 --- a/contracts/go/seats/gopackageregistry/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry - -go 1.26.0 diff --git a/contracts/go/seats/meshartifactsweep/go.mod b/contracts/go/seats/meshartifactsweep/go.mod deleted file mode 100644 index 77f0649b..00000000 --- a/contracts/go/seats/meshartifactsweep/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep - -go 1.26.0 diff --git a/contracts/go/seats/meshcatalog/contract.go b/contracts/go/seats/meshcatalog/contract.go deleted file mode 100644 index 32870c7c..00000000 --- a/contracts/go/seats/meshcatalog/contract.go +++ /dev/null @@ -1,717 +0,0 @@ -// 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" - "time" -) - -// Seat is the seat's name. -const Seat = "mesh-catalog" - -const ( - VerbModules = "modules" - VerbModule = "module" - VerbProvides = "provides" - VerbDependents = "dependents" - VerbSeatDefinitions = "seat-definitions" -) - -const ( - EventSeatDefinitionChanged = "seat-definition-changed" - EventSeatDefinitionRemoved = "seat-definition-removed" - EventManifestAccepted = "manifest-accepted" - EventManifestRefused = "manifest-refused" -) - -// 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, VerbSeatDefinitions} -} - -// Events is every event published on the seat's subject: a new copy each call. -func Events() []string { - return []string{EventSeatDefinitionChanged, EventSeatDefinitionRemoved, EventManifestAccepted, EventManifestRefused} -} - -// 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" - -// SubjectManifestAccepted is where manifest-accepted is published. -const SubjectManifestAccepted = "mesh.seat.mesh-catalog.event.manifest-accepted" - -// SubjectManifestRefused is where manifest-refused is published. -const SubjectManifestRefused = "mesh.seat.mesh-catalog.event.manifest-refused" - -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 -} - -// 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 struct { - Modules []ModulesAnswerModulesItem `json:"modules"` -} - -// ModuleInput is what module takes. -type ModuleInput struct { - // the module's name - 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 struct { - Module ModuleAnswerModule `json:"module"` -} - -// 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 - -// SeatDefinitionsInput is what seat-definitions takes. -type SeatDefinitionsInput struct { - // the seat's name; none lists every trunk seat - Seat string `json:"seat,omitempty"` -} - -// SeatDefinitionsAnswerEntry is a type of the contract. -type SeatDefinitionsAnswerEntry struct { - // the definition, resolved with the parts it includes - Definition map[string]any `json:"definition,omitempty"` - Digest string `json:"digest"` - Seat string `json:"seat"` - // trunk - Source string `json:"source"` - // why the definition could not be read, when it could not - Unparsed string `json:"unparsed,omitempty"` -} - -// SeatDefinitionsAnswerSeatsItem is a type of the contract. -type SeatDefinitionsAnswerSeatsItem struct { - Digest string `json:"digest"` - Seat string `json:"seat"` - // trunk - Source string `json:"source"` -} - -// SeatDefinitionsAnswer is what seat-definitions answers. -type SeatDefinitionsAnswer struct { - Entry *SeatDefinitionsAnswerEntry `json:"entry,omitempty"` - Found bool `json:"found,omitempty"` - Revision int64 `json:"revision,omitempty"` - Seat string `json:"seat,omitempty"` - Seats []SeatDefinitionsAnswerSeatsItem `json:"seats,omitempty"` -} - -// 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"` -} - -// ManifestAcceptedEventBusStateItem is a type of the contract. -type ManifestAcceptedEventBusStateItem struct { - Configuration map[string]any `json:"configuration,omitempty"` - Name string `json:"name"` - Options map[string]any `json:"options,omitempty"` - Storage string `json:"storage"` - Template string `json:"template,omitempty"` - Types []map[string]any `json:"types,omitempty"` -} - -// ManifestAcceptedEvent is the body of the event manifest-accepted. -type ManifestAcceptedEvent struct { - BusState []ManifestAcceptedEventBusStateItem `json:"bus-state"` - Digest string `json:"digest"` - Judge string `json:"judge"` - Module string `json:"module"` - // the indexer's revision of the verdict it says; a reader asks its first view again on a gap in it; zero from an indexer that does not say it - Revision int64 `json:"revision,omitempty"` - V int64 `json:"v"` -} - -// ManifestRefusedEvent is the body of the event manifest-refused. -type ManifestRefusedEvent struct { - Digest string `json:"digest"` - Judge string `json:"judge"` - Module string `json:"module"` - Problems []string `json:"problems"` - // the indexer's revision of the verdict it says; a reader asks its first view again on a gap in it; zero from an indexer that does not say it - Revision int64 `json:"revision,omitempty"` - V int64 `json:"v"` -} - -// 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"` - // 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"` - 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 *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"` -} - -// PredefinedByListValue is one value of the state predefined under the key list. -type PredefinedByListValue struct { - // when (RFC 3339 time) - At *time.Time `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"` - // a declared bound; 0 is a bound, absent is none - Maximum *float64 `json:"maximum,omitempty"` - // a declared bound; 0 is a bound, absent is none - 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 *time.Time `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 *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"` - 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 *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"` - 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 *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"` - 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 *time.Time `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 deleted file mode 100644 index b6d95b15..00000000 --- a/contracts/go/seats/meshcatalog/contract_test.go +++ /dev/null @@ -1,82 +0,0 @@ -// 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 (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 (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 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}, - {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 pull request's configuration state whose bounds are declared 0 (a zero is a bound, not none)", subject: "", given: "{\"declares\":{\"nats\":{\"limit\":{\"default\":0,\"kind\":\"setting\",\"maximum\":0,\"minimum\":0,\"why\":\"a bound\"}}},\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"pull\":\"novox/mesh-catalog#1\",\"v\":1}", wire: "{\"declares\":{\"nats\":{\"limit\":{\"default\":0,\"kind\":\"setting\",\"maximum\":0,\"minimum\":0,\"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}, - {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)) - 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 deleted file mode 100644 index e6fd10a2..00000000 --- a/contracts/go/seats/meshcatalog/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 1adc3e86..00000000 --- a/contracts/go/seats/meshdelivery/contract.go +++ /dev/null @@ -1,455 +0,0 @@ -// 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" - "time" -) - -// 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 time.Time `json:"approved"` - Ask string `json:"ask,omitempty"` - Group string `json:"group"` - Heads string `json:"heads"` - // when (RFC 3339 time) - Since time.Time `json:"since"` - Step string `json:"step,omitempty"` - WaitsFor string `json:"waits-for,omitempty"` - // when (RFC 3339 time) - WaitsSince *time.Time `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 deleted file mode 100644 index f128120d..00000000 --- a/contracts/go/seats/meshdelivery/contract_test.go +++ /dev/null @@ -1,64 +0,0 @@ -// 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 deleted file mode 100644 index 12356bcf..00000000 --- a/contracts/go/seats/meshdelivery/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery - -go 1.26.0 diff --git a/contracts/go/seats/meshdeliverychecker/go.mod b/contracts/go/seats/meshdeliverychecker/go.mod deleted file mode 100644 index d50dcda1..00000000 --- a/contracts/go/seats/meshdeliverychecker/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker - -go 1.26.0 diff --git a/contracts/go/seats/meshwants/contract.go b/contracts/go/seats/meshwants/contract.go deleted file mode 100644 index 74860fa6..00000000 --- a/contracts/go/seats/meshwants/contract.go +++ /dev/null @@ -1,337 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// Package meshwants is the contract of the seat "mesh-wants", defined whole (seats/mesh-wants/seat.json): The seat that says what each node should run and whether each module's new builds wait for a person (ADR 0565, ADR 0539). -package meshwants - -import ( - "bytes" - "encoding/json" - "errors" - "strings" -) - -// Seat is the seat's name. -const Seat = "mesh-wants" - -const ( - VerbUpgrade = "upgrade" - VerbBus = "bus" -) - -const ( - EventPolicyChanged = "policy-changed" -) - -// Verbs is every verb the seat serves, in its definition's order: a new copy each call. -func Verbs() []string { - return []string{VerbUpgrade, VerbBus} -} - -// Events is every event published on the seat's subject: a new copy each call. -func Events() []string { - return []string{EventPolicyChanged} -} - -// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. -const ( - Family = "mesh-wants.events" - FamilyMaxAgeDays = 7 -) - -// SubjectPolicyChanged is where policy-changed is published. -const SubjectPolicyChanged = "mesh.seat.mesh-wants.event.policy-changed" - -const ( - StateWants = "wants" - StateDifferences = "differences" - StateCompositions = "compositions" -) - -// WantsByWantNodeModuleKey is the key of one value of the state wants under the form "want..". -func WantsByWantNodeModuleKey(node, module string) string { return "want." + node + "." + module } - -// ParseWantsByWantNodeModuleKey reads the parts of a key of the form "want..", in the order of the form; the last result is false for a key of another form. -func ParseWantsByWantNodeModuleKey(key string) (string, string, bool) { - rest := key - if !strings.HasPrefix(rest, "want.") { - return "", "", false - } - rest = rest[5:] - 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 -} - -// DecodeWantsByWantNodeModuleValue reads one value of the state wants under the form "want..", refusing a field WantsByWantNodeModuleValue does not know and any text after the value. -func DecodeWantsByWantNodeModuleValue(raw []byte) (WantsByWantNodeModuleValue, error) { - var v WantsByWantNodeModuleValue - 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 -} - -// WantsByWantNodeNodeKey is the key of one value of the state wants under the form "want..node". -func WantsByWantNodeNodeKey(node string) string { return "want." + node + ".node" } - -// ParseWantsByWantNodeNodeKey reads the parts of a key of the form "want..node", in the order of the form; the last result is false for a key of another form. -func ParseWantsByWantNodeNodeKey(key string) (string, bool) { - rest := key - if !strings.HasPrefix(rest, "want.") { - return "", false - } - rest = rest[5:] - v1, after1, found1 := strings.Cut(rest, ".node") - if !found1 || v1 == "" { - return "", false - } - rest = after1 - if rest != "" { - return "", false - } - return v1, true -} - -// DecodeWantsByWantNodeNodeValue reads one value of the state wants under the form "want..node", refusing a field WantsByWantNodeNodeValue does not know and any text after the value. -func DecodeWantsByWantNodeNodeValue(raw []byte) (WantsByWantNodeNodeValue, error) { - var v WantsByWantNodeNodeValue - 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 -} - -// WantsByHeldModuleFingerprintKey is the key of one value of the state wants under the form "held..". -func WantsByHeldModuleFingerprintKey(module, fingerprint string) string { - return "held." + module + "." + fingerprint -} - -// ParseWantsByHeldModuleFingerprintKey reads the parts of a key of the form "held..", in the order of the form; the last result is false for a key of another form. -func ParseWantsByHeldModuleFingerprintKey(key string) (string, string, bool) { - rest := key - if !strings.HasPrefix(rest, "held.") { - return "", "", false - } - rest = rest[5:] - 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 -} - -// DecodeWantsByHeldModuleFingerprintValue reads one value of the state wants under the form "held..", refusing a field WantsByHeldModuleFingerprintValue does not know and any text after the value. -func DecodeWantsByHeldModuleFingerprintValue(raw []byte) (WantsByHeldModuleFingerprintValue, error) { - var v WantsByHeldModuleFingerprintValue - 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 -} - -// DifferencesByNodeModuleKey is the key of one value of the state differences under the form ".". -func DifferencesByNodeModuleKey(node, module string) string { return node + "." + module } - -// ParseDifferencesByNodeModuleKey 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 ParseDifferencesByNodeModuleKey(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 -} - -// DecodeDifferencesByNodeModuleValue reads one value of the state differences under the form ".", refusing a field DifferencesByNodeModuleValue does not know and any text after the value. -func DecodeDifferencesByNodeModuleValue(raw []byte) (DifferencesByNodeModuleValue, error) { - var v DifferencesByNodeModuleValue - 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 -} - -// CompositionsByGroupHeadsKey is the key of one value of the state compositions under the form ".". -func CompositionsByGroupHeadsKey(group, heads string) string { return group + "." + heads } - -// ParseCompositionsByGroupHeadsKey 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 ParseCompositionsByGroupHeadsKey(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 -} - -// DecodeCompositionsByGroupHeadsValue reads one value of the state compositions under the form ".", refusing a field CompositionsByGroupHeadsValue does not know and any text after the value. -func DecodeCompositionsByGroupHeadsValue(raw []byte) (CompositionsByGroupHeadsValue, error) { - var v CompositionsByGroupHeadsValue - 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 -} - -// UpgradeInput is what upgrade takes. -type UpgradeInput struct { - // "true": each node's own composition, by its digest, and the assigner's findings - Compare string `json:"compare,omitempty"` - // one module - Module string `json:"module,omitempty"` - // with module: roll, together, record, or default to take a person's choice back - Policy string `json:"policy,omitempty"` - // with policy: why, required and kept - Why string `json:"why,omitempty"` -} - -// The values of UpgradeInput's compare. -const ( - UpgradeInputCompareTrue = "true" - UpgradeInputCompareFalse = "false" -) - -// UpgradeAnswer is what upgrade answers. -type UpgradeAnswer = map[string]any - -// BusInput is what bus takes. -type BusInput struct { -} - -// BusAnswer is what bus answers. -type BusAnswer = map[string]any - -// PolicyChangedEvent is the body of the event policy-changed. -type PolicyChangedEvent = map[string]any - -// WantsByWantNodeModuleValue is one value of the state wants under the key want... -type WantsByWantNodeModuleValue struct { - // again or skip - Backup string `json:"backup,omitempty"` - Composed string `json:"composed,omitempty"` - Fp string `json:"fp,omitempty"` - Hold bool `json:"hold,omitempty"` - Newest string `json:"newest,omitempty"` - Put bool `json:"put,omitempty"` - Release string `json:"release,omitempty"` - Removed bool `json:"removed,omitempty"` - // the rule that holds the node back, 1 to 10 - Rule int64 `json:"rule,omitempty"` - Self bool `json:"self,omitempty"` - V int64 `json:"v,omitempty"` - Words string `json:"words,omitempty"` - Yes string `json:"yes,omitempty"` -} - -// The values of WantsByWantNodeModuleValue's backup. -const ( - WantsByWantNodeModuleValueBackupAgain = "again" - WantsByWantNodeModuleValueBackupSkip = "skip" -) - -// WantsByWantNodeNodeValue is one value of the state wants under the key want..node. -type WantsByWantNodeNodeValue struct { - Composed string `json:"composed"` - Self bool `json:"self,omitempty"` - V int64 `json:"v,omitempty"` - Words string `json:"words,omitempty"` -} - -// WantsByHeldModuleFingerprintValue is one value of the state wants under the key held... -type WantsByHeldModuleFingerprintValue struct { - Node string `json:"node"` - V int64 `json:"v,omitempty"` - Words string `json:"words,omitempty"` -} - -// DifferencesByNodeModuleValue is one value of the state differences under the key .. -type DifferencesByNodeModuleValue struct { - Rule int64 `json:"rule,omitempty"` - Sent string `json:"sent,omitempty"` - SentBuild string `json:"sent-build,omitempty"` - Unexplained bool `json:"unexplained,omitempty"` - V int64 `json:"v,omitempty"` - Want string `json:"want,omitempty"` - WantRemoved bool `json:"want-removed,omitempty"` - Words string `json:"words,omitempty"` -} - -// CompositionsByGroupHeadsValue is one value of the state compositions under the key .. -type CompositionsByGroupHeadsValue struct { - Group string `json:"group"` - // 12 lowercase hex digits - Heads string `json:"heads"` - Machine string `json:"machine,omitempty"` - Machines []string `json:"machines,omitempty"` - Sdk string `json:"sdk,omitempty"` - // pass, fail or waits - State string `json:"state"` - // milliseconds - Took int64 `json:"took,omitempty"` - V int64 `json:"v,omitempty"` - Warnings []string `json:"warnings,omitempty"` - Words string `json:"words,omitempty"` -} diff --git a/contracts/go/seats/meshwants/contract_test.go b/contracts/go/seats/meshwants/contract_test.go deleted file mode 100644 index 1355a9b4..00000000 --- a/contracts/go/seats/meshwants/contract_test.go +++ /dev/null @@ -1,76 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -package meshwants - -import ( - "encoding/json" - "reflect" - "regexp" - "testing" -) - -// TestFixtures reads every fixture of seats/mesh-wants/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 want", subject: "", given: "{\"fp\":\"ab12\",\"rule\":3,\"v\":1}", wire: "{\"fp\":\"ab12\",\"rule\":3,\"v\":1}", - decode: func(b []byte) (any, error) { - var v WantsByWantNodeModuleValue - err := json.Unmarshal(b, &v) - return v, err - }, subjectIs: nil}, - {name: "a node's own want", subject: "", given: "{\"composed\":\"sha256:aa\",\"self\":true,\"v\":1}", wire: "{\"composed\":\"sha256:aa\",\"self\":true,\"v\":1}", - decode: func(b []byte) (any, error) { - var v WantsByWantNodeNodeValue - err := json.Unmarshal(b, &v) - return v, err - }, subjectIs: nil}, - {name: "a held fingerprint", subject: "", given: "{\"node\":\"ace\",\"v\":1}", wire: "{\"node\":\"ace\",\"v\":1}", - decode: func(b []byte) (any, error) { - var v WantsByHeldModuleFingerprintValue - err := json.Unmarshal(b, &v) - return v, err - }, subjectIs: nil}, - {name: "a difference", subject: "", given: "{\"unexplained\":true,\"v\":1,\"want\":\"ab12\"}", wire: "{\"unexplained\":true,\"v\":1,\"want\":\"ab12\"}", - decode: func(b []byte) (any, error) { - var v DifferencesByNodeModuleValue - err := json.Unmarshal(b, &v) - return v, err - }, subjectIs: nil}, - {name: "a composition", subject: "", given: "{\"group\":\"g1\",\"heads\":\"0123456789ab\",\"state\":\"pass\",\"v\":1}", wire: "{\"group\":\"g1\",\"heads\":\"0123456789ab\",\"state\":\"pass\",\"v\":1}", - decode: func(b []byte) (any, error) { - var v CompositionsByGroupHeadsValue - 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/meshwants/go.mod b/contracts/go/seats/meshwants/go.mod deleted file mode 100644 index edcaa257..00000000 --- a/contracts/go/seats/meshwants/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants - -go 1.26.0 diff --git a/contracts/go/seats/nodebuildgo/contract_test.go b/contracts/go/seats/nodebuildgo/contract_test.go deleted file mode 100644 index 2750cb84..00000000 --- a/contracts/go/seats/nodebuildgo/contract_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// 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$")}, - {name: "a check ask with its fetch, siblings and levels", subject: "mesh.seat.node-build-go.accept.check", given: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"beside\":[{\"commit\":\"fedcba9876543210fedcba9876543210fedcba98\",\"repository\":\"novox/mesh-sdk\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"fedcba9876543210fedcba9876543210fedcba98\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-catalog\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"],\"node-names\":[\"ace\",\"g14\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"2\",\"version\":\"1.26.0\"}},\"id\":\"c3d4\",\"kind\":\"check\",\"language\":\"go\",\"v\":1}", wire: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"beside\":[{\"commit\":\"fedcba9876543210fedcba9876543210fedcba98\",\"repository\":\"novox/mesh-sdk\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"fedcba9876543210fedcba9876543210fedcba98\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-catalog\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"],\"node-names\":[\"ace\",\"g14\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"2\",\"version\":\"1.26.0\"}},\"id\":\"c3d4\",\"kind\":\"check\",\"language\":\"go\",\"v\":1}", - decode: func(b []byte) (any, error) { var v CheckAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.accept\\.check$")}, - {name: "a finished build step with its artifacts", subject: "", given: "{\"ended\":\"2026-11-05T10:03:00Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"result\":{\"artifacts\":[{\"artifact\":\"go\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"reference\":\"container-image-registry/nats/go@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}],\"imports\":[\"git.novox.be/novox/mesh-sdk/go\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"build\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:03:00Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"result\":{\"artifacts\":[{\"artifact\":\"go\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"reference\":\"container-image-registry/nats/go@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}],\"imports\":[\"git.novox.be/novox/mesh-sdk/go\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"build\",\"v\":1}", - decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a finished check step with its levels", subject: "", given: "{\"ended\":\"2026-11-05T10:06:00Z\",\"id\":\"c3d4\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"changed\":[\"a.go\",\"modules/m/main.go\"],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"merge\":\"fedcba9876543210fedcba9876543210fedcba98\",\"modules\":[\".\"],\"trees\":{\"modules/m\":{\"locks\":{\"go.sum\":\"c5fc83c01e92404452b986527d239140ccf9a48b88e0c268fbf38c2e1429e9c9\"},\"manifest\":\"ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356\",\"tree\":\"1111111111111111111111111111111111111111\"}},\"workspace\":\"mvii1iwu5414e4\"},\"levels\":[{\"digest\":{\"level\":\"unit\",\"packages\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\"}]},\"kept\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\",\"tests\":12}],\"level\":\"unit\",\"tests\":[{\"package\":\"./modules/b\",\"pass\":true}],\"unmet\":[{\"needs\":[\"docker\"],\"package\":\"./modules/c\"}]}]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:06:00Z\",\"id\":\"c3d4\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"changed\":[\"a.go\",\"modules/m/main.go\"],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"merge\":\"fedcba9876543210fedcba9876543210fedcba98\",\"modules\":[\".\"],\"trees\":{\"modules/m\":{\"locks\":{\"go.sum\":\"c5fc83c01e92404452b986527d239140ccf9a48b88e0c268fbf38c2e1429e9c9\"},\"manifest\":\"ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356\",\"tree\":\"1111111111111111111111111111111111111111\"}},\"workspace\":\"mvii1iwu5414e4\"},\"levels\":[{\"digest\":{\"level\":\"unit\",\"packages\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\"}]},\"kept\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\",\"tests\":12}],\"level\":\"unit\",\"tests\":[{\"package\":\"./modules/b\",\"pass\":true}],\"unmet\":[{\"needs\":[\"docker\"],\"package\":\"./modules/c\"}]}]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", - decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a declined step", subject: "", given: "{\"id\":\"c3d5\",\"node\":\"ace\",\"result\":{\"declined\":true,\"lacks\":[\"docker\"],\"needs\":[\"docker\"]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"refused\",\"tool\":\"check\",\"v\":1}", wire: "{\"id\":\"c3d5\",\"node\":\"ace\",\"result\":{\"declined\":true,\"lacks\":[\"docker\"],\"needs\":[\"docker\"]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"refused\",\"tool\":\"check\",\"v\":1}", - decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a finished check step whose head does not merge", subject: "", given: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"c3d5\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"conflicts\":[\"a.go\",\"sub/b.go\"],\"modules\":[\".\"],\"workspace\":\"mvii1iwz5050eb\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"c3d5\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"conflicts\":[\"a.go\",\"sub/b.go\"],\"modules\":[\".\"],\"workspace\":\"mvii1iwz5050eb\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", - decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a run ask of the checking program over two heads", subject: "mesh.seat.node-build-go.accept.check", given: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"id\":\"r5e6\",\"kind\":\"check\",\"language\":\"go\",\"run\":{\"args\":[\"-verdict\"],\"heads\":[{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"repository\":\"novox/mesh-delivery\"},{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"repository\":\"novox/mesh-sdk\"}],\"input\":{\"digest\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"size\":7,\"verb\":\"seat:mesh-delivery-checker.input\"},\"program\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"dir\":\"modules/mesh-delivery-checker\",\"package\":\"./cmd/mesh-checking-program\",\"repository\":\"novox/mesh-delivery\"},\"reader\":\"checking program of mesh-delivery head\"},\"v\":1}", wire: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"id\":\"r5e6\",\"kind\":\"check\",\"language\":\"go\",\"run\":{\"args\":[\"-verdict\"],\"heads\":[{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"repository\":\"novox/mesh-delivery\"},{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"repository\":\"novox/mesh-sdk\"}],\"input\":{\"digest\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"size\":7,\"verb\":\"seat:mesh-delivery-checker.input\"},\"program\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"dir\":\"modules/mesh-delivery-checker\",\"package\":\"./cmd/mesh-checking-program\",\"repository\":\"novox/mesh-delivery\"},\"reader\":\"checking program of mesh-delivery head\"},\"v\":1}", - decode: func(b []byte) (any, error) { var v CheckAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.accept\\.check$")}, - {name: "a finished run step whose program exited non-zero", subject: "", given: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"r5e6\",\"node\":\"ace\",\"result\":{\"built\":true,\"exit\":2,\"key\":\"program-0123456789abcdef\",\"said\":\"the facts name no head\"},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"failed\",\"tool\":\"check\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"r5e6\",\"node\":\"ace\",\"result\":{\"built\":true,\"exit\":2,\"key\":\"program-0123456789abcdef\",\"said\":\"the facts name no head\"},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"failed\",\"tool\":\"check\",\"v\":1}", - decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a step ended on a transport fault, given again by the bus", subject: "", given: "{\"again-at\":\"2026-11-05T10:01:30Z\",\"ended\":\"2026-11-05T10:00:30Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"passing\":true,\"reason\":\"fetching the commit: kex_exchange_identification: read: Connection reset by peer\",\"run\":1,\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"again\",\"tool\":\"build\",\"v\":1}", wire: "{\"again-at\":\"2026-11-05T10:01:30Z\",\"ended\":\"2026-11-05T10:00:30Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"passing\":true,\"reason\":\"fetching the commit: kex_exchange_identification: read: Connection reset by peer\",\"run\":1,\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"again\",\"tool\":\"build\",\"v\":1}", - decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "an ask dropped by the caller that no longer waits", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"mesh-delivery-builder\",\"v\":1,\"why\":\"a newer fingerprint supersedes it\"}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"mesh-delivery-builder\",\"v\":1,\"why\":\"a newer fingerprint supersedes it\"}", - decode: func(b []byte) (any, error) { var v PausesByDroppedIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "drop names the ask and why", subject: "", given: "{\"by\":\"mesh-delivery-builder\",\"id\":\"a1b2\",\"why\":\"a newer fingerprint supersedes it\"}", wire: "{\"by\":\"mesh-delivery-builder\",\"id\":\"a1b2\",\"why\":\"a newer fingerprint supersedes it\"}", - decode: func(b []byte) (any, error) { var v DropInput; 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/nodebuildgo/go.mod b/contracts/go/seats/nodebuildgo/go.mod deleted file mode 100644 index 7d786c14..00000000 --- a/contracts/go/seats/nodebuildgo/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo - -go 1.26.0 diff --git a/contracts/go/seats/nodebuildimage/contract_test.go b/contracts/go/seats/nodebuildimage/contract_test.go deleted file mode 100644 index 2926f492..00000000 --- a/contracts/go/seats/nodebuildimage/contract_test.go +++ /dev/null @@ -1,74 +0,0 @@ -// 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$")}, - {name: "a step ended on a transport fault, given again by the bus", subject: "", given: "{\"again-at\":\"2026-11-05T10:01:30Z\",\"ended\":\"2026-11-05T10:00:30Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"passing\":true,\"reason\":\"fetching the commit: kex_exchange_identification: read: Connection reset by peer\",\"run\":1,\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"again\",\"tool\":\"build\",\"v\":1}", wire: "{\"again-at\":\"2026-11-05T10:01:30Z\",\"ended\":\"2026-11-05T10:00:30Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"passing\":true,\"reason\":\"fetching the commit: kex_exchange_identification: read: Connection reset by peer\",\"run\":1,\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"again\",\"tool\":\"build\",\"v\":1}", - decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "an ask dropped by the caller that no longer waits", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"mesh-delivery-builder\",\"v\":1,\"why\":\"a newer fingerprint supersedes it\"}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"mesh-delivery-builder\",\"v\":1,\"why\":\"a newer fingerprint supersedes it\"}", - decode: func(b []byte) (any, error) { var v PausesByDroppedIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "drop names the ask and why", subject: "", given: "{\"by\":\"mesh-delivery-builder\",\"id\":\"a1b2\",\"why\":\"a newer fingerprint supersedes it\"}", wire: "{\"by\":\"mesh-delivery-builder\",\"id\":\"a1b2\",\"why\":\"a newer fingerprint supersedes it\"}", - decode: func(b []byte) (any, error) { var v DropInput; 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/nodebuildimage/go.mod b/contracts/go/seats/nodebuildimage/go.mod deleted file mode 100644 index 33cb3f5b..00000000 --- a/contracts/go/seats/nodebuildimage/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildimage - -go 1.26.0 diff --git a/contracts/go/seats/nodebuildnode/contract_test.go b/contracts/go/seats/nodebuildnode/contract_test.go deleted file mode 100644 index f8bedce6..00000000 --- a/contracts/go/seats/nodebuildnode/contract_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// 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$")}, - {name: "a check ask with the group's tagged package", subject: "mesh.seat.node-build-node.accept.check", given: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"package\":{\"name\":\"@novox/mesh-sdk\",\"version\":\"0.1.15-feat-x.1\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-delivery\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"],\"node-names\":[\"ace\",\"g14\"],\"toolchain\":{\"language\":\"typescript\",\"recipe\":\"1\",\"version\":\"26.12.0\"}},\"id\":\"c3d5\",\"kind\":\"check\",\"language\":\"typescript\",\"v\":1}", wire: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"package\":{\"name\":\"@novox/mesh-sdk\",\"version\":\"0.1.15-feat-x.1\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-delivery\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"],\"node-names\":[\"ace\",\"g14\"],\"toolchain\":{\"language\":\"typescript\",\"recipe\":\"1\",\"version\":\"26.12.0\"}},\"id\":\"c3d5\",\"kind\":\"check\",\"language\":\"typescript\",\"v\":1}", - decode: func(b []byte) (any, error) { var v CheckAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-node\\.accept\\.check$")}, - {name: "a finished check step of a TypeScript merge preview", subject: "", given: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"c3d6\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"changed\":[\"modules/m/package.json\"],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"merge\":\"0123456789abcdef0123456789abcdef01234567\",\"modules\":[\"modules/m\"],\"trees\":{\"modules/m\":{\"manifest\":\"0581b699d9d05517e69b21f5cedbeb2eee8aba2b205280b561be731594d7feef\",\"tree\":\"0123456789abcdef0123456789abcdef01234567\"}},\"workspace\":\"mvii1sxt979f67\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"c3d6\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"changed\":[\"modules/m/package.json\"],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"merge\":\"0123456789abcdef0123456789abcdef01234567\",\"modules\":[\"modules/m\"],\"trees\":{\"modules/m\":{\"manifest\":\"0581b699d9d05517e69b21f5cedbeb2eee8aba2b205280b561be731594d7feef\",\"tree\":\"0123456789abcdef0123456789abcdef01234567\"}},\"workspace\":\"mvii1sxt979f67\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", - decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a step ended on a transport fault, given again by the bus", subject: "", given: "{\"again-at\":\"2026-11-05T10:01:30Z\",\"ended\":\"2026-11-05T10:00:30Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"passing\":true,\"reason\":\"fetching the commit: kex_exchange_identification: read: Connection reset by peer\",\"run\":1,\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"again\",\"tool\":\"build\",\"v\":1}", wire: "{\"again-at\":\"2026-11-05T10:01:30Z\",\"ended\":\"2026-11-05T10:00:30Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"passing\":true,\"reason\":\"fetching the commit: kex_exchange_identification: read: Connection reset by peer\",\"run\":1,\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"again\",\"tool\":\"build\",\"v\":1}", - decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "an ask dropped by the caller that no longer waits", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"mesh-delivery-builder\",\"v\":1,\"why\":\"a newer fingerprint supersedes it\"}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"mesh-delivery-builder\",\"v\":1,\"why\":\"a newer fingerprint supersedes it\"}", - decode: func(b []byte) (any, error) { var v PausesByDroppedIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "drop names the ask and why", subject: "", given: "{\"by\":\"mesh-delivery-builder\",\"id\":\"a1b2\",\"why\":\"a newer fingerprint supersedes it\"}", wire: "{\"by\":\"mesh-delivery-builder\",\"id\":\"a1b2\",\"why\":\"a newer fingerprint supersedes it\"}", - decode: func(b []byte) (any, error) { var v DropInput; 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/nodebuildnode/go.mod b/contracts/go/seats/nodebuildnode/go.mod deleted file mode 100644 index 836a6dc8..00000000 --- a/contracts/go/seats/nodebuildnode/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildnode - -go 1.26.0 diff --git a/contracts/go/seats/npmpackageregistry/go.mod b/contracts/go/seats/npmpackageregistry/go.mod deleted file mode 100644 index acddbbfb..00000000 --- a/contracts/go/seats/npmpackageregistry/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry - -go 1.26.0 diff --git a/contracts/go/seats/ociartifactregistry/go.mod b/contracts/go/seats/ociartifactregistry/go.mod deleted file mode 100644 index f145223c..00000000 --- a/contracts/go/seats/ociartifactregistry/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry - -go 1.26.0 diff --git a/contracts/seats/git/seat.json b/contracts/seats/git/seat.json deleted file mode 100644 index b7c55a10..00000000 --- a/contracts/seats/git/seat.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "format": 1, - "seat": "git", - "scope": "mesh", - "grain": "seat-list", - "domain": "Delivery", - "decision": "novox/hq ADR 0111", - "delivers": "git" -} diff --git a/contracts/seats/mesh-catalog/fixtures/states.json b/contracts/seats/mesh-catalog/fixtures/states.json deleted file mode 100644 index 43af3306..00000000 --- a/contracts/seats/mesh-catalog/fixtures/states.json +++ /dev/null @@ -1,223 +0,0 @@ -[ - { - "name": "a manifest (xclip, read through mesh-catalog.module)", - "of": "state:manifests/", - "given": { - "v": 1, - "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 (xclip, read through mesh-catalog.modules; the inputs and toolchains no tool reads are not here)", - "of": "state:modules/", - "given": { - "v": 1, - "repository": "novox/mesh-catalog", - "commit": "f469eb387a3015569d9b16c66ed418e9dd098f64", - "path": "modules/xclip", - "fp2": "src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56" - } - }, - { - "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 refused verdict (nats, read through the indexer's verdicts tool)", - "of": "state:verdicts/", - "given": { - "module": "nats", - "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" - } - }, - { - "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 pull request's configuration state whose bounds are declared 0 (a zero is a bound, not none)", - "of": "state:config-candidates/", - "given": { - "v": 1, - "pull": "novox/mesh-catalog#1", - "head": "cccccccccccccccccccccccccccccccccccccccc", - "declares": { - "nats": { - "limit": { - "kind": "setting", - "default": 0, - "why": "a bound", - "minimum": 0, - "maximum": 0 - } - } - } - } - }, - { - "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" - } - }, - { - "name": "a module with its toolchain", - "of": "state:modules/", - "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" - } - } - } -] diff --git a/contracts/seats/mesh-catalog/seat.json b/contracts/seats/mesh-catalog/seat.json deleted file mode 100644 index 13f861f1..00000000 --- a/contracts/seats/mesh-catalog/seat.json +++ /dev/null @@ -1,1161 +0,0 @@ -{ - "format": 1, - "seat": "mesh-catalog", - "scope": "mesh", - "grain": "whole", - "domain": "Delivery", - "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": "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" - ] - } - }, - { - "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": "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" - ] - } - }, - { - "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" - } - }, - { - "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.", - "optional": true, - "input": { - "properties": { - "seat": { - "description": "the seat's name; none lists every trunk seat", - "type": "string" - } - }, - "type": "object" - }, - "answer": { - "type": "object", - "description": "with no seat: the revision of the list and every trunk seat with its digest; with a seat: whether it was found and its entry, the definition resolved", - "properties": { - "revision": { - "type": "integer" - }, - "seats": { - "type": "array", - "items": { - "type": "object", - "properties": { - "seat": { - "type": "string" - }, - "source": { - "type": "string", - "description": "trunk" - }, - "digest": { - "type": "string" - } - }, - "required": [ - "seat", - "source", - "digest" - ] - } - }, - "seat": { - "type": "string" - }, - "found": { - "type": "boolean" - }, - "entry": { - "type": "object", - "properties": { - "seat": { - "type": "string" - }, - "source": { - "type": "string", - "description": "trunk" - }, - "digest": { - "type": "string" - }, - "definition": { - "type": "object", - "description": "the definition, resolved with the parts it includes" - }, - "unparsed": { - "type": "string", - "description": "why the definition could not be read, when it could not" - } - }, - "required": [ - "seat", - "source", - "digest" - ] - } - } - } - } - ], - "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" - ] - } - }, - { - "name": "manifest-accepted", - "body": { - "type": "object", - "properties": { - "v": { - "type": "integer" - }, - "module": { - "type": "string" - }, - "digest": { - "type": "string" - }, - "judge": { - "type": "string" - }, - "revision": { - "type": "integer", - "description": "the indexer's revision of the verdict it says; a reader asks its first view again on a gap in it; zero from an indexer that does not say it" - }, - "bus-state": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "storage": { - "type": "string" - }, - "template": { - "type": "string" - }, - "options": { - "type": "object" - }, - "configuration": { - "type": "object" - }, - "types": { - "type": "array", - "items": { - "type": "object" - } - } - }, - "required": [ - "name", - "storage" - ] - } - } - }, - "required": [ - "v", - "module", - "digest", - "judge", - "bus-state" - ] - } - }, - { - "name": "manifest-refused", - "body": { - "type": "object", - "properties": { - "v": { - "type": "integer" - }, - "module": { - "type": "string" - }, - "digest": { - "type": "string" - }, - "judge": { - "type": "string" - }, - "revision": { - "type": "integer", - "description": "the indexer's revision of the verdict it says; a reader asks its first view again on a gap in it; zero from an indexer that does not say it" - }, - "problems": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "v", - "module", - "digest", - "judge", - "problems" - ] - } - } - ], - "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": { - "$ref": "#/defs/ToolchainUsed" - }, - "description": "by language: the toolchain FP2 was computed with" - }, - "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)", - "format": "date-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)", - "format": "date-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", - "null" - ], - "description": "a declared bound; 0 is a bound, absent is none" - }, - "maximum": { - "type": [ - "number", - "null" - ], - "description": "a declared bound; 0 is a bound, absent is none" - }, - "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)", - "format": "date-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)", - "format": "date-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)", - "format": "date-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)", - "format": "date-time" - } - }, - "required": [ - "module", - "repository", - "commit", - "packages" - ] - } - }, - "required": [ - "version" - ] - }, - "at": { - "type": "string", - "description": "when (RFC 3339 time)", - "format": "date-time" - } - }, - "required": [ - "name", - "series" - ] - }, - "description": "a published package series, numbered and newest" - } - ], - "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" - ] - } - } -} diff --git a/contracts/seats/mesh-delivery-checker/fixtures/events.json b/contracts/seats/mesh-delivery-checker/fixtures/events.json deleted file mode 100644 index 7df46f68..00000000 --- a/contracts/seats/mesh-delivery-checker/fixtures/events.json +++ /dev/null @@ -1,44 +0,0 @@ -[ - { - "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/seat.json b/contracts/seats/mesh-delivery/seat.json deleted file mode 100644 index dd617cc4..00000000 --- a/contracts/seats/mesh-delivery/seat.json +++ /dev/null @@ -1,574 +0,0 @@ -{ - "format": 1, - "seat": "mesh-delivery", - "scope": "mesh", - "grain": "whole", - "domain": "Delivery", - "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", - "format": "date-time", - "description": "when (RFC 3339 time)" - }, - "since": { - "type": "string", - "format": "date-time", - "description": "when (RFC 3339 time)" - }, - "waits-for": { - "type": "string" - }, - "waits-since": { - "type": "string", - "format": "date-time", - "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/mesh-wants/fixtures/states.json b/contracts/seats/mesh-wants/fixtures/states.json deleted file mode 100644 index cb38a399..00000000 --- a/contracts/seats/mesh-wants/fixtures/states.json +++ /dev/null @@ -1,47 +0,0 @@ -[ - { - "name": "a want", - "of": "state:wants/want..", - "given": { - "v": 1, - "fp": "ab12", - "rule": 3 - } - }, - { - "name": "a node's own want", - "of": "state:wants/want..node", - "given": { - "v": 1, - "composed": "sha256:aa", - "self": true - } - }, - { - "name": "a held fingerprint", - "of": "state:wants/held..", - "given": { - "v": 1, - "node": "ace" - } - }, - { - "name": "a difference", - "of": "state:differences/.", - "given": { - "v": 1, - "want": "ab12", - "unexplained": true - } - }, - { - "name": "a composition", - "of": "state:compositions/.", - "given": { - "v": 1, - "group": "g1", - "heads": "0123456789ab", - "state": "pass" - } - } -] diff --git a/contracts/seats/mesh-wants/seat.json b/contracts/seats/mesh-wants/seat.json deleted file mode 100644 index a8834526..00000000 --- a/contracts/seats/mesh-wants/seat.json +++ /dev/null @@ -1,284 +0,0 @@ -{ - "format": 1, - "seat": "mesh-wants", - "scope": "mesh", - "grain": "whole", - "domain": "Delivery", - "decision": "novox/hq ADR 0565, ADR 0634, ADR 0635", - "description": "The seat that says what each node should run and whether each module's new builds wait for a person (ADR 0565, ADR 0539).", - "verbs": [ - { - "name": "upgrade", - "description": "Whether each module's new builds wait for a person: its policy (roll, together or record) and where it comes from (a person, the module, the bus, its irreplaceable data, the default); the bus is never rolled. With module, that one; with module, policy and why, a person's choice for it, recorded as a hand act; with policy default, the choice taken back. With compare, each node's own composition and the assigner's findings.", - "input": { - "properties": { - "compare": { - "description": "\"true\": each node's own composition, by its digest, and the assigner's findings", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "module": { - "description": "one module", - "type": "string" - }, - "policy": { - "description": "with module: roll, together, record, or default to take a person's choice back", - "type": "string" - }, - "why": { - "description": "with policy: why, required and kept", - "type": "string" - } - }, - "type": "object" - }, - "answer": { - "type": "object", - "description": "each module's policy, or the one chosen, or the comparison" - } - }, - { - "name": "bus", - "description": "What a bus upgrade would do: the fingerprint of the bus module each node runs against the indexer's newest, and the last bus steps from the hand-act log and the assigner's puts of the bus. Read-only: the step itself is the put on the operator's warrant.", - "input": { - "properties": {}, - "type": "object" - }, - "answer": { - "type": "object", - "description": "what a bus upgrade would do, read only" - } - } - ], - "events": [ - { - "name": "policy-changed", - "body": { - "type": "object" - } - } - ], - "family": { - "name": "mesh-wants.events", - "max-age-days": 7 - }, - "states": [ - { - "name": "wants", - "template": "state", - "key-forms": [ - { - "key": "want..", - "value": { - "type": "object", - "description": "a node's want for a module", - "properties": { - "v": { - "type": "integer" - }, - "fp": { - "type": "string" - }, - "composed": { - "type": "string" - }, - "removed": { - "type": "boolean" - }, - "newest": { - "type": "string" - }, - "rule": { - "type": "integer", - "description": "the rule that holds the node back, 1 to 10" - }, - "backup": { - "type": "string", - "description": "again or skip", - "enum": [ - "again", - "skip" - ] - }, - "words": { - "type": "string" - }, - "put": { - "type": "boolean" - }, - "hold": { - "type": "boolean" - }, - "yes": { - "type": "string" - }, - "release": { - "type": "string" - }, - "self": { - "type": "boolean" - } - } - }, - "description": "a node's want for a module" - }, - { - "key": "want..node", - "value": { - "type": "object", - "description": "a node's own want", - "properties": { - "v": { - "type": "integer" - }, - "composed": { - "type": "string" - }, - "words": { - "type": "string" - }, - "self": { - "type": "boolean" - } - }, - "required": [ - "composed" - ] - }, - "description": "a node's own want, naming the digest of its own composition" - }, - { - "key": "held..", - "value": { - "type": "object", - "description": "a fingerprint the assigner holds", - "properties": { - "v": { - "type": "integer" - }, - "node": { - "type": "string" - }, - "words": { - "type": "string" - } - }, - "required": [ - "node" - ] - }, - "description": "a module's fingerprint the assigner holds" - } - ], - "history": 20, - "description": "what each node should run, and the fingerprints held back" - }, - { - "name": "differences", - "template": "state", - "key-forms": [ - { - "key": ".", - "value": { - "type": "object", - "description": "the difference between a want and what is sent", - "properties": { - "v": { - "type": "integer" - }, - "want": { - "type": "string" - }, - "want-removed": { - "type": "boolean" - }, - "sent": { - "type": "string" - }, - "sent-build": { - "type": "string" - }, - "rule": { - "type": "integer" - }, - "unexplained": { - "type": "boolean" - }, - "words": { - "type": "string" - } - } - }, - "description": "a module on a node" - } - ], - "history": 64, - "description": "differences between a want and what the node-engine is sent" - }, - { - "name": "compositions", - "template": "state", - "key-forms": [ - { - "key": ".", - "value": { - "type": "object", - "description": "the composition verdict of a group's check at its heads", - "properties": { - "v": { - "type": "integer" - }, - "group": { - "type": "string" - }, - "heads": { - "type": "string", - "description": "12 lowercase hex digits" - }, - "state": { - "type": "string", - "description": "pass, fail or waits" - }, - "words": { - "type": "string" - }, - "sdk": { - "type": "string" - }, - "machines": { - "type": "array", - "items": { - "type": "string" - } - }, - "machine": { - "type": "string" - }, - "warnings": { - "type": "array", - "items": { - "type": "string" - } - }, - "took": { - "type": "integer", - "description": "milliseconds" - } - }, - "required": [ - "group", - "heads", - "state" - ] - }, - "description": "a group's check at its heads" - } - ], - "history": 5, - "description": "composition verdicts of a group's check; the checker reads them" - } - ] -} diff --git a/contracts/seats/node-build-go/fixtures/verbs.json b/contracts/seats/node-build-go/fixtures/verbs.json deleted file mode 100644 index 727e95b6..00000000 --- a/contracts/seats/node-build-go/fixtures/verbs.json +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - "name": "drop names the ask and why", - "of": "verb:drop.input", - "given": { - "id": "a1b2", - "by": "mesh-delivery-builder", - "why": "a newer fingerprint supersedes it" - } - } -] diff --git a/contracts/seats/node-build-image/fixtures/states.json b/contracts/seats/node-build-image/fixtures/states.json deleted file mode 100644 index 7e3ef453..00000000 --- a/contracts/seats/node-build-image/fixtures/states.json +++ /dev/null @@ -1,90 +0,0 @@ -[ - { - "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" - } - ] - } - } - }, - { - "name": "a step ended on a transport fault, given again by the bus", - "of": "state:steps/.", - "given": { - "v": 1, - "id": "a1b2", - "tool": "build", - "node": "ace", - "state": "again", - "started": "2026-11-05T10:00:00Z", - "ended": "2026-11-05T10:00:30Z", - "reason": "fetching the commit: kex_exchange_identification: read: Connection reset by peer", - "run": 1, - "again-at": "2026-11-05T10:01:30Z", - "passing": true - } - }, - { - "name": "an ask dropped by the caller that no longer waits", - "of": "state:pauses/dropped.", - "given": { - "v": 1, - "by": "mesh-delivery-builder", - "why": "a newer fingerprint supersedes it", - "at": "2026-11-05T10:00:00Z" - } - } -] diff --git a/contracts/seats/node-build-image/fixtures/verbs.json b/contracts/seats/node-build-image/fixtures/verbs.json deleted file mode 100644 index 727e95b6..00000000 --- a/contracts/seats/node-build-image/fixtures/verbs.json +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - "name": "drop names the ask and why", - "of": "verb:drop.input", - "given": { - "id": "a1b2", - "by": "mesh-delivery-builder", - "why": "a newer fingerprint supersedes it" - } - } -] diff --git a/contracts/seats/node-build-node/fixtures/states.json b/contracts/seats/node-build-node/fixtures/states.json deleted file mode 100644 index d8e5ea20..00000000 --- a/contracts/seats/node-build-node/fixtures/states.json +++ /dev/null @@ -1,162 +0,0 @@ -[ - { - "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" - } - ] - } - } - }, - { - "name": "a check ask with the group's tagged package", - "of": "accept:check", - "subject": "mesh.seat.node-build-node.accept.check", - "given": { - "v": 1, - "id": "c3d5", - "kind": "check", - "language": "typescript", - "asker": "mesh-delivery-checker", - "at": "2026-11-05T10:00:00Z", - "check": { - "fetch": { - "repository": "novox/mesh-delivery", - "commit": "0123456789abcdef0123456789abcdef01234567", - "ref": "refs/pull/7/head", - "base": "89abcdef0123456789abcdef0123456789abcdef", - "package": { - "name": "@novox/mesh-sdk", - "version": "0.1.15-feat-x.1" - } - }, - "levels": [ - "unit", - "integration" - ], - "kept": true, - "toolchain": { - "language": "typescript", - "version": "26.12.0", - "recipe": "1" - }, - "node-names": [ - "ace", - "g14" - ] - } - } - }, - { - "name": "a finished check step of a TypeScript merge preview", - "of": "state:steps/.", - "given": { - "v": 1, - "id": "c3d6", - "tool": "check", - "node": "ace", - "state": "passed", - "started": "2026-11-05T10:00:00Z", - "ended": "2026-11-05T10:01:00Z", - "result": { - "fetch": { - "workspace": "mvii1sxt979f67", - "commit": "0123456789abcdef0123456789abcdef01234567", - "modules": [ - "modules/m" - ], - "base": "abcdef0123456789abcdef0123456789abcdef01", - "merge": "0123456789abcdef0123456789abcdef01234567", - "changed": [ - "modules/m/package.json" - ], - "trees": { - "modules/m": { - "tree": "0123456789abcdef0123456789abcdef01234567", - "manifest": "0581b699d9d05517e69b21f5cedbeb2eee8aba2b205280b561be731594d7feef" - } - } - } - } - } - }, - { - "name": "a step ended on a transport fault, given again by the bus", - "of": "state:steps/.", - "given": { - "v": 1, - "id": "a1b2", - "tool": "build", - "node": "ace", - "state": "again", - "started": "2026-11-05T10:00:00Z", - "ended": "2026-11-05T10:00:30Z", - "reason": "fetching the commit: kex_exchange_identification: read: Connection reset by peer", - "run": 1, - "again-at": "2026-11-05T10:01:30Z", - "passing": true - } - }, - { - "name": "an ask dropped by the caller that no longer waits", - "of": "state:pauses/dropped.", - "given": { - "v": 1, - "by": "mesh-delivery-builder", - "why": "a newer fingerprint supersedes it", - "at": "2026-11-05T10:00:00Z" - } - } -] diff --git a/contracts/seats/node-build-node/fixtures/verbs.json b/contracts/seats/node-build-node/fixtures/verbs.json deleted file mode 100644 index 727e95b6..00000000 --- a/contracts/seats/node-build-node/fixtures/verbs.json +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - "name": "drop names the ask and why", - "of": "verb:drop.input", - "given": { - "id": "a1b2", - "by": "mesh-delivery-builder", - "why": "a newer fingerprint supersedes it" - } - } -] diff --git a/contracts/ts/seats/containerimageregistry/tsconfig.json b/contracts/ts/seats/containerimageregistry/tsconfig.json deleted file mode 100644 index 60274c6a..00000000 --- a/contracts/ts/seats/containerimageregistry/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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/tsconfig.json b/contracts/ts/seats/gopackageregistry/tsconfig.json deleted file mode 100644 index 60274c6a..00000000 --- a/contracts/ts/seats/gopackageregistry/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*.ts"] -} diff --git a/contracts/ts/seats/meshartifactsweep/tsconfig.json b/contracts/ts/seats/meshartifactsweep/tsconfig.json deleted file mode 100644 index 60274c6a..00000000 --- a/contracts/ts/seats/meshartifactsweep/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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/src/index.ts b/contracts/ts/seats/meshcatalog/src/index.ts deleted file mode 100644 index ac232102..00000000 --- a/contracts/ts/seats/meshcatalog/src/index.ts +++ /dev/null @@ -1,545 +0,0 @@ -// 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", - seatDefinitions: "seat-definitions", -} as const; - -export const events = { - seatDefinitionChanged: "seat-definition-changed", - seatDefinitionRemoved: "seat-definition-removed", - manifestAccepted: "manifest-accepted", - manifestRefused: "manifest-refused", -} 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 manifestAcceptedSubject = "mesh.seat.mesh-catalog.event.manifest-accepted"; - -export const manifestRefusedSubject = "mesh.seat.mesh-catalog.event.manifest-refused"; - -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 }; -} - -/** 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 interface ModulesAnswer { - "modules": ModulesAnswerModulesItem[]; -} - -/** is what module takes. */ -export interface ModuleInput { - /** the module's name */ - "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; - "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 interface ModuleAnswer { - "module": ModuleAnswerModule; -} - -/** 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 what seat-definitions takes. */ -export interface SeatDefinitionsInput { - /** the seat's name; none lists every trunk seat */ - "seat"?: string; -} - -export interface SeatDefinitionsAnswerEntry { - /** the definition, resolved with the parts it includes */ - "definition"?: Record; - "digest": string; - "seat": string; - /** trunk */ - "source": string; - /** why the definition could not be read, when it could not */ - "unparsed"?: string; -} - -export interface SeatDefinitionsAnswerSeatsItem { - "digest": string; - "seat": string; - /** trunk */ - "source": string; -} - -/** is what seat-definitions answers. */ -export interface SeatDefinitionsAnswer { - "entry"?: SeatDefinitionsAnswerEntry; - "found"?: boolean; - "revision"?: number; - "seat"?: string; - "seats"?: SeatDefinitionsAnswerSeatsItem[]; -} - -/** 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; -} - -export interface ManifestAcceptedEventBusStateItem { - "configuration"?: Record; - "name": string; - "options"?: Record; - "storage": string; - "template"?: string; - "types"?: Record[]; -} - -/** is the body of the event manifest-accepted. */ -export interface ManifestAcceptedEvent { - "bus-state": ManifestAcceptedEventBusStateItem[]; - "digest": string; - "judge": string; - "module": string; - /** the indexer's revision of the verdict it says; a reader asks its first view again on a gap in it; zero from an indexer that does not say it */ - "revision"?: number; - "v": number; -} - -/** is the body of the event manifest-refused. */ -export interface ManifestRefusedEvent { - "digest": string; - "judge": string; - "module": string; - "problems": string[]; - /** the indexer's revision of the verdict it says; a reader asks its first view again on a gap in it; zero from an indexer that does not say it */ - "revision"?: number; - "v": number; -} - -/** 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; - /** by language: the toolchain FP2 was computed with */ - "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; - /** a declared bound; 0 is a bound, absent is none */ - "maximum"?: number | null; - /** a declared bound; 0 is a bound, absent is none */ - "minimum"?: number | null; - "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 deleted file mode 100644 index 60274c6a..00000000 --- a/contracts/ts/seats/meshcatalog/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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/src/index.ts b/contracts/ts/seats/meshdelivery/src/index.ts deleted file mode 100644 index 261fe53b..00000000 --- a/contracts/ts/seats/meshdelivery/src/index.ts +++ /dev/null @@ -1,338 +0,0 @@ -// 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 deleted file mode 100644 index 60274c6a..00000000 --- a/contracts/ts/seats/meshdelivery/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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/tsconfig.json b/contracts/ts/seats/meshdeliverychecker/tsconfig.json deleted file mode 100644 index 60274c6a..00000000 --- a/contracts/ts/seats/meshdeliverychecker/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*.ts"] -} diff --git a/contracts/ts/seats/meshwants/src/index.ts b/contracts/ts/seats/meshwants/src/index.ts deleted file mode 100644 index 38dab791..00000000 --- a/contracts/ts/seats/meshwants/src/index.ts +++ /dev/null @@ -1,211 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// The contract of the seat "mesh-wants", defined whole (seats/mesh-wants/seat.json): The seat that says what each node should run and whether each module's new builds wait for a person (ADR 0565, ADR 0539). - -export const seat = "mesh-wants"; - -export const verbs = { - upgrade: "upgrade", - bus: "bus", -} as const; - -export const events = { - policyChanged: "policy-changed", -} as const; - -export const family = { name: "mesh-wants.events", maxAgeDays: 7 } as const; - -export const policyChangedSubject = "mesh.seat.mesh-wants.event.policy-changed"; - -export const states = { - wants: "wants", - differences: "differences", - compositions: "compositions", -} as const; - -/** The key of one value of the state wants under the form "want..". */ -export function wantsByWantNodeModuleKey(node: string, module: string): string { - return "want." + node + "." + module; -} - -/** The parts of a key of the form "want..", or undefined for a key of another form. */ -export function parseWantsByWantNodeModuleKey(_key: string): { node: string; module: string } | undefined { - let _rest = _key; - if (!_rest.startsWith("want.")) return undefined; - _rest = _rest.slice(5); - const _at1 = _rest.indexOf("."); - if (_at1 <= 0) return undefined; - const node = _rest.slice(0, _at1); - _rest = _rest.slice(_at1 + 1); - if (_rest === "") return undefined; - const module = _rest; - _rest = ""; - if (_rest !== "") return undefined; - return { node, module }; -} - -/** The key of one value of the state wants under the form "want..node". */ -export function wantsByWantNodeNodeKey(node: string): string { - return "want." + node + ".node"; -} - -/** The parts of a key of the form "want..node", or undefined for a key of another form. */ -export function parseWantsByWantNodeNodeKey(_key: string): { node: string } | undefined { - let _rest = _key; - if (!_rest.startsWith("want.")) return undefined; - _rest = _rest.slice(5); - const _at1 = _rest.indexOf(".node"); - if (_at1 <= 0) return undefined; - const node = _rest.slice(0, _at1); - _rest = _rest.slice(_at1 + 5); - if (_rest !== "") return undefined; - return { node }; -} - -/** The key of one value of the state wants under the form "held..". */ -export function wantsByHeldModuleFingerprintKey(module: string, fingerprint: string): string { - return "held." + module + "." + fingerprint; -} - -/** The parts of a key of the form "held..", or undefined for a key of another form. */ -export function parseWantsByHeldModuleFingerprintKey(_key: string): { module: string; fingerprint: string } | undefined { - let _rest = _key; - if (!_rest.startsWith("held.")) return undefined; - _rest = _rest.slice(5); - const _at1 = _rest.indexOf("."); - if (_at1 <= 0) return undefined; - const module = _rest.slice(0, _at1); - _rest = _rest.slice(_at1 + 1); - if (_rest === "") return undefined; - const fingerprint = _rest; - _rest = ""; - if (_rest !== "") return undefined; - return { module, fingerprint }; -} - -/** The key of one value of the state differences under the form ".". */ -export function differencesByNodeModuleKey(node: string, module: string): string { - return node + "." + module; -} - -/** The parts of a key of the form ".", or undefined for a key of another form. */ -export function parseDifferencesByNodeModuleKey(_key: string): { node: string; module: 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 module = _rest; - _rest = ""; - if (_rest !== "") return undefined; - return { node, module }; -} - -/** The key of one value of the state compositions under the form ".". */ -export function compositionsByGroupHeadsKey(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 parseCompositionsByGroupHeadsKey(_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 }; -} - -/** is what upgrade takes. */ -export interface UpgradeInput { - /** "true": each node's own composition, by its digest, and the assigner's findings */ - "compare"?: "true" | "false"; - /** one module */ - "module"?: string; - /** with module: roll, together, record, or default to take a person's choice back */ - "policy"?: string; - /** with policy: why, required and kept */ - "why"?: string; -} - -/** is what upgrade answers. */ -export type UpgradeAnswer = Record; - -/** is what bus takes. */ -export interface BusInput { -} - -/** is what bus answers. */ -export type BusAnswer = Record; - -/** is the body of the event policy-changed. */ -export type PolicyChangedEvent = Record; - -/** is one value of the state wants under the key want... */ -export interface WantsByWantNodeModuleValue { - /** again or skip */ - "backup"?: "again" | "skip"; - "composed"?: string; - "fp"?: string; - "hold"?: boolean; - "newest"?: string; - "put"?: boolean; - "release"?: string; - "removed"?: boolean; - /** the rule that holds the node back, 1 to 10 */ - "rule"?: number; - "self"?: boolean; - "v"?: number; - "words"?: string; - "yes"?: string; -} - -/** is one value of the state wants under the key want..node. */ -export interface WantsByWantNodeNodeValue { - "composed": string; - "self"?: boolean; - "v"?: number; - "words"?: string; -} - -/** is one value of the state wants under the key held... */ -export interface WantsByHeldModuleFingerprintValue { - "node": string; - "v"?: number; - "words"?: string; -} - -/** is one value of the state differences under the key .. */ -export interface DifferencesByNodeModuleValue { - "rule"?: number; - "sent"?: string; - "sent-build"?: string; - "unexplained"?: boolean; - "v"?: number; - "want"?: string; - "want-removed"?: boolean; - "words"?: string; -} - -/** is one value of the state compositions under the key .. */ -export interface CompositionsByGroupHeadsValue { - "group": string; - /** 12 lowercase hex digits */ - "heads": string; - "machine"?: string; - "machines"?: string[]; - "sdk"?: string; - /** pass, fail or waits */ - "state": string; - /** milliseconds */ - "took"?: number; - "v"?: number; - "warnings"?: string[]; - "words"?: string; -} - diff --git a/contracts/ts/seats/meshwants/tsconfig.json b/contracts/ts/seats/meshwants/tsconfig.json deleted file mode 100644 index 60274c6a..00000000 --- a/contracts/ts/seats/meshwants/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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/src/index.ts b/contracts/ts/seats/nodebuildgo/src/index.ts deleted file mode 100644 index a465a45d..00000000 --- a/contracts/ts/seats/nodebuildgo/src/index.ts +++ /dev/null @@ -1,191 +0,0 @@ -// 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", - drop: "drop", -} 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 "dropped.". */ -export function pausesByDroppedIDKey(id: string): string { - return "dropped." + id; -} - -/** The parts of a key of the form "dropped.", or undefined for a key of another form. */ -export function parsePausesByDroppedIDKey(_key: string): { id: string } | undefined { - let _rest = _key; - if (!_rest.startsWith("dropped.")) return undefined; - _rest = _rest.slice(8); - if (_rest === "") return undefined; - const id = _rest; - _rest = ""; - if (_rest !== "") return undefined; - return { id }; -} - -/** 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 AskArtifact = buildStep.AskArtifact; -export type BuildAnswer = buildStep.BuildAnswer; -export type BuiltArtifact = buildStep.BuiltArtifact; -export type Capacity = buildStep.Capacity; -export type CheckAnswer = buildStep.CheckAnswer; -export type ComposeAnswer = buildStep.ComposeAnswer; -export type CurrentAnswer = buildStep.CurrentAnswer; -export type CurrentInput = buildStep.CurrentInput; -export type Drop = buildStep.Drop; -export type DropAnswer = buildStep.DropAnswer; -export type DropInput = buildStep.DropInput; -export type Fetch = buildStep.Fetch; -export type FetchAnswer = buildStep.FetchAnswer; -export type FetchBesideItem = buildStep.FetchBesideItem; -export type FetchPackage = buildStep.FetchPackage; -export type FetchRefusal = buildStep.FetchRefusal; -export type LevelAnswer = buildStep.LevelAnswer; -export type LevelAnswerDigest = buildStep.LevelAnswerDigest; -export type LevelAnswerDigestNeedsItem = buildStep.LevelAnswerDigestNeedsItem; -export type LevelAnswerDigestPackagesItem = buildStep.LevelAnswerDigestPackagesItem; -export type LevelAnswerKeptItem = buildStep.LevelAnswerKeptItem; -export type LevelAnswerUnmetItem = buildStep.LevelAnswerUnmetItem; -export type ModuleTree = buildStep.ModuleTree; -export type Pause = buildStep.Pause; -export type PauseAnswer = buildStep.PauseAnswer; -export type PauseInput = buildStep.PauseInput; -export type PlacedBeside = buildStep.PlacedBeside; -export type Program = buildStep.Program; -export type ResumeAnswer = buildStep.ResumeAnswer; -export type ResumeInput = buildStep.ResumeInput; -export type RunAnswer = buildStep.RunAnswer; -export type RunHead = buildStep.RunHead; -export type RunInput = buildStep.RunInput; -export type RunJob = buildStep.RunJob; -export type RunProgram = buildStep.RunProgram; -export type Step = buildStep.Step; -export type StepAsk = buildStep.StepAsk; -export type StepAskBuild = buildStep.StepAskBuild; -export type StepAskCheck = buildStep.StepAskCheck; -export type StepAskCompose = buildStep.StepAskCompose; -export type StepResult = buildStep.StepResult; -export type TakenVersion = buildStep.TakenVersion; -export type Toolchain = buildStep.Toolchain; -export type ToolchainUsed = buildStep.ToolchainUsed; - -/** 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 dropped.. */ -export type PausesByDroppedIDValue = Drop; - -/** 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/tsconfig.json b/contracts/ts/seats/nodebuildgo/tsconfig.json deleted file mode 100644 index 60274c6a..00000000 --- a/contracts/ts/seats/nodebuildgo/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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/src/index.ts b/contracts/ts/seats/nodebuildimage/src/index.ts deleted file mode 100644 index 8024a0d0..00000000 --- a/contracts/ts/seats/nodebuildimage/src/index.ts +++ /dev/null @@ -1,191 +0,0 @@ -// 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", - drop: "drop", -} 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 "dropped.". */ -export function pausesByDroppedIDKey(id: string): string { - return "dropped." + id; -} - -/** The parts of a key of the form "dropped.", or undefined for a key of another form. */ -export function parsePausesByDroppedIDKey(_key: string): { id: string } | undefined { - let _rest = _key; - if (!_rest.startsWith("dropped.")) return undefined; - _rest = _rest.slice(8); - if (_rest === "") return undefined; - const id = _rest; - _rest = ""; - if (_rest !== "") return undefined; - return { id }; -} - -/** 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 AskArtifact = buildStep.AskArtifact; -export type BuildAnswer = buildStep.BuildAnswer; -export type BuiltArtifact = buildStep.BuiltArtifact; -export type Capacity = buildStep.Capacity; -export type CheckAnswer = buildStep.CheckAnswer; -export type ComposeAnswer = buildStep.ComposeAnswer; -export type CurrentAnswer = buildStep.CurrentAnswer; -export type CurrentInput = buildStep.CurrentInput; -export type Drop = buildStep.Drop; -export type DropAnswer = buildStep.DropAnswer; -export type DropInput = buildStep.DropInput; -export type Fetch = buildStep.Fetch; -export type FetchAnswer = buildStep.FetchAnswer; -export type FetchBesideItem = buildStep.FetchBesideItem; -export type FetchPackage = buildStep.FetchPackage; -export type FetchRefusal = buildStep.FetchRefusal; -export type LevelAnswer = buildStep.LevelAnswer; -export type LevelAnswerDigest = buildStep.LevelAnswerDigest; -export type LevelAnswerDigestNeedsItem = buildStep.LevelAnswerDigestNeedsItem; -export type LevelAnswerDigestPackagesItem = buildStep.LevelAnswerDigestPackagesItem; -export type LevelAnswerKeptItem = buildStep.LevelAnswerKeptItem; -export type LevelAnswerUnmetItem = buildStep.LevelAnswerUnmetItem; -export type ModuleTree = buildStep.ModuleTree; -export type Pause = buildStep.Pause; -export type PauseAnswer = buildStep.PauseAnswer; -export type PauseInput = buildStep.PauseInput; -export type PlacedBeside = buildStep.PlacedBeside; -export type Program = buildStep.Program; -export type ResumeAnswer = buildStep.ResumeAnswer; -export type ResumeInput = buildStep.ResumeInput; -export type RunAnswer = buildStep.RunAnswer; -export type RunHead = buildStep.RunHead; -export type RunInput = buildStep.RunInput; -export type RunJob = buildStep.RunJob; -export type RunProgram = buildStep.RunProgram; -export type Step = buildStep.Step; -export type StepAsk = buildStep.StepAsk; -export type StepAskBuild = buildStep.StepAskBuild; -export type StepAskCheck = buildStep.StepAskCheck; -export type StepAskCompose = buildStep.StepAskCompose; -export type StepResult = buildStep.StepResult; -export type TakenVersion = buildStep.TakenVersion; -export type Toolchain = buildStep.Toolchain; -export type ToolchainUsed = buildStep.ToolchainUsed; - -/** 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 dropped.. */ -export type PausesByDroppedIDValue = Drop; - -/** 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/tsconfig.json b/contracts/ts/seats/nodebuildimage/tsconfig.json deleted file mode 100644 index 60274c6a..00000000 --- a/contracts/ts/seats/nodebuildimage/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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/src/index.ts b/contracts/ts/seats/nodebuildnode/src/index.ts deleted file mode 100644 index 85ec77b6..00000000 --- a/contracts/ts/seats/nodebuildnode/src/index.ts +++ /dev/null @@ -1,191 +0,0 @@ -// 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", - drop: "drop", -} 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 "dropped.". */ -export function pausesByDroppedIDKey(id: string): string { - return "dropped." + id; -} - -/** The parts of a key of the form "dropped.", or undefined for a key of another form. */ -export function parsePausesByDroppedIDKey(_key: string): { id: string } | undefined { - let _rest = _key; - if (!_rest.startsWith("dropped.")) return undefined; - _rest = _rest.slice(8); - if (_rest === "") return undefined; - const id = _rest; - _rest = ""; - if (_rest !== "") return undefined; - return { id }; -} - -/** 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 AskArtifact = buildStep.AskArtifact; -export type BuildAnswer = buildStep.BuildAnswer; -export type BuiltArtifact = buildStep.BuiltArtifact; -export type Capacity = buildStep.Capacity; -export type CheckAnswer = buildStep.CheckAnswer; -export type ComposeAnswer = buildStep.ComposeAnswer; -export type CurrentAnswer = buildStep.CurrentAnswer; -export type CurrentInput = buildStep.CurrentInput; -export type Drop = buildStep.Drop; -export type DropAnswer = buildStep.DropAnswer; -export type DropInput = buildStep.DropInput; -export type Fetch = buildStep.Fetch; -export type FetchAnswer = buildStep.FetchAnswer; -export type FetchBesideItem = buildStep.FetchBesideItem; -export type FetchPackage = buildStep.FetchPackage; -export type FetchRefusal = buildStep.FetchRefusal; -export type LevelAnswer = buildStep.LevelAnswer; -export type LevelAnswerDigest = buildStep.LevelAnswerDigest; -export type LevelAnswerDigestNeedsItem = buildStep.LevelAnswerDigestNeedsItem; -export type LevelAnswerDigestPackagesItem = buildStep.LevelAnswerDigestPackagesItem; -export type LevelAnswerKeptItem = buildStep.LevelAnswerKeptItem; -export type LevelAnswerUnmetItem = buildStep.LevelAnswerUnmetItem; -export type ModuleTree = buildStep.ModuleTree; -export type Pause = buildStep.Pause; -export type PauseAnswer = buildStep.PauseAnswer; -export type PauseInput = buildStep.PauseInput; -export type PlacedBeside = buildStep.PlacedBeside; -export type Program = buildStep.Program; -export type ResumeAnswer = buildStep.ResumeAnswer; -export type ResumeInput = buildStep.ResumeInput; -export type RunAnswer = buildStep.RunAnswer; -export type RunHead = buildStep.RunHead; -export type RunInput = buildStep.RunInput; -export type RunJob = buildStep.RunJob; -export type RunProgram = buildStep.RunProgram; -export type Step = buildStep.Step; -export type StepAsk = buildStep.StepAsk; -export type StepAskBuild = buildStep.StepAskBuild; -export type StepAskCheck = buildStep.StepAskCheck; -export type StepAskCompose = buildStep.StepAskCompose; -export type StepResult = buildStep.StepResult; -export type TakenVersion = buildStep.TakenVersion; -export type Toolchain = buildStep.Toolchain; -export type ToolchainUsed = buildStep.ToolchainUsed; - -/** 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 dropped.. */ -export type PausesByDroppedIDValue = Drop; - -/** 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/tsconfig.json b/contracts/ts/seats/nodebuildnode/tsconfig.json deleted file mode 100644 index 60274c6a..00000000 --- a/contracts/ts/seats/nodebuildnode/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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/tsconfig.json b/contracts/ts/seats/npmpackageregistry/tsconfig.json deleted file mode 100644 index 60274c6a..00000000 --- a/contracts/ts/seats/npmpackageregistry/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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/tsconfig.json b/contracts/ts/seats/ociartifactregistry/tsconfig.json deleted file mode 100644 index 60274c6a..00000000 --- a/contracts/ts/seats/ociartifactregistry/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*.ts"] -} diff --git a/modules/mesh-delivery-assigner/asks/asks.go b/modules/mesh-delivery-assigner/asks/asks.go index 7fe58160..bc29ad14 100644 --- a/modules/mesh-delivery-assigner/asks/asks.go +++ b/modules/mesh-delivery-assigner/asks/asks.go @@ -16,10 +16,10 @@ import ( "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" - "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" - "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" - "git.novox.be/novox/mesh-delivery/sdk/kept" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshdeliveryclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/catalogkey" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/kept" "git.novox.be/novox/mesh-sdk/go/keys" ) @@ -38,10 +38,16 @@ const ( // `assignments.`. The kind needs no proof, as the bus's put does not: its yes is an Approve, which only a // channel proving who answered takes and the view never does (ViewAnswerFits). AskKindAssignments = "assignments" + // AskKindOutside is the kind of the assigner's ask about a moved module whose failing health check has waited past its bound + // on an outside cause (design 47 G §706): go on, or count it as failed. `outside...`. + AskKindOutside = "outside" + // AskKindRevert is the kind of the assigner's ask about a held revert, one with no restore point (design 47 G §710): keep it + // held, or start the reverted build on the live data. `revert...`, the reverted build's. + AskKindRevert = "revert" ) // AskKey is an ask's key read whole: its Kind, and the parts its kind has: a merge's Group and heads Hash, a hold's Module -// and fingerprint FP, a restore's and a backup's Node, Module and FP, a record's Node, Module and the digest Composed of the +// and fingerprint FP, a restore's, a backup's, an outside's and a revert's Node, Module and FP, a record's Node, Module and the digest Composed of the // composition it would allow, the assignments' Proposal. type AskKey struct { Proposal string @@ -60,11 +66,11 @@ func AskNeedsProof(kind string) (bool, error) { switch kind { case meshdeliveryclient.AskKindMerge: return true, nil - case AskKindHold, AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments: + case AskKindHold, AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments, AskKindOutside, AskKindRevert: return false, nil } - return false, fmt.Errorf("asks: %q is not a kind of ask, %s, %s, %s, %s, %s or %s", kind, meshdeliveryclient.AskKindMerge, AskKindHold, - AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments) + return false, fmt.Errorf("asks: %q is not a kind of ask, %s, %s, %s, %s, %s, %s, %s or %s", kind, meshdeliveryclient.AskKindMerge, AskKindHold, + AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments, AskKindOutside, AskKindRevert) } // HoldAskKey is the key of the ask about a module's held fingerprint: `hold..`. @@ -89,10 +95,23 @@ func RestoreAskKey(node, module, fingerprint string) (string, error) { return AskKindRestore + "." + node + "." + key, nil } +// OutsideAskKey is the key of the ask about a module's failing check on a node that waited past its bound on an outside cause: +// `outside...`. +func OutsideAskKey(node, module, fingerprint string) (string, error) { + key, err := RestoreAskKey(node, module, fingerprint) + if err != nil { + return "", err + } + return AskKindOutside + strings.TrimPrefix(key, AskKindRestore), nil +} + +// OutsideAbout is what the outside ask is about: `..` (design 47 G §706). +func OutsideAbout(key string) string { return strings.TrimPrefix(key, AskKindOutside+".") } + // BackupAskKey is the key of the ask about a module's failed backup on a node before it applies a fingerprint: // `backup...`. func BackupAskKey(node, module, fingerprint string) (string, error) { - part, err := meshcatalogclient.KeyPart(fingerprint) + part, err := catalogkey.KeyPart(fingerprint) if err != nil { return "", err } @@ -103,6 +122,16 @@ func BackupAskKey(node, module, fingerprint string) (string, error) { return AskKindBackup + "." + key, nil } +// RevertAskKey is the key of the ask about a held revert of a module on a node to the reverted build's fingerprint: +// `revert...` (design 47 G §710). +func RevertAskKey(node, module, fingerprint string) (string, error) { + key, err := BackupAskKey(node, module, fingerprint) + if err != nil { + return "", err + } + return AskKindRevert + strings.TrimPrefix(key, AskKindBackup), nil +} + // AssignmentsAskKey is the key of the authorising ask about a proposal of assignments: `assignments.` (ADR 0567 §5). func AssignmentsAskKey(proposal string) (string, error) { if err := meshmoduleassignerclient.CheckProposalID(proposal); err != nil { @@ -124,7 +153,7 @@ func RecordAskKey(node, module, composed string) (string, error) { return AskKindRecord + "." + have + "." + strings.TrimPrefix(composed, "sha256:"), nil } -// ParseAnyAskKey reads an ask key of any kind (a merge, hold, restore, backup, record or assignments ask); it refuses an unknown +// ParseAnyAskKey reads an ask key of any kind (a merge, hold, restore, backup, record, assignments, outside or revert ask); it refuses an unknown // kind and a key whose parts do not fit its kind, naming the key. func ParseAnyAskKey(key string) (AskKey, error) { kind, rest, _ := strings.Cut(key, ".") @@ -157,12 +186,19 @@ func ParseAnyAskKey(key string) (AskKey, error) { return refused(err) } return AskKey{Kind: kind, Node: node, Module: module, FP: fp}, nil - case AskKindBackup: + case AskKindOutside: + k, err := ParseAnyAskKey(AskKindRestore + "." + rest) + if err != nil { + return refused(fmt.Errorf("an outside ask is outside...")) + } + k.Kind = kind + return k, nil + case AskKindBackup, AskKindRevert: node, module, part, isNode, err := nodeengineclient.ParseComposedKey(rest) if err != nil || isNode { - return refused(fmt.Errorf("a backup ask is backup...")) + return refused(fmt.Errorf("a %s ask is %s...", kind, kind)) } - fp, err := meshcatalogclient.ParseKeyPart(part) + fp, err := catalogkey.ParseKeyPart(part) if err != nil { return refused(err) } diff --git a/modules/mesh-delivery-assigner/asks/asks_test.go b/modules/mesh-delivery-assigner/asks/asks_test.go index c522584c..d97babc9 100644 --- a/modules/mesh-delivery-assigner/asks/asks_test.go +++ b/modules/mesh-delivery-assigner/asks/asks_test.go @@ -19,6 +19,7 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) { backup, _ := BackupAskKey("n1", "novox.be", fp) assignments, _ := AssignmentsAskKey("p-20261028t183000-1a2b3c") record, _ := RecordAskKey("n1", "gitea", digest) + revert, _ := RevertAskKey("n1", "nats", fp) for key, want := range map[string]struct { got string back AskKey @@ -28,6 +29,7 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) { "backup": {backup, AskKey{Kind: "backup", Node: "n1", Module: "novox.be", FP: fp}}, "assignments": {assignments, AskKey{Kind: "assignments", Proposal: "p-20261028t183000-1a2b3c"}}, "record": {record, AskKey{Kind: "record", Node: "n1", Module: "gitea", Composed: digest}}, + "revert": {revert, AskKey{Kind: "revert", Node: "n1", Module: "nats", FP: fp}}, } { back, err := ParseAnyAskKey(want.got) if err != nil || back != want.back { @@ -35,7 +37,7 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) { } } for got, want := range map[string]string{hold: "hold.gitea." + hex, restore: "restore.n1.gitea." + hex, backup: "backup.n1.novox=2ebe." + hex, - assignments: "assignments.p-20261028t183000-1a2b3c", record: "record.n1.gitea." + strings.Repeat("cd", 32)} { + assignments: "assignments.p-20261028t183000-1a2b3c", record: "record.n1.gitea." + strings.Repeat("cd", 32), revert: "revert.n1.nats." + hex} { if got != want { t.Errorf("key %q, want %q", got, want) } @@ -45,7 +47,7 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) { t.Errorf("a merge ask key: %+v, %v", merge, err) } for _, bad := range []string{"", "other.x", "hold.gitea", "hold.gitea.src2:" + hex, "restore.n1", "backup.n1.node", "backup.n1.gitea." + hex[:60], - "assignment.AB", "record.n1.gitea." + hex[:10], "record.n1.node." + hex, "merge.g"} { + "assignment.AB", "record.n1.gitea." + hex[:10], "record.n1.node." + hex, "merge.g", "revert.n1.node", "revert.n1.nats." + hex[:60]} { if _, err := ParseAnyAskKey(bad); err == nil { t.Errorf("ParseAnyAskKey(%q) reads a wrong key", bad) } @@ -53,8 +55,10 @@ func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) { if proof, err := AskNeedsProof("merge"); err != nil || !proof { t.Error("a merge ask needs proof") } - if proof, err := AskNeedsProof("hold"); err != nil || proof { - t.Error("a hold ask needs no proof") + for _, kind := range []string{"hold", "revert"} { + if proof, err := AskNeedsProof(kind); err != nil || proof { + t.Errorf("a %s ask needs no proof", kind) + } } if _, err := AskNeedsProof("view"); err == nil { t.Error("an unknown kind of ask is taken") diff --git a/modules/mesh-delivery-assigner/asks/go.mod b/modules/mesh-delivery-assigner/asks/go.mod index 6ed0f8c5..07db4a1d 100644 --- a/modules/mesh-delivery-assigner/asks/go.mod +++ b/modules/mesh-delivery-assigner/asks/go.mod @@ -5,19 +5,19 @@ go 1.26.0 require ( git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000 - git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 - git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0-00010101000000-000000000000 - git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 ) require ( git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000 // indirect - git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 // indirect - git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000 // indirect - git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000 // indirect - git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000 // indirect - git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000 // indirect + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 // indirect + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000 // indirect + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000 // indirect + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000 // indirect + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000 // indirect ) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/actingwaits.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/actingwaits.go new file mode 100644 index 00000000..2297a750 --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/actingwaits.go @@ -0,0 +1,225 @@ +package main + +import ( + "context" + "maps" + "slices" + "strings" + + meshwants "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient" + seat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" + + "mesh-delivery-assigner/internal/acting" + "mesh-delivery-assigner/internal/assign" + "mesh-delivery-assigner/internal/assigngraph" + "mesh-delivery-assigner/internal/assignment" + "mesh-delivery-assigner/internal/buckets" +) + +// The waits for an acting part's new version in the steps the assigner holds (design 47 G §266, §268, §276): an entry of the assignment +// graph whose acting edge names composing or placing waits, its want held, until the part's version runs on the edge's node. The wait is +// kept in the state `waits` under `acting...`, said as `wait-held` when it starts and `wait-ended` (ran, released, +// replaced) when it ends; `assigner_waits` reads it and `assigner_release` with how `now` lets the entry move with the part's running +// version. A node away starts no wait: its clock starts when the node is present (§268). + +// actingOf is the Module.Acting of module: the held acting edges of its entry on a node, those whose wait was not released left out. +func (p *passer) actingOf(module string, released map[string]bool) func(node string) []assign.ActingEdge { + return func(node string) []assign.ActingEdge { + var out []assign.ActingEdge + if p.engineOn(node) == module { + return nil // the node-engine's own want is held by no acting edge, so it is written first (§268, A-323, A-330) + } + for _, e := range p.entryEdges(node, module) { + if acting.Held(e.Step) && !released[acting.Key(node, module, e.Module)+"@"+e.Fp] { + out = append(out, assign.ActingEdge{Node: e.Node, Part: e.Module, Fp: e.Fp, Step: e.Step}) + } + } + return out + } +} + +// engineOn is the module that holds the seat node-engine on node in the revision the pass computed, read from the acting edges' +// seat (A-330); "" when none is read. +func (p *passer) engineOn(node string) string { + if p.graph == nil { + return "" + } + return assigngraph.Engines(slices.Collect(maps.Values(p.graph.computed.Entries)))[node] +} + +// entryEdges are the acting edges of the entry of module on node in the revision the pass computed. +func (p *passer) entryEdges(node, module string) []seat.Edge { + g := p.graph + if g == nil { + return nil + } + for _, ref := range g.computed.Revision.Entries { + if ref.Node == node && ref.Module == module { + var out []seat.Edge + for _, e := range g.computed.Entries[ref.Digest].Edges { + if e.Kind == "acting" { + out = append(out, e) + } + } + return out + } + } + return nil +} + +// actingReleased is the waits released by hand and not yet run, by `@`. +func (p *passer) actingReleased() map[string]bool { + out := map[string]bool{} + for key, raw := range p.w.read(buckets.Waits()) { + if w, err := acting.Decode(raw); err == nil && w.Released && (acting.Is(key) || acting.IsOrder(key)) { + out[key+"@"+w.Fp] = true + } + } + return out +} + +// actingWaits keeps the acting waits after the pass's wants: it starts the wait of each entry held for a part's version, ends the +// wait of one that ran, was replaced or was released, and says each. +func (p *passer) actingWaits(ctx context.Context, n assign.Nodes, wanted map[string]map[string]meshwants.Want) { + g := p.graph + if g == nil || p.kvs[buckets.Waits()] == nil { + return + } + now := p.clock() + kept := p.w.read(buckets.Waits()) + standing := map[string]bool{} + engines := assigngraph.Engines(slices.Collect(maps.Values(g.computed.Entries))) + held := map[string]bool{} // `/` of each entry held for a part's version + for _, ref := range g.computed.Revision.Entries { + entry := g.computed.Entries[ref.Digest] + if w, ok := wanted[ref.Module][ref.Node]; ok && w.Fp == entry.Fp { + continue // the entry's version is wanted already: nothing moves, so nothing waits + } + if engines[ref.Node] == ref.Module { + continue // the node-engine's own want waits for no acting part (§268, A-323, A-330) + } + for _, e := range entry.Edges { + if e.Kind != "acting" || !acting.Held(e.Step) { + continue + } + key := acting.Key(ref.Node, ref.Module, e.Module) + if n.Runs(assign.ActingEdge{Node: e.Node, Part: e.Module, Fp: e.Fp, Step: e.Step}) { + continue + } + standing[key] = true + held[ref.Node+"/"+ref.Module] = true + if raw := kept[key]; raw != nil { + if w, err := acting.Decode(raw); err == nil && w.Entry == entry.Fp && w.Fp == e.Fp { + p.actingReleaseSaid(ctx, key, w) + continue + } + } + if !n.Online[ref.Node] { + continue // a node away holds only itself, and its clock has not started (§268) + } + w := acting.Wait{Step: e.Step, Node: ref.Node, Module: ref.Module, Part: e.Module, PartNode: e.Node, Fp: e.Fp, Entry: entry.Fp, + Since: now, ClockFrom: now, Bound: acting.BoundOf(e).String()} + if err := p.wantsSay(seat.EventWaitHeld, acting.Body(w, assignment.Module()), key+"."+e.Fp+"."+entry.Fp); err != nil { + p.logf("the wait of %s on %s for %s is not said, and is said at the next pass: %v", ref.Module, ref.Node, e.Module, err) + continue + } + p.put(ctx, buckets.Waits(), key, func() ([]byte, error) { return acting.Encode(w) }) + } + } + p.actingCycles(g.computed.Entries, engines, held, n) + for key, raw := range kept { + if !acting.Is(key) || standing[key] { + continue + } + w, err := acting.Decode(raw) + if err != nil { + continue + } + how := acting.HowRan + if w.Released { + how = acting.HowReleased + } else if e, ok := p.edgeOf(w); !ok || !n.Runs(e) { + how = acting.HowReplaced + } + if !w.EndSaid { + body := acting.Body(w, assignment.Module()) + body.How = how + if err := p.wantsSay(seat.EventWaitEnded, body, key+"."+w.Fp+"."+w.Entry+"."+how); err != nil { + p.logf("the end of the wait of %s on %s for %s is not said, and is said at the next pass: %v", w.Module, w.Node, w.Part, err) + continue + } + } + p.remove(ctx, buckets.Waits(), key) + } +} + +// actingReleaseSaid says wait-ended of a wait released by hand, once, and keeps its record so the entry is not held again. +func (p *passer) actingReleaseSaid(ctx context.Context, key string, w acting.Wait) { + if !w.Released || w.EndSaid { + return + } + body := acting.Body(w, assignment.Module()) + body.How = acting.HowReleased + body.By = w.By + if err := p.wantsSay(seat.EventWaitEnded, body, key+"."+w.Fp+"."+w.Entry+"."+acting.HowReleased); err != nil { + p.logf("the release of the wait of %s on %s for %s is not said, and is said at the next pass: %v", w.Module, w.Node, w.Part, err) + return + } + w.EndSaid = true + p.put(ctx, buckets.Waits(), key, func() ([]byte, error) { return acting.Encode(w) }) +} + +// edgeOf is the edge a kept wait stands on, as an acting edge. +func (p *passer) edgeOf(w acting.Wait) (assign.ActingEdge, bool) { + return assign.ActingEdge{Node: w.PartNode, Part: w.Part, Fp: w.Fp, Step: w.Step}, true +} + +// actingCycles writes the finding acting-cycle of each held want whose holding edges lie on a cycle no rule breaks, and clears it of +// every other want, a finding a pass before or another process wrote included (design 47 G A-323, A-330). An edge holds while its +// step is the assigner's, its part's version does not run and its wait was not released; the node-engine's own want is held by none. +func (p *passer) actingCycles(entries map[string]seat.AssignmentEntry, engines map[string]string, held map[string]bool, n assign.Nodes) { + if p.findings.Put == nil { + return + } + released := p.actingReleased() + cycles := assigngraph.HeldCycles(slices.Collect(maps.Values(entries)), func(e seat.AssignmentEntry, edge seat.Edge) bool { + return held[e.Node+"/"+e.Module] && engines[e.Node] != e.Module && edge.Kind == "acting" && acting.Held(edge.Step) && + !released[acting.Key(e.Node, e.Module, edge.Module)+"@"+edge.Fp] && !n.Runs(assign.ActingEdge{Node: edge.Node, Part: edge.Module, Fp: edge.Fp, Step: edge.Step}) + }) + raised := map[string]bool{} + for _, at := range slices.Sorted(maps.Keys(cycles)) { + node, module, _ := strings.Cut(at, "/") + o := assign.ActingCycleFinding(node, module, cycles[at]) + key := mcseatKey(o) + raised[key] = true + if p.said[key+"="+o.Summary] { + continue // written already as it stands + } + if err := p.findings.Put(o.Finding(p.clock())); err != nil { + p.logf("the finding %s is not written: %v", key, err) + continue + } + p.said[key+"="+o.Summary] = true + } + if p.findings.Keys == nil || p.findings.Delete == nil { + return + } + keys, err := p.findings.Keys() + if err != nil { + p.logf("the findings %s are not read, so none is cleared this pass: %v", assign.KindActingCycle, err) + return + } + for _, key := range keys { + if strings.HasSuffix(key, "."+assign.KindActingCycle) && !raised[key] { + if err := p.findings.Delete(key); err != nil { + p.logf("the finding %s is not cleared: %v", key, err) + continue + } + for said := range p.said { + if strings.HasPrefix(said, key+"=") { + delete(p.said, said) + } + } + } + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/actingwaits_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/actingwaits_test.go new file mode 100644 index 00000000..762332aa --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/actingwaits_test.go @@ -0,0 +1,184 @@ +package main + +import ( + "context" + "encoding/json" + "strings" + "testing" + "time" + + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + mcseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" + mwc "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient" + meshwants "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" + + "mesh-delivery-assigner/internal/acting" + "mesh-delivery-assigner/internal/assign" + "mesh-delivery-assigner/internal/buckets" +) + +// Design 47 G §266, §268, §276 (AS9): an entry with an acting edge in composing waits for the part's version to run on the edge's node; +// the wait is kept, said as wait-held with its bound and clock-from, read by assigner_waits, released by assigner_release with how +// `now` and ended when the version runs. +func TestAnEntryWaitsForItsActingPartAndTheWaitIsKeptSaidAndEnded(t *testing.T) { + now := time.Date(2026, 11, 12, 9, 0, 0, 0, time.UTC) + p := newPasser(kvSet{buckets.Waits(): &memKV{values: map[string][]byte{}}}, now) + var events []string + var bodies []meshwants.Wait + p.publishWants = func(event string, body any, id string) error { + events = append(events, event) + bodies = append(bodies, body.(meshwants.Wait)) + return nil + } + entry := meshwants.AssignmentEntry{Node: "ace", Module: "nats", Fp: fpTest, Edges: []meshwants.Edge{ + {Kind: "acting", Node: "ace", Module: "mesh-host", Fp: "src2:new", Seat: "node-engine", Step: "compose", Bound: "15m"}, + {Kind: "acting", Node: "ace", Module: "indexer", Fp: "src2:idx", Seat: "mesh-catalog", Step: "take-in"}}} + p.graph = &graphKeeper{} + p.graph.computed.Revision.Entries = []meshwants.NodeEntryRef{{Node: "ace", Module: "nats", Digest: "d1"}} + p.graph.computed.Entries = map[string]meshwants.AssignmentEntry{"d1": entry} + n := assign.Nodes{Online: map[string]bool{"ace": true}, Running: map[string]map[string]nodeengine.Running{"ace": {"mesh-host": {Fingerprint: "src2:old"}}}} + wanted := map[string]map[string]mwc.Want{"nats": {"ace": {Fp: "src2:older"}}} + ctx := context.Background() + + if got := p.actingOf("nats", nil)("ace"); len(got) != 1 || got[0].Part != "mesh-host" { + t.Fatalf("the assigner holds composing only: %+v", got) + } + p.actingWaits(ctx, n, wanted) + p.actingWaits(ctx, n, wanted) // said once + if len(events) != 1 || events[0] != meshwants.EventWaitHeld { + t.Fatalf("events %v", events) + } + b := bodies[0] + if b.Step != "compose" || b.Part != "mesh-host" || b.Fp != "src2:new" || b.Bound != "15m0s" || b.ClockFrom == nil || !b.ClockFrom.Equal(now) || b.Owner == "" { + t.Errorf("wait-held %+v", b) + } + key := acting.Key("ace", "nats", "mesh-host") + if _, err := acting.Decode(p.w.read(buckets.Waits())[key]); err != nil { + t.Fatalf("the wait is kept: %v", err) + } + // a release by hand lets the entry move, and ends the wait once + w, _ := acting.Decode(p.w.read(buckets.Waits())[key]) + w.Released = true + raw, _ := acting.Encode(w) + p.w.put(buckets.Waits(), key, raw) + if got := p.actingOf("nats", p.actingReleased())("ace"); len(got) != 0 { + t.Errorf("a released wait still holds: %+v", got) + } + p.actingWaits(ctx, n, wanted) + p.actingWaits(ctx, n, wanted) + if len(events) != 2 || events[1] != meshwants.EventWaitEnded || bodies[1].How != acting.HowReleased { + t.Fatalf("events %v bodies %+v", events, bodies) + } + // the version runs: the record goes, said nothing more + n.Running["ace"]["mesh-host"] = nodeengine.Running{Fingerprint: "src2:new"} + p.actingWaits(ctx, n, wanted) + if len(p.w.read(buckets.Waits())) != 0 || len(events) != 2 { + t.Errorf("kept %v events %v", p.w.read(buckets.Waits()), events) + } +} + +func TestAnActingWaitOfANodeAwayStartsNoClockAndAnEntryWantedAlreadyWaitsNot(t *testing.T) { + p := newPasser(kvSet{buckets.Waits(): &memKV{values: map[string][]byte{}}}, time.Now()) + p.publishWants = func(string, any, string) error { t.Fatal("said"); return nil } + p.graph = &graphKeeper{} + p.graph.computed.Revision.Entries = []meshwants.NodeEntryRef{{Node: "ace", Module: "nats", Digest: "d1"}} + p.graph.computed.Entries = map[string]meshwants.AssignmentEntry{"d1": {Node: "ace", Module: "nats", Fp: "src2:a", Edges: []meshwants.Edge{ + {Kind: "acting", Node: "ace", Module: "mesh-host", Fp: "src2:new", Step: "compose"}}}} + n := assign.Nodes{Online: map[string]bool{}} + p.actingWaits(context.Background(), n, map[string]map[string]mwc.Want{}) + n.Online["ace"] = true + p.actingWaits(context.Background(), n, map[string]map[string]mwc.Want{"nats": {"ace": {Fp: "src2:a"}}}) + if len(p.w.read(buckets.Waits())) != 0 || strings.Contains(p.graph.computed.Entries["d1"].Fp, "x") { + t.Errorf("kept %v", p.w.read(buckets.Waits())) + } +} + +func TestTheToolsReadAndReleaseAnActingWait(t *testing.T) { + var acts []string + s := newPutTools(memStore{waitsState: &memKV{values: map[string][]byte{}}, buckets.DeliveryWaitsState: &memKV{values: map[string][]byte{}}, buckets.DeliveredAgainState: &memKV{values: map[string][]byte{}}}, func(key string, body any) (json.RawMessage, error) { + acts = append(acts, key) + return json.RawMessage(`{"ok":true,"log":"logged"}`), nil + }) + w := acting.Wait{Step: "compose", Node: "ace", Module: "nats", Part: "mesh-host", PartNode: "ace", Fp: "src2:new", Entry: "src2:a", Since: time.Now(), ClockFrom: time.Now(), Bound: "30m0s"} + raw, _ := acting.Encode(w) + s.st.put(waitsState, acting.Key("ace", "nats", "mesh-host"), raw) + got, err := s.doWaits(map[string]any{"module": "nats"}) + if list := got.(map[string]any)["waits"].([]any); err != nil || len(list) != 1 || list[0].(actingSaid).BoundEnds == "" { + t.Fatalf("assigner_waits = %+v %v", got, err) + } + if _, err := s.doRelease(map[string]any{"node": "ace", "module": "nats", "how": "now"}, "x"); err == nil { + t.Error("a release with no why is taken") + } + if _, err := s.doRelease(map[string]any{"node": "ace", "module": "nats", "how": "now", "why": "the new engine is known good"}, "shanks.node-tools"); err != nil || len(acts) == 0 { + t.Fatalf("%v %v", err, acts) + } + raw, _, _, _ = s.st.get(waitsState, acting.Key("ace", "nats", "mesh-host")) + if r, _ := acting.Decode(raw); !r.Released || !strings.Contains(r.By, "known good") { + t.Errorf("wait = %+v", r) + } +} + +// Design 47 G §268, A-323, A-330: the assigner and the node-engine on a node are one step: the node-engine's own want waits for no +// acting part, so it moves first, and the node's other wants, the assigner's own included, wait for the new node-engine; a want held +// on a cycle no rule breaks is the finding acting-cycle (scope module, id .) naming each edge's step, cleared by the +// next pass that finds it on none. +func TestTheNodeEngineIsWantedFirstAndAWantHeldOnACycleIsAFinding(t *testing.T) { + p := newPasser(kvSet{buckets.Waits(): &memKV{values: map[string][]byte{}}}, time.Date(2026, 11, 13, 2, 0, 0, 0, time.UTC)) + p.publishWants = func(string, any, string) error { return nil } + findings := map[string]mcseat.Finding{} + p.findings = assign.Findings{ + Put: func(f mcseat.Finding) error { findings[f.Scope+"."+f.ID+"."+f.Kind] = f; return nil }, + Delete: func(key string) error { delete(findings, key); return nil }, + Keys: func() ([]string, error) { + var keys []string + for k := range findings { + keys = append(keys, k) + } + return keys, nil + }} + compose := meshwants.Edge{Kind: "acting", Node: "ace", Module: "mesh-host", Fp: "src2:engine", Seat: "node-engine", Step: "compose"} + place := meshwants.Edge{Kind: "acting", Node: "ace", Module: "assigner", Fp: "src2:assigner", Seat: "mesh-wants", Step: "place"} + entries := map[string]meshwants.AssignmentEntry{ + "d1": {Node: "ace", Module: "mesh-host", Fp: "src2:engine", Edges: []meshwants.Edge{place}}, + "d2": {Node: "ace", Module: "assigner", Fp: "src2:assigner", Edges: []meshwants.Edge{compose}}, + "d3": {Node: "ace", Module: "app", Fp: "src2:app", Edges: []meshwants.Edge{compose, place}}, + } + p.graph = &graphKeeper{} + for d, e := range entries { + p.graph.computed.Revision.Entries = append(p.graph.computed.Revision.Entries, meshwants.NodeEntryRef{Node: e.Node, Module: e.Module, Digest: d}) + } + p.graph.computed.Entries = entries + n := assign.Nodes{Online: map[string]bool{"ace": true}, Running: map[string]map[string]nodeengine.Running{"ace": { + "mesh-host": {Fingerprint: "src2:old"}, "assigner": {Fingerprint: "src2:old"}}}} + wanted := map[string]map[string]mwc.Want{} + if got := p.actingOf("mesh-host", nil)("ace"); len(got) != 0 { + t.Errorf("the node-engine's want is held by %+v, want by no acting edge", got) + } + if got := p.actingOf("assigner", nil)("ace"); len(got) != 1 || got[0].Part != "mesh-host" { + t.Errorf("the assigner's own want is held by %+v, want by the new node-engine", got) + } + p.actingWaits(context.Background(), n, wanted) + kept := p.w.read(buckets.Waits()) + if kept[acting.Key("ace", "mesh-host", "assigner")] != nil || kept[acting.Key("ace", "assigner", "mesh-host")] == nil || kept[acting.Key("ace", "app", "mesh-host")] == nil { + t.Errorf("the waits kept: %v", kept) + } + if len(findings) != 0 { + t.Errorf("the cycle the node-engine's want breaks is a finding: %v", findings) + } + // Two parts holding each other across compose and place, neither the node-engine: no rule breaks it. + entries["d4"] = meshwants.AssignmentEntry{Node: "ace", Module: "x", Fp: "src2:x", Edges: []meshwants.Edge{{Kind: "acting", Node: "ace", Module: "y", Fp: "src2:y", Seat: "s-y", Step: "place"}}} + entries["d5"] = meshwants.AssignmentEntry{Node: "ace", Module: "y", Fp: "src2:y", Edges: []meshwants.Edge{{Kind: "acting", Node: "ace", Module: "x", Fp: "src2:x", Seat: "s-x", Step: "compose"}}} + p.graph.computed.Revision.Entries = append(p.graph.computed.Revision.Entries, meshwants.NodeEntryRef{Node: "ace", Module: "x", Digest: "d4"}, + meshwants.NodeEntryRef{Node: "ace", Module: "y", Digest: "d5"}) + p.actingWaits(context.Background(), n, wanted) + f, ok := findings["module.ace.x.acting-cycle"] + if len(findings) != 2 || !ok || !strings.Contains(f.Summary, "ace/x -[place]-> ace/y -[compose]-> ace/x") { + t.Fatalf("the findings of a cycle no rule breaks: %v", findings) + } + // x's new version runs: y no longer waits, so neither is held on a cycle, and both findings clear. + n.Running["ace"]["x"] = nodeengine.Running{Fingerprint: "src2:x"} + p.actingWaits(context.Background(), n, wanted) + if len(findings) != 0 { + t.Errorf("the findings after the cycle broke: %v", findings) + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/askagain_tool.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/askagain_tool.go index 762a44b1..dcc18206 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/askagain_tool.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/askagain_tool.go @@ -10,8 +10,8 @@ import ( occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" - "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-teller/tellerevents" stdio "git.novox.be/novox/mesh-sdk/go" @@ -36,7 +36,7 @@ const ( func askAgainByHand(st store, ask func(string, any) (json.RawMessage, error), key, why, by string, now time.Time) (any, error) { switch { case key == "": - return nil, errors.New("name the ask's key, as assigner_read_asks lists it") + return nil, errors.New("name the ask's key, as the tool asks lists it") case why == "": return nil, errors.New("a hand ask again says why") case by == "" || by == stdio.Unproven: @@ -47,7 +47,7 @@ func askAgainByHand(st store, ask func(string, any) (json.RawMessage, error), ke return nil, err } if !found { - return nil, fmt.Errorf("the assigner holds no ask under %s: assigner_read_asks lists them", key) + return nil, fmt.Errorf("the assigner holds no ask under %s: the tool asks lists them", key) } _, routerAsk, err := assign.DecodeAsk(key, current) if err != nil { @@ -90,7 +90,7 @@ func askAgainTools(st store, ask func(string, any) (json.RawMessage, error), now "under key is written again under a new asking, which the teller asks on the operator channel, as when the channel says it " + "ended without an answer. Refused for an asking the operator answered, a proposal's authorising ask (the seat's verb again " + "asks it) and a record ask a put asked. Recorded as a hand act.", - Input: schema(map[string]string{"key": "the ask's key, as assigner_read_asks lists it", "why": "why it is asked again"}, + Input: schema(map[string]string{"key": "the ask's key, as the tool asks lists it", "why": "why it is asked again"}, []string{"key", "why"}), Run: func(a map[string]any) (any, error) { key, _ := a["key"].(string) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/askended.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/askended.go index 3b9d5ba1..3573bc89 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/askended.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/askended.go @@ -18,7 +18,6 @@ import ( oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" "mesh-delivery-assigner/internal/assign" - plc "mesh-delivery-assigner/internal/assignment" "mesh-delivery-assigner/internal/buckets" "mesh-delivery-assigner/internal/puts" ) @@ -49,7 +48,7 @@ func staleOfWorld(w *world) staleOf { func hearAskEnded(ctx context.Context, js jetstream.JetStream, hub *process.EventHub, asksKV jetstream.KeyValue, stale staleOf, logf func(string, ...any)) error { return hub.Handle(askEndedSubject, func(m jetstream.Msg) { var putsKV jetstream.KeyValue - if kv, err := js.KeyValue(ctx, buckets.Puts); err == nil { + if kv, err := js.KeyValue(ctx, buckets.Puts()); err == nil { putsKV = kv } if err := askAgain(ctx, asksKV, putsKV, stale, m.Data(), time.Now(), logf); err != nil { @@ -119,11 +118,7 @@ func (cw callWait) keep(ctx context.Context, id, callee string, call func() (jso if said || clock().Sub(since) < mcc.CallUnansweredAfter || cw.heard == nil || !cw.heard() || cw.findings.Put == nil { continue } - o := mcc.CallUnanswered(id, callee, "", err.Error(), "assigner."+mcc.KindCallUnanswered, since) - value, eerr := mcc.EncodeFinding(o.Finding(plc.Module, "", clock().UTC())) - if eerr == nil { - eerr = cw.findings.Put(key, value) - } + eerr := cw.findings.Put(assign.CallUnanswered(id, callee, "", err.Error(), "assigner."+mcc.KindCallUnanswered, since).Finding(clock())) if eerr != nil { logf("the finding %s is not written: %v", key, eerr) continue @@ -166,7 +161,7 @@ func endedAtBind(ctx context.Context, js jetstream.JetStream, asksKV jetstream.K } var putsKV jetstream.KeyValue if js != nil { - if kv, err := js.KeyValue(ctx, buckets.Puts); err == nil { + if kv, err := js.KeyValue(ctx, buckets.Puts()); err == nil { putsKV = kv } } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/asksform_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/asksform_test.go new file mode 100644 index 00000000..67549841 --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/asksform_test.go @@ -0,0 +1,74 @@ +package main + +import ( + "encoding/json" + "testing" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" +) + +// Design 47 G §717: the assigner's asks are the seat mesh-wants' state asks, so every key its asks package writes is one of +// that state's key forms, read back to the same parts, and each value is the seat's Ask. +func TestTheAssignersAsksAreTheSeatMeshWantsAsks(t *testing.T) { + const hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + const fp, digest = "src2:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + must := func(k string, err error) string { + t.Helper() + if err != nil { + t.Fatal(err) + } + return k + } + three := func(parse func(string) (string, string, string, bool)) func(string) []string { + return func(k string) []string { + if a, b, c, ok := parse(k); ok { + return []string{a, b, c} + } + return nil + } + } + for _, row := range []struct { + key string + parse func(string) []string + want []string + }{ + {must(asks.HoldAskKey("web", fp)), func(k string) []string { + if a, b, ok := meshwants.ParseAsksByHoldModuleFingerprintKey(k); ok { + return []string{a, b} + } + return nil + }, []string{"web", hex}}, + {must(asks.RestoreAskKey("ace", "web", fp)), three(meshwants.ParseAsksByRestoreNodeModuleFingerprintKey), []string{"ace", "web", hex}}, + {must(asks.BackupAskKey("ace", "web", fp)), three(meshwants.ParseAsksByBackupNodeModuleFingerprintKey), []string{"ace", "web", hex}}, + {must(asks.OutsideAskKey("ace", "web", fp)), three(meshwants.ParseAsksByOutsideNodeModuleFingerprintKey), []string{"ace", "web", hex}}, + {must(asks.RevertAskKey("ace", "web", fp)), three(meshwants.ParseAsksByRevertNodeModuleFingerprintKey), []string{"ace", "web", hex}}, + {must(asks.RecordAskKey("ace", "web", digest)), three(meshwants.ParseAsksByRecordNodeModuleDigestKey), []string{"ace", "web", "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}, + {must(asks.AssignmentsAskKey("p-20261028t183000-1a2b3c")), func(k string) []string { + if a, ok := meshwants.ParseAsksByAssignmentsProposalKey(k); ok { + return []string{a} + } + return nil + }, []string{"p-20261028t183000-1a2b3c"}}, + } { + got := row.parse(row.key) + if len(got) != len(row.want) { + t.Errorf("%s is no key form of mesh-wants' asks: read as %v", row.key, got) + continue + } + for i := range got { + if got[i] != row.want[i] { + t.Errorf("%s reads as %v, want %v", row.key, got, row.want) + break + } + } + } + key := must(asks.HoldAskKey("web", fp)) + raw, err := asks.EncodeAsk(key, asks.Ask{Ask: json.RawMessage(`{"id":"x"}`), Asking: 2}) + if err != nil { + t.Fatal(err) + } + if v, err := meshwants.DecodeAsksByHoldModuleFingerprintValue(raw); err != nil || v.Asking != 2 { + t.Errorf("the ask %s is not the seat's Ask: %+v, %v", raw, v, err) + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildended.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildended.go new file mode 100644 index 00000000..5c09da9d --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildended.go @@ -0,0 +1,170 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "sync" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/process" + "github.com/nats-io/nats.go/jetstream" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "mesh-delivery-assigner/internal/assign" + "mesh-delivery-assigner/internal/assignment" +) + +// What a want names of a build is heard, never read from the builder's state (design 47 G §379, §526, A-94, A-96; ADR 0596 §2): the +// builder's event `build.ended` (a done build's parts by digest, its keeping node and its branch) and the catalog's +// `version-released` (a released version and each part's digest, with the source revision it was built from), each kept as heard. + +// builtEnds are the done builds' ends heard, by module and fingerprint (hearBuildEnded). +type builtEnds struct { + mu sync.Mutex + ends map[string]builds.BuildEnded +} + +func (b *builtEnds) key(module, fp string) string { return module + "@" + fp } + +// put takes one build.ended in. +func (b *builtEnds) put(data []byte) error { + var e builds.BuildEnded + if err := json.Unmarshal(data, &e); err != nil { + return fmt.Errorf("a build.ended that is no end: %w", err) + } + if e.Module == "" || e.Fingerprint == "" { + return fmt.Errorf("a build's end naming no module or fingerprint: %s", data) + } + if e.Outcome == "" { // an end said before A-162 is a done build's + e.Outcome = builds.BuildDone + } + b.mu.Lock() + defer b.mu.Unlock() + if b.ends == nil { + b.ends = map[string]builds.BuildEnded{} + } + b.ends[b.key(e.Module, e.Fingerprint)] = e + return nil +} + +// endOf is the end heard of module's build at fp, done or failed (A-162): none while the build has not ended. +func (b *builtEnds) endOf(module, fp string) (builds.BuildEnded, bool) { + b.mu.Lock() + defer b.mu.Unlock() + e, ok := b.ends[b.key(module, fp)] + return e, ok +} + +// partsOf is the parts of module's build at fp as its end names them (each artifact's digest by `:`), with the +// node that keeps it, for a kept build or a build of another watched branch; false for a build of the trunk not kept, whose +// parts the version's release says (releasedVersions). +func (b *builtEnds) partsOf(module, fp string) (assign.KeptBuild, bool) { + b.mu.Lock() + e, ok := b.ends[b.key(module, fp)] + b.mu.Unlock() + if !ok || (e.KeptOn == "" && e.Branch == "") { + return assign.KeptBuild{}, false + } + parts := map[string]string{} + for name, digest := range e.Made { + parts[module+":"+name] = digest + } + return assign.KeptBuild{Parts: parts, KeptOn: e.KeptOn}, true +} + +// releasedVersions are the catalog's version-released heard, by module and source revision. +type releasedVersions struct { + mu sync.Mutex + seen map[string]assign.KeptBuild + // newest is, by module, the last version-released heard of it (delivered.go). + newest map[string]releaseHeard +} + +func (r *releasedVersions) key(module string, source int64) string { + return fmt.Sprintf("%s@%d", module, source) +} + +// put takes one version-released in. +func (r *releasedVersions) put(data []byte) error { + var e meshcatalog.VersionReleasedEvent + if err := json.Unmarshal(data, &e); err != nil { + return fmt.Errorf("a version-released that is none: %w", err) + } + if e.Module == "" || e.Version == "" { + return fmt.Errorf("a version-released naming no module or version: %s", data) + } + parts := map[string]string{} + for _, a := range e.Artifacts { + parts[e.Module+":"+a.Artifact] = a.Digest + } + r.mu.Lock() + defer r.mu.Unlock() + if r.seen == nil { + r.seen = map[string]assign.KeptBuild{} + } + r.seen[r.key(e.Module, e.Source)] = assign.KeptBuild{Version: e.Version, Parts: parts} + if r.newest == nil { + r.newest = map[string]releaseHeard{} + } + h := releaseHeard{Version: e.Version, Parts: parts, Source: e.Source} + if e.At != nil { + h.At = *e.At + } + r.newest[e.Module] = h // the stream's order is the order of release + return nil +} + +// of is the version of module released from the source revision, with its parts' digests; false before it is heard. +func (r *releasedVersions) of(module string, source int64) (assign.KeptBuild, bool) { + r.mu.Lock() + defer r.mu.Unlock() + k, ok := r.seen[r.key(module, source)] + return k, ok +} + +// buildsStreamName is the builder's stream of build ends as the bus names it: kept last per subject (one end per module and +// fingerprint, its manifest's `last-per-subject`, design 47 G A-181, §703). +var buildsStreamName = catalogue.StreamBusName(builds.BuildsStream) + +// hearBuilds reads the builder's stream of build ends whole at the start and then follows it (design 47 G A-181, §556): what was +// built, and a failed build's error, survive the assigner's restart, and it reads none of the builder's buckets. The read ends before +// it returns, so the first pass knows every build; a stream it cannot hear is said, and no want names a build until the assigner +// starts again. +func hearBuilds(ctx context.Context, js jetstream.JetStream, ends *builtEnds, wake func(), findings assign.Findings, logf func(string, ...any)) { + acct := process.Account{Node: thisNode, Module: assignment.Module()} + nh := notHeardWriter(findings, logf) + cc, err := acct.HearStream(ctx, js, buildsStreamName, func(m jetstream.Msg) { + if err := ends.put(m.Data()); err != nil { + logf("%s is not kept: %v", m.Subject(), err) + } + if err := m.Ack(); err != nil { + logf("acknowledging %s: %v", m.Subject(), err) + } + wake() + }, logf) + if err != nil { + nh.NotHeard(buildsStreamName, "what the builder built is not heard until the assigner starts again, so no want names a build's parts or its keeping node and no put goes on", err) + return + } + nh.Heard(buildsStreamName) + go func() { + <-ctx.Done() + cc.Stop() + }() +} + +// hearTaken registers the hub's taker of the indexer's take-in.taken, each waking the pass (orderwaits.go, §298, A-260); before +// the hub starts. +func hearTaken(hub *process.EventHub, taken *takenSources, wake func(), logf func(string, ...any)) error { + return hub.Handle(takenSubject, func(m jetstream.Msg) { + if err := taken.put(m.Data()); err != nil { + logf("%s is not kept: %v", m.Subject(), err) + } + if err := m.Ack(); err != nil { + logf("acknowledging %s: %v", m.Subject(), err) + } + wake() + }) +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildended_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildended_test.go new file mode 100644 index 00000000..7a86f9b3 --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildended_test.go @@ -0,0 +1,110 @@ +package main + +import ( + "encoding/json" + "reflect" + "testing" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" + "mesh-delivery-assigner/internal/assign" +) + +// Design 47 G §379, §526, A-96: a build.ended heard names a kept build's or another branch's parts by digest (`:`) and its +// keeping node; a build of the trunk not kept names none, its parts being the release's. +func TestABuildEndedHeardNamesAKeptBuildsPartsAndItsNode(t *testing.T) { + put := func(b *builtEnds, e builds.BuildEnded) { + raw, _ := json.Marshal(e) + if err := b.put(raw); err != nil { + t.Fatal(err) + } + } + made := map[string]string{"program": "sha256:1"} + var b builtEnds + put(&b, builds.BuildEnded{Module: "node-engine", Fingerprint: "a", Made: made}) + if _, ok := b.partsOf("node-engine", "a"); ok { + t.Error("a trunk build not kept names parts") + } + put(&b, builds.BuildEnded{Module: "node-engine", Fingerprint: "b", Made: made, KeptOn: "ace"}) + if k, ok := b.partsOf("node-engine", "b"); !ok || k.KeptOn != "ace" || !reflect.DeepEqual(k.Parts, map[string]string{"node-engine:program": "sha256:1"}) { + t.Errorf("kept build = %+v, %v", k, ok) + } + put(&b, builds.BuildEnded{Module: "node-engine", Fingerprint: "c", Made: made, Branch: "cand"}) + if k, ok := b.partsOf("node-engine", "c"); !ok || k.KeptOn != "" || len(k.Parts) != 1 { + t.Errorf("branch build = %+v, %v", k, ok) + } + if err := b.put([]byte(`{"module":"x"}`)); err == nil { + t.Error("an end naming no fingerprint is kept") + } +} + +// Design 47 G §379, A-96: a version-released heard names the version and the parts' digests of the module's source revision. +func TestAVersionReleasedHeardNamesItsVersionAndPartsBySource(t *testing.T) { + var r releasedVersions + raw, _ := json.Marshal(meshcatalog.VersionReleasedEvent{Module: "mail", Source: 7, Version: "1.2.3", Artifacts: []meshcatalog.VersionArtifact{{Artifact: "image", Digest: "sha256:9"}}}) + if err := r.put(raw); err != nil { + t.Fatal(err) + } + want := assign.KeptBuild{Version: "1.2.3", Parts: map[string]string{"mail:image": "sha256:9"}} + if k, ok := r.of("mail", 7); !ok || !reflect.DeepEqual(k, want) { + t.Errorf("released = %+v, %v", k, ok) + } + if _, ok := r.of("mail", 8); ok { + t.Error("another source revision names a version") + } +} + +// Design 47 G §379, A-96: the trunk's entry takes its version and parts from the version-released of the kept assignment graph +// revision's source; a kept build from its end; no revision kept, or another fingerprint, names none. +func TestATrunkEntryTakesItsVersionFromTheReleaseOfTheKeptRevisionsSource(t *testing.T) { + w := newWorld() + raw, _ := json.Marshal(meshcatalog.VersionReleasedEvent{Module: "mail", Source: 7, Version: "1.2.3", Artifacts: []meshcatalog.VersionArtifact{{Artifact: "image", Digest: "sha256:9"}}}) + if err := w.released.put(raw); err != nil { + t.Fatal(err) + } + endRaw, _ := json.Marshal(builds.BuildEnded{Module: "mail", Fingerprint: "k", Made: map[string]string{"image": "sha256:2"}, KeptOn: "g14"}) + if err := w.ends.put(endRaw); err != nil { + t.Fatal(err) + } + p := &passer{w: w, graph: &graphKeeper{ok: true, source: 7}} + named := p.namedBuild("mail", "t") + if k, ok := named("t"); !ok || k.Version != "1.2.3" || k.Parts["mail:image"] != "sha256:9" || k.KeptOn != "" { + t.Errorf("trunk entry = %+v, %v", k, ok) + } + if k, ok := named("k"); !ok || k.KeptOn != "g14" || k.Version != "" { + t.Errorf("kept build = %+v, %v", k, ok) + } + if _, ok := named("other"); ok { + t.Error("another fingerprint names a release") + } + p.graph.source = 8 + if _, ok := named("t"); ok { + t.Error("a source with no release names a version") + } + p.graph.ok = false + if _, ok := p.namedBuild("mail", "t")("t"); ok { + t.Error("no revision kept names a release") + } +} + +// A-162: the assigner knows a failed build from its `build.ended`: the error whole; a done end or none heard is not failed. +func TestTheStandingOfABuildIsHeardFromItsEnd(t *testing.T) { + w := newWorld() + say := func(e builds.BuildEnded) { + raw, _ := json.Marshal(e) + if err := w.ends.put(raw); err != nil { + t.Fatal(err) + } + } + if failed, _ := w.buildStanding("web", "fp1"); failed { + t.Error("a build nothing was heard of failed") + } + say(builds.BuildEnded{Module: "web", Fingerprint: "fp1", Outcome: builds.BuildFailed, Error: "transport fault: connection"}) + if failed, why := w.buildStanding("web", "fp1"); !failed || why != "transport fault: connection" { + t.Errorf("failed end: failed %v why %q", failed, why) + } + say(builds.BuildEnded{Module: "web", Fingerprint: "fp1", Outcome: builds.BuildDone}) + if failed, _ := w.buildStanding("web", "fp1"); failed { + t.Error("a later done end leaves the build failed") + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildends_helper_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildends_helper_test.go new file mode 100644 index 00000000..ce5afcda --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/buildends_helper_test.go @@ -0,0 +1,24 @@ +package main + +import ( + "encoding/json" + "testing" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" +) + +// endDone is the builder's `build.ended` of a done build of module at fp heard by w, naming the digest of its manifest (design 47 G +// A-181: what was built is heard, never read from the builder's bucket). +func endDone(t testing.TB, w *world, module, fp, manifest string) { + t.Helper() + data, err := json.Marshal(builds.BuildEnded{Module: module, Fingerprint: fp, Outcome: builds.BuildDone, Manifest: manifest}) + if err != nil { + t.Fatal(err) + } + if err := w.ends.put(data); err != nil { + t.Fatal(err) + } +} + +// trunkBranch is the first watched branch the tests give the world: the code names none (design 47 G §507). +const trunkBranch = "main" diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/candidateadded.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/candidateadded.go new file mode 100644 index 00000000..1bbfa391 --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/candidateadded.go @@ -0,0 +1,50 @@ +package main + +import ( + mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" + mwc "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient" + plc "mesh-delivery-assigner/internal/assignment" + "mesh-delivery-assigner/internal/buckets" +) + +// addedOf is the `added` of node's candidate (design 47 G A-316, §379, §421): by module, each module the candidate adds to node +// (laid over current) with no want there, the version and parts the want it would write carries, taken from the same code that +// writes a want's (namedBuild, the want's build at the fingerprint of the branch the want's code takes). A module whose build names none is not listed. +func (p *passer) addedOf(node string, laid *mma.Assignments, current map[string]mma.Assignments) map[string]mma.AddedModule { + if laid == nil { + return nil + } + placed := map[string]string{} + for _, m := range laid.Modules { + placed[m.Module] = m.Branch + } + var out map[string]mma.AddedModule + for _, module := range plc.ChangeOf(current[node], *laid).Added { + if key, err := mwc.WantKey(node, module); err != nil || p.w.has(buckets.Wants(), key) { + continue + } + // The branch the want's code takes: the one the candidate's placement names, else the module's default (groups, wants). + branch := p.w.trunkName() + if p.ends.following() { + branch = p.ends.branchOf(module, placed[module]) + } + fp := p.w.trunkOf(branch)[module].Fp2 + if fp == "" { + continue + } + k, ok := p.namedBuild(module, fp)(fp) + if !ok { + continue + } + if out == nil { + out = map[string]mma.AddedModule{} + } + out[module] = mma.AddedModule{Version: k.Version, Parts: k.Parts} + } + return out +} + +// candidateOf is the candidate of proposal id for node: the assignments laid over it, and what it adds (addedOf). +func (p *passer) candidateOf(id, node string, laid *mma.Assignments, current map[string]mma.Assignments) mma.CandidatesByProposalNodeValue { + return mma.CandidatesByProposalNodeValue{Proposal: id, Assignments: laid, Added: p.addedOf(node, laid, current)} +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/candidateadded_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/candidateadded_test.go new file mode 100644 index 00000000..b6a9505f --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/candidateadded_test.go @@ -0,0 +1,59 @@ +package main + +import ( + "encoding/json" + "reflect" + "testing" + + mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/graph" + "mesh-delivery-assigner/internal/buckets" +) + +// Design 47 G A-316: the candidate of a node carries, for each module it adds there without a want, the version and parts the +// want it would write carries; a module with a want there, or a build that names none, is not listed. +func TestACandidateThatAddsAModuleCarriesItsParts(t *testing.T) { + w := newWorld() + for _, m := range []string{"mail", "web", "db"} { + w.putModule("main", m, meshcatalog.ModulesByModuleValue{Fp2: "fp-" + m}, graph.Node{}, nil) + } + w.first = "main" + rel, _ := json.Marshal(meshcatalog.VersionReleasedEvent{Module: "mail", Source: 7, Version: "1.2.3", Artifacts: []meshcatalog.VersionArtifact{{Artifact: "image", Digest: "sha256:9"}}}) + if err := w.released.put(rel); err != nil { + t.Fatal(err) + } + end, _ := json.Marshal(builds.BuildEnded{Module: "db", Fingerprint: "fp-db", Made: map[string]string{"program": "sha256:3"}, KeptOn: "g14"}) + if err := w.ends.put(end); err != nil { + t.Fatal(err) + } + w.put(buckets.Wants(), "want.ace.web", []byte(`{}`)) + p := &passer{w: w, graph: &graphKeeper{ok: true, source: 7}} + laid := &mma.Assignments{Node: "ace", Modules: []mma.PlacedModule{{Module: "mail"}, {Module: "web"}, {Module: "db"}, {Module: "old"}}} + current := map[string]mma.Assignments{"ace": {Node: "ace", Modules: []mma.PlacedModule{{Module: "old"}}}} + c := p.candidateOf("p1", "ace", laid, current) + want := map[string]mma.AddedModule{ + "mail": {Version: "1.2.3", Parts: map[string]string{"mail:image": "sha256:9"}}, + "db": {Parts: map[string]string{"db:program": "sha256:3"}}, + } + if c.Proposal != "p1" || c.Assignments != laid || !reflect.DeepEqual(c.Added, want) { + t.Errorf("candidate = %+v, added want %+v", c, want) + } + if got := p.candidateOf("p1", "ace", nil, current); got.Added != nil { + t.Errorf("a candidate taking the key away adds %+v", got.Added) + } + // A placement on a watched branch is added at that branch's fingerprint, as the want's code takes it, not the trunk's. + w.putModule("feat", "mail", meshcatalog.ModulesByModuleValue{Fp2: "fp-feat"}, graph.Node{}, nil) + feat, _ := json.Marshal(builds.BuildEnded{Module: "mail", Fingerprint: "fp-feat", Made: map[string]string{"program": "sha256:4"}, KeptOn: "g14"}) + if err := w.ends.put(feat); err != nil { + t.Fatal(err) + } + p.ends = newPipelineEnds(newMemEnds(), func() {}, t.Logf) + p.ends.trunk = w.trunkName + p.ends.on.Store(true) + onFeat := &mma.Assignments{Node: "g14", Modules: []mma.PlacedModule{{Module: "mail", Branch: "feat"}}} + if got, want := p.candidateOf("p2", "g14", onFeat, nil).Added, map[string]mma.AddedModule{"mail": {Parts: map[string]string{"mail:program": "sha256:4"}}}; !reflect.DeepEqual(got, want) { + t.Errorf("a placement on branch feat adds %+v, want %+v", got, want) + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/candidates.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/candidates.go new file mode 100644 index 00000000..c19cfb45 --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/candidates.go @@ -0,0 +1,113 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "maps" + "strings" + + mcc "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-sdk/go/buscalls" + + "mesh-delivery-assigner/internal/assigngraph" +) + +// A run's candidate assignment graph (design 47 G §189, §190, A-201, A-235): the candidate `run-`'s catalog graph revision, +// handed by meshcatalogclient's candidates hook, is read whole through the catalog seat's verb revision; the assignment graph is +// computed from it in place of its branch's half, beside every other followed branch's, and kept under candidate..; +// the runner's group-ended of the run drops it. It writes no want and says no event; the gate's heads-replaced drops nothing. + +// runGroupPrefix is a run on request's candidate group before its id, `run-` (A-160). +const runGroupPrefix = "run-" + +// readCandidateHalf reads a candidate's catalog graph revision whole through the seat's verb revision, page by page, with the +// candidate's group and heads and the revision its event names. +func readCandidateHalf(call mcc.Call, ev meshcatalog.CatalogRevisionEvent) (mcc.Half, error) { + half := mcc.Half{Entries: map[string]meshcatalog.CatalogEntry{}} + after := "" + for { + a, err := askCandidatePage(call, meshcatalog.RevisionInput{Branch: ev.Branch, Group: ev.Group, Heads: ev.Heads, Revision: ev.Revision, After: after}) + if err != nil { + return half, err + } + if a.Revision == nil || a.Revision.Revision != ev.Revision || a.Revision.Group != ev.Group { + return half, fmt.Errorf("%s answered %+v for the candidate %s's revision %d", mcc.RevisionVerb, a.Revision, ev.Group, ev.Revision) + } + half.Revision = *a.Revision + for _, e := range a.Entries { + half.Entries[e.Module] = e + } + if a.Next == "" { + return half, nil + } + after = a.Next + } +} + +// keepCandidate computes and keeps the candidate's assignment graph of a run's catalog-revision; another candidate's (a try's, a +// group's) is passed over. +func (p *passer) keepCandidate(ctx context.Context, call mcc.Call, ev meshcatalog.CatalogRevisionEvent) error { + if !strings.HasPrefix(ev.Group, runGroupPrefix) { + return nil + } + g := p.graph + if g == nil || g.objects == nil || g.halves == nil { + return errors.New("the assigner keeps no assignment graph, so no candidate's is kept") + } + half, err := readCandidateHalf(call, ev) + if err != nil { + return fmt.Errorf("the candidate %s's revision %d is not read, so its assignment graph is not kept: %w", ev.Group, ev.Revision, err) + } + in := p.graphInputs() + in.Halves = maps.Clone(in.Halves) + in.Halves[half.Revision.Branch] = half + c := assigngraph.Compute(in) + for _, u := range c.Uncomputed { + p.logf("the candidate %s's assignment graph leaves out %s", ev.Group, u) + } + rev, fresh, err := assigngraph.KeepCandidate(ctx, g.objects, c, ev.Group, ev.Heads, meshcatalog.EventCatalogRevision, p.clock()) + if err != nil { + return fmt.Errorf("the candidate %s's assignment graph is not kept: %w", ev.Group, err) + } + if fresh { + p.logf("the candidate %s's assignment graph revision %d is kept, from its catalog graph revision %d", ev.Group, rev.Revision, ev.Revision) + } + return nil +} + +// dropCandidate removes a run's candidate assignment graph on the runner's group-ended of that run (A-235). +func (p *passer) dropCandidate(ctx context.Context, group string) error { + g := p.graph + if g == nil || g.objects == nil { + return nil + } + o, ok := g.objects.(assigngraph.Remover) + if !ok { + return fmt.Errorf("the objects %s remove nothing, so the candidate %s is kept", assigngraph.RevisionsObjects, group) + } + n, err := assigngraph.DropCandidate(ctx, o, group) + if err == nil && n > 0 { + p.logf("the candidate %s's assignment graph is dropped: its run ended", group) + } + return err +} + +// askCandidatePage is one page of a candidate's revision as the seat's verb revision answers it. +func askCandidatePage(call mcc.Call, in meshcatalog.RevisionInput) (meshcatalog.RevisionAnswer, error) { + raw, err := call(mcc.RevisionVerb, in) + if err != nil { + return meshcatalog.RevisionAnswer{}, err + } + answer, said, ok := buscalls.Unwrap(raw) + if !ok { + return meshcatalog.RevisionAnswer{}, fmt.Errorf("%s answered: %s", mcc.RevisionVerb, said) + } + var a meshcatalog.RevisionAnswer + if err := json.Unmarshal(answer, &a); err != nil { + return a, fmt.Errorf("%s answered no revision: %w", mcc.RevisionVerb, err) + } + return a, nil +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/composed.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/composed.go index b70b521c..ebffbad0 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/composed.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/composed.go @@ -5,7 +5,7 @@ import ( "strings" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" - "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/catalogkey" ) // Every node's published compositions, read only to compare (novox/hq ADR 0593 §8): each node's engine writes its own @@ -44,7 +44,7 @@ func published(values map[string][]byte) map[string][]publishedRow { _, module, part, isNode, err := nodeengineclient.ParseComposedKey(key) var fp string if err == nil && !isNode { - fp, err = meshcatalogclient.ParseKeyPart(part) + fp, err = catalogkey.ParseKeyPart(part) } switch { case err != nil: diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositioninputs.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositioninputs.go new file mode 100644 index 00000000..fa91aadb --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositioninputs.go @@ -0,0 +1,165 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "slices" + "strconv" + "sync" + "time" + + nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + meshwants "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// The inputs of a composition step (design 47 G §354, A-243, A-252): one request per machine, each carrying that machine's input, +// asked of the machine's node-engine by the seat's verb compose-candidate. + +// composeCandidateKey is the seat verb's call key, asked of one node. +const composeCandidateKey = "seat:" + ne.Seat + "." + ne.VerbComposeCandidate + +// candidateKeyOf is the key the step's pipeline names its candidate by (A-226): a run on request's `run-`, or for a branch +// pipeline the branch's catalog graph revision `.`, the branch escaped as a key part as the pipeline key does. +func candidateKeyOf(run meshwants.StepRun) (string, error) { + switch { + case run.Run != "": + return "run-" + run.Run, nil + case run.Branch == "" || run.Revision <= 0: + return "", errors.New("the step names neither a run on request nor a branch's revision, so no candidate is named") + } + return keys.EscapeName(run.Branch) + "." + strconv.FormatInt(run.Revision, 10), nil +} + +// machinesFor are the machines of a step (A-252): the nodes whose placement of the module names the step's branch (a placement +// naming none is the trunk's, trunk the first watched branch); a branch that is not watched takes the nodes placing the module on the trunk. In name order. +func machinesFor(placed map[string]map[string]string, module, branch, trunk string, watched bool) []string { + if branch == "" || !watched { + branch = trunk + } + var out []string + for node, b := range placed[module] { + if b == "" { + b = trunk + } + if b == branch { + out = append(out, node) + } + } + slices.Sort(out) + return out +} + +// machinesOf are the machines of a composition step in the world's assignments. +func (w *world) machinesOf(module, branch string) []string { + return machinesFor(w.placedBranches(), module, branch, w.trunkName(), w.holdsBranch(branch)) +} + +// nodeInput is one machine's input: its bytes, or why the node gave none, or that it is away. +type nodeInput struct { + Node string + Body []byte + // Why is set when the node gave no input: its words, or the call's error. + Why string + // Away is a machine the watchman said away: it is not asked (§64). + Away bool +} + +// compositionInputs asks the machines of a step for their inputs. +type compositionInputs struct { + machines func(module, branch string) []string + // call asks one node's seat verb (process.Conn.CallKeptOn). + call func(node, key string, body any) (json.RawMessage, error) + bound time.Duration +} + +// gather asks each machine of the step's module and branch for its input with the step's candidate key, and answers every machine in +// name order: one away (away) is not asked; a node that is silent, unready or answers no input has Why set; no machine at all is an +// error. The run keeps each request built from an input under its run ask (compositionRun.machine). +func (c *compositionInputs) gather(ctx context.Context, run meshwants.StepRun, away func(node string) bool) ([]nodeInput, error) { + key, err := candidateKeyOf(run) + if err != nil { + return nil, err + } + nodes := c.machines(run.Module, run.Branch) + if len(nodes) == 0 { + return nil, fmt.Errorf("no node places %s on the branch %q, so the composition step has no machine to compose", run.Module, run.Branch) + } + bound := c.bound + if bound <= 0 { + bound = nec.AnswerBound + } + out := make([]nodeInput, len(nodes)) + var wg sync.WaitGroup + for i, node := range nodes { + if away != nil && away(node) { + out[i] = nodeInput{Node: node, Away: true} + continue + } + wg.Add(1) + go func() { + defer wg.Done() + out[i] = c.one(ctx, bound, node, key) + }() + } + wg.Wait() + return out, nil +} + +// one asks one node for its input. +func (c *compositionInputs) one(ctx context.Context, bound time.Duration, node, key string) nodeInput { + in := nodeInput{Node: node} + at, cancel := context.WithTimeout(ctx, bound) + defer cancel() + type reply struct { + raw json.RawMessage + err error + } + got := make(chan reply, 1) + go func() { + raw, err := c.call(node, composeCandidateKey, ne.ComposeCandidateInput{Candidate: key, Input: true}) + got <- reply{raw, err} + }() + var raw json.RawMessage + select { + case <-at.Done(): + in.Why = "no answer within its bound: " + at.Err().Error() + return in + case r := <-got: + if r.err != nil { + in.Why = r.err.Error() + return in + } + raw = r.raw + } + var a ne.ComposeCandidateAnswer + // its numbers as written (json.Number), so the input kept is the engine's own bytes' values + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.UseNumber() + if err := dec.Decode(&a); err != nil { + in.Why = "its answer is not a compose-candidate answer: " + err.Error() + return in + } + switch { + case a.Verdict == nec.Unready: + in.Why = "unready: " + a.Refused + return in + case len(a.Input) == 0: + in.Why = "the engine answered no composition input" + if a.Refused != "" { + in.Why += ": " + a.Refused + } + return in + } + body, err := json.Marshal(a.Input) + if err != nil { + in.Why = "its composition input cannot be encoded again: " + err.Error() + return in + } + in.Body = body + return in +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositioninputs_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositioninputs_test.go new file mode 100644 index 00000000..fee01d44 --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositioninputs_test.go @@ -0,0 +1,96 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "strings" + "sync" + "testing" + "time" + + meshwants "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" +) + +// Design 47 G A-226: the candidate a step asks is `run-` for a run on request, else `.`. +func TestTheCandidateKeyOfAStep(t *testing.T) { + for _, c := range []struct { + run meshwants.StepRun + want string + fail bool + }{ + {run: meshwants.StepRun{Run: "r1", Branch: "main"}, want: "run-r1"}, + {run: meshwants.StepRun{Branch: "feat/x", Revision: 42}, want: "feat/x.42"}, + {run: meshwants.StepRun{Branch: "main"}, fail: true}, + } { + got, err := candidateKeyOf(c.run) + if (err != nil) != c.fail || got != c.want { + t.Errorf("%+v: %q, %v; want %q", c.run, got, err, c.want) + } + } +} + +// A-252: the machines of a step are the nodes whose placement names its branch; none named is the trunk; an unwatched branch takes +// the trunk's nodes. +func TestTheMachinesOfAStep(t *testing.T) { + placed := map[string]map[string]string{"m": {"ace": "", "g14": "feat", "novox": "main", "shanks": "feat"}, "other": {"ace": ""}} + for _, c := range []struct { + branch string + watched bool + want string + }{ + {"main", true, "ace novox"}, + {"feat", true, "g14 shanks"}, + {"gone", false, "ace novox"}, + {"", true, "ace novox"}, + } { + if got := strings.Join(machinesFor(placed, "m", c.branch, trunkBranch, c.watched), " "); got != c.want { + t.Errorf("branch %q watched %v: %q, want %q", c.branch, c.watched, got, c.want) + } + } + if got := machinesFor(placed, "none", "main", trunkBranch, true); len(got) != 0 { + t.Errorf("a module placed nowhere has machines %v", got) + } +} + +// §354, A-243, A-252, §64: each machine is asked compose-candidate with the step's key and input true, its numbers as written; a +// silent or unready node says why, and a machine away is not asked. +func TestAStepsInputsAreAskedOfEachMachine(t *testing.T) { + // gather asks the machines side by side, so the record of what each was asked is guarded. + var mu sync.Mutex + asked := map[string]string{} + call := func(node, key string, body any) (json.RawMessage, error) { + raw, _ := json.Marshal(body) + mu.Lock() + asked[node] = key + " " + string(raw) + mu.Unlock() + switch node { + case "g14": + return nil, errors.New("no responders") + case "novox": + return json.RawMessage(`{"candidate":"run-r1","node":"novox","verdict":"unready","refused":"starting","modules":[],"own":{},"took":1,"v":1}`), nil + } + return json.RawMessage(`{"candidate":"run-r1","node":"` + node + `","verdict":"composed","input":{"n":12345678901234567890,"node":"` + node + `"},"modules":[],"own":{},"took":1,"v":1}`), nil + } + c := &compositionInputs{machines: func(module, branch string) []string { return []string{"ace", "g14", "novox", "shanks"} }, call: call, bound: time.Second} + got, err := c.gather(context.Background(), meshwants.StepRun{Module: "m", Branch: "main", Run: "r1"}, func(node string) bool { return node == "shanks" }) + if err != nil || len(got) != 4 { + t.Fatalf("%+v %v", got, err) + } + if want := `seat:node-engine.compose-candidate {"candidate":"run-r1","input":true}`; asked["ace"] != want { + t.Errorf("asked %q, want %q", asked["ace"], want) + } + if got[0].Node != "ace" || got[0].Why != "" || !strings.Contains(string(got[0].Body), "12345678901234567890") { + t.Errorf("ace: %+v", got[0]) + } + if !strings.Contains(got[1].Why, "no responders") || !strings.Contains(got[2].Why, "unready") || got[1].Body != nil || got[2].Body != nil { + t.Errorf("g14 and novox: %+v %+v", got[1], got[2]) + } + if _, was := asked["shanks"]; was || !got[3].Away { + t.Errorf("the machine away is asked or not said away: %+v", got[3]) + } + none := &compositionInputs{machines: func(string, string) []string { return nil }, call: call} + if _, err := none.gather(context.Background(), meshwants.StepRun{Module: "m", Run: "r1"}, nil); err == nil { + t.Error("a step with no machine is gathered") + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositionrun.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositionrun.go new file mode 100644 index 00000000..c4b576f4 --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositionrun.go @@ -0,0 +1,506 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "slices" + "strings" + "sync" + "time" + + "github.com/nats-io/nats.go/jetstream" + + ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + mcc "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient" + meshcatalog "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" + meshwants "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/composer" + "git.novox.be/novox/mesh-sdk/go/buscalls" + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// The run of a composition step (design 47 G §354, §63, §64; A-226, A-238, A-240, A-243, A-252, A-254, A-261): one +// composer.Request per machine, depth compose, the one machine and one case `change`, its manifests, verdicts and seats from the +// step's catalog revision read through mesh-catalog's `revision`; one run ask per machine of the seat node-build-go, naming no +// head, its program the node-engine build's catalog entry with the composer's package; the run's end heard on the build seat's +// step-ended of that ask, its answer (a composer.Verdict) read inline from `output`. A machine the watchman said away +// (machine-away) is not composed and passes with a warning (§64). + +const ( + // runAsker is the assigner's name as an asker of a build seat, its seat's own name: the last token of the subject its run + // asks are put on and the first of their step keys (§467, §481, §600, A-163). + runAsker = meshwants.Seat + // changeCase is the one case each request composes. + changeCase = "change" + // inputVerbKey is the verb a run ask's input is served on: the seat mesh-wants' input (§354). + inputVerbKey = "seat:" + meshwants.Seat + "." + meshwants.VerbInput + // entryVerbKey and seatDefinitionsKey are the mesh-catalog seat verbs the step reads. + entryVerbKey = "seat:" + meshcatalog.Seat + "." + meshcatalog.VerbEntry + seatDefinitionsKey = "seat:" + meshcatalog.Seat + "." + meshcatalog.VerbSeatDefinitions +) + +// runEnds hands each build seat's step-ended of an ask to the composition run that waits for it (A-240), by the ask's id. +type runEnds struct { + mu sync.Mutex + waits map[string]chan buildstep.StepEnded +} + +// expect is the end of the ask id, once heard; called before the ask is put, so no end is missed. +func (r *runEnds) expect(id string) <-chan buildstep.StepEnded { + r.mu.Lock() + defer r.mu.Unlock() + if r.waits == nil { + r.waits = map[string]chan buildstep.StepEnded{} + } + ch := make(chan buildstep.StepEnded, 1) + r.waits[id] = ch + return ch +} + +// forget stops waiting for the ask id. +func (r *runEnds) forget(id string) { + r.mu.Lock() + delete(r.waits, id) + r.mu.Unlock() +} + +// heard hands an end to the run that waits for its ask; one nobody waits for is another asker's, left. +func (r *runEnds) heard(e buildstep.StepEnded) { + r.mu.Lock() + ch := r.waits[e.Ask] + delete(r.waits, e.Ask) + r.mu.Unlock() + if ch != nil { + ch <- e + } +} + +// awayNodes are the machines the watchman said away (machine-away of the seat mesh-watchdogs, §389) and not back since. +type awayNodes struct { + mu sync.Mutex + away map[string]bool +} + +// set keeps machine away (true) or back (false). +func (a *awayNodes) set(machine string, away bool) { + a.mu.Lock() + defer a.mu.Unlock() + if a.away == nil { + a.away = map[string]bool{} + } + if away { + a.away[machine] = true + } else { + delete(a.away, machine) + } +} + +// is says machine is away. +func (a *awayNodes) is(machine string) bool { + if a == nil { + return false + } + a.mu.Lock() + defer a.mu.Unlock() + return a.away[machine] +} + +// compositionRun runs a composition step. +type compositionRun struct { + inputs *compositionInputs + keeper *inputKeeper + // call asks a seat verb held once for the mesh (process.Conn.CallKept). + call func(key string, body any) (json.RawMessage, error) + // trunk is the trunk's catalog half as the assigner holds it. + trunk func() (mcc.Half, bool) + // running is the last `running` heard of a module on a node (world.runningOf), never the node-engine's bucket. + running func(node, module string) (ne.Running, bool) + away *awayNodes + // put puts a run ask on the build seat's check queue under the assigner's name. + put func(ctx context.Context, a buildstep.StepAsk) error + ends *runEnds + // ask asks a seat verb on one node, for the run's answer kept as the build seat's object (buildstep.StepEnded.ReadOutput). + ask buildstep.AskVerb + now func() time.Time + logf func(string, ...any) +} + +// machineEnd is what one machine came to. +type machineEnd struct { + failed, couldNot, warning string +} + +// run runs one composition step and answers what it came to: failed when a machine does not compose, could not run when a machine +// could not be composed, else passed with a warning per machine away or not judged. +func (r *compositionRun) run(ctx context.Context, _ string, run meshwants.StepRun) stepOutcome { + half, err := r.revision(run) + if err != nil { + return couldNotRun("the step's catalog revision is not read: " + err.Error()) + } + seats, err := r.seats(half.Revision.Definitions) + if err != nil { + return couldNotRun(err.Error()) + } + manifests, verdicts, err := caseOf(half) + if err != nil { + return couldNotRun(err.Error()) + } + programOf, err := r.programs(half) + if err != nil { + return couldNotRun(err.Error()) + } + inputs, err := r.inputs.gather(ctx, run, r.away.is) + if err != nil { + return couldNotRun(err.Error()) + } + setPhase(ctx, meshwants.StepProgressEventPhaseRun) + ends := make([]machineEnd, len(inputs)) + var wg sync.WaitGroup + for i, in := range inputs { + wg.Add(1) + go func() { + defer wg.Done() + ends[i] = r.machine(ctx, in, buildstep.NewID(), programOf, manifests, verdicts, seats) + }() + } + wg.Wait() + setPhase(ctx, meshwants.StepProgressEventPhaseReport) + var out stepOutcome + var failed, couldNot []string + for _, e := range ends { + failed = appendSome(failed, e.failed) + couldNot = appendSome(couldNot, e.couldNot) + out.Warnings = appendSome(out.Warnings, e.warning) + } + switch { + case len(failed) > 0: + out.Outcome, out.Said = meshwants.StepEndedOutcomeFailed, strings.Join(failed, "; ") + case len(couldNot) > 0: + out.Outcome, out.Said = meshwants.StepEndedOutcomeCouldNotRun, strings.Join(couldNot, "; ") + default: + out.Outcome = meshwants.StepEndedOutcomePassed + } + return out +} + +func appendSome(to []string, s string) []string { + if s == "" { + return to + } + return append(to, s) +} + +func couldNotRun(said string) stepOutcome { + return stepOutcome{Outcome: meshwants.StepEndedOutcomeCouldNotRun, Said: said} +} + +// machine composes one machine: its request kept under its ask, the ask put, its end heard and its verdict read. +func (r *compositionRun) machine(ctx context.Context, in nodeInput, ask string, programOf func(node string) (buildstep.RunProgram, error), + manifests map[string]composer.Manifest, verdicts map[string]json.RawMessage, seats []json.RawMessage) machineEnd { + node := in.Node + switch { + case in.Away: + return machineEnd{warning: node + " is away (the watchman's machine-away), so it is not composed"} + case in.Why != "": + return machineEnd{couldNot: node + " gave no composition input: " + in.Why} + } + program, err := programOf(node) + if err != nil { + return machineEnd{couldNot: node + ": " + err.Error()} + } + req := composer.Request{V: composer.Version, Input: in.Body, Depth: composer.DepthCompose, Machines: []string{node}, + Cases: []composer.Case{{Name: changeCase, Manifests: manifests, Verdicts: verdicts}}, Seats: seats} + raw, err := json.Marshal(req) + if err != nil { + return machineEnd{couldNot: node + ": its request cannot be encoded: " + err.Error()} + } + if _, err := r.keeper.keep(ask, raw); err != nil { + r.logf("the composition request of %s is refused: %v", node, err) + return machineEnd{couldNot: node + ": its request is not kept: " + err.Error()} + } + ref, err := buildstep.RunInputOf(inputVerbKey, raw) + if err != nil { + return machineEnd{couldNot: node + ": " + err.Error()} + } + a := buildstep.StepAsk{ID: ask, Kind: buildstep.AskCheck, Language: buildstep.AskGo, Asker: runAsker, At: r.now().UTC(), + Run: &buildstep.RunJob{Program: program, Heads: []buildstep.RunHead{}, Input: ref, Reader: "the composition of " + node + " by the node-engine at " + program.Commit}} + ended := r.ends.expect(ask) + defer r.ends.forget(ask) + if err := r.put(ctx, a); err != nil { + return machineEnd{couldNot: node + ": its run ask " + ask + " is not put: " + err.Error()} + } + var e buildstep.StepEnded + select { + case <-ctx.Done(): + return machineEnd{couldNot: node + ": the step stopped before its run ask " + ask + " ended"} + case e = <-ended: + } + if e.Outcome != buildstep.OutcomePassed { + return machineEnd{couldNot: fmt.Sprintf("%s: its run ask %s ended %s on %s: %s", node, ask, e.Outcome, e.Node, e.Said)} + } + answer, err := e.ReadOutput(buildstep.AskGo, r.ask) // the program's last line of standard output (A-254) + if err != nil { + return machineEnd{couldNot: node + ": " + err.Error()} + } + return verdictOf(node, answer) +} + +// verdictOf reads the composer's verdict of one machine: not composing is failed, naming why; not judged passes with a warning. +func verdictOf(node string, answer []byte) machineEnd { + var v composer.Verdict + if err := json.Unmarshal(answer, &v); err != nil { + return machineEnd{couldNot: node + ": the composer answered no verdict: " + err.Error()} + } + if v.V != composer.Version { + return machineEnd{couldNot: fmt.Sprintf("%s: the composer answered a verdict of version %d, the assigner reads %d", node, v.V, composer.Version)} + } + for _, c := range v.Cases { + if c.Name != changeCase { + continue + } + m, ok := c.Machines[node] + switch { + case !ok: + return machineEnd{couldNot: node + ": the composer's verdict names no such machine"} + case m.NotJudged != "": + return machineEnd{warning: node + " was not judged: " + m.NotJudged} + case !m.Composes: + return machineEnd{failed: node + " does not compose: " + strings.Join(appendSome(append(slices.Clone(m.Problems), m.Refusals...), m.Reason), "; ")} + } + return machineEnd{} + } + return machineEnd{couldNot: node + ": the composer's verdict has no case " + changeCase} +} + +// revision reads the step's catalog revision whole, page by page, through mesh-catalog's `revision` (A-252). +func (r *compositionRun) revision(run meshwants.StepRun) (mcc.Half, error) { + if run.Revision <= 0 { + return mcc.Half{}, errors.New("the step names no catalog revision") + } + half := mcc.Half{Entries: map[string]meshcatalog.CatalogEntry{}} + in := meshcatalog.RevisionInput{Branch: keys.EscapeName(run.Branch), Revision: run.Revision} + for { + raw, err := r.call(mcc.RevisionVerb, in) + if err != nil { + return mcc.Half{}, err + } + body, said, ok := buscalls.Unwrap(raw) + if !ok { + return mcc.Half{}, fmt.Errorf("%s answered: %s", mcc.RevisionVerb, said) + } + var a meshcatalog.RevisionAnswer + if err := json.Unmarshal(body, &a); err != nil { + return mcc.Half{}, fmt.Errorf("%s answered no revision: %w", mcc.RevisionVerb, err) + } + if a.NoneYet || a.Revision == nil || a.Revision.Revision != run.Revision { + return mcc.Half{}, fmt.Errorf("%s answered no revision %d of %q", mcc.RevisionVerb, run.Revision, run.Branch) + } + if in.After == "" { + half.Revision = *a.Revision // the first page names what the revision was read from, its definitions included + } + for _, e := range a.Entries { + half.Entries[e.Module] = e + } + if a.Next == "" { + return half, nil + } + in.After = a.Next + } +} + +// seats are the definitions of the revision's (seat, digest) pairs, raw (A-261): the revision's definitions carry no definition +// yet (A-234), so each is mesh-catalog's seat-definitions answer for the seat where the trunk's digest is the revision's; a digest +// neither holds is an error naming the seat and the digest. +func (r *compositionRun) seats(defs []meshcatalog.CatalogRevisionDefinitionsItem) ([]json.RawMessage, error) { + out := make([]json.RawMessage, 0, len(defs)) + for _, d := range defs { + raw, err := r.call(seatDefinitionsKey, meshcatalog.SeatDefinitionsInput{Seat: d.Seat}) + if err != nil { + return nil, fmt.Errorf("the definition of the seat %s is not read: %w", d.Seat, err) + } + body, said, ok := buscalls.Unwrap(raw) + if !ok { + return nil, fmt.Errorf("the definition of the seat %s is not read: %s", d.Seat, said) + } + var a meshcatalog.SeatDefinitionsAnswer + if err := json.Unmarshal(body, &a); err != nil { + return nil, fmt.Errorf("the definition of the seat %s is not read: %w", d.Seat, err) + } + if a.Entry == nil || a.Entry.Digest != d.Digest || a.Entry.Definition == nil { + return nil, fmt.Errorf("no definition of the seat %s with the digest %s is held: neither the revision nor the trunk holds it", d.Seat, d.Digest) + } + def, err := json.Marshal(a.Entry.Definition) + if err != nil { + return nil, err + } + out = append(out, def) + } + return out, nil +} + +// caseOf is the case's manifests and verdicts from the revision's entries: each manifest whole, and the verdict of each one not +// accepted (a module without one is accepted). +func caseOf(half mcc.Half) (map[string]composer.Manifest, map[string]json.RawMessage, error) { + manifests := map[string]composer.Manifest{} + verdicts := map[string]json.RawMessage{} + for name, e := range half.Entries { + if len(e.Manifest) == 0 { + continue + } + raw, err := json.Marshal(e.Manifest) + if err != nil { + return nil, nil, fmt.Errorf("the manifest of %s cannot be encoded: %w", name, err) + } + manifests[name] = composer.Manifest{Raw: raw} + if e.Verdict == "" || e.Verdict == mcc.VerdictAccepted { + continue + } + v := meshcatalog.VerdictsByDigestValue{V: 1, Module: name, State: e.Verdict} + if e.Verdict != mcc.VerdictRefused { + v.State = mcc.VerdictManifestWaits + } + if e.Inputs != nil { + v.Problems, v.Unknown, v.Warnings = e.Inputs.Problems, e.Inputs.Unknown, e.Inputs.Warnings + } + raw, err = json.Marshal(v) + if err != nil { + return nil, nil, err + } + verdicts[name] = raw + } + return manifests, verdicts, nil +} + +// nodeEngineEntry is the entry of the module that claims the seat node-engine, or false. +func nodeEngineEntry(entries map[string]meshcatalog.CatalogEntry) (meshcatalog.CatalogEntry, bool) { + for _, name := range slices.Sorted(mapsKeys(entries)) { + for _, s := range entries[name].Serves { + if strings.HasPrefix(s, ne.Seat+".") { + return entries[name], true + } + } + } + return meshcatalog.CatalogEntry{}, false +} + +func mapsKeys(m map[string]meshcatalog.CatalogEntry) func(func(string) bool) { + return func(yield func(string) bool) { + for k := range m { + if !yield(k) { + return + } + } + } +} + +// programOfEntry is the composer built from a node-engine entry: its repository, commit and directory, the composer's package. +func programOfEntry(e meshcatalog.CatalogEntry) (buildstep.RunProgram, error) { + dir := e.Path + if dir == "" { + dir = "." + } + if e.Repository == "" || e.Commit == "" { + return buildstep.RunProgram{}, fmt.Errorf("the node-engine's entry %s names no repository or commit", e.Fp) + } + return buildstep.RunProgram{Repository: e.Repository, Commit: e.Commit, Dir: dir, Package: composer.Package}, nil +} + +// programs answers, per machine, the composer its run builds (A-226, A-243, A-252): where the revision's node-engine entry differs +// from the trunk's, that build for every machine; else the build the machine's node-engine runs, as its `running` says, its entry +// read through mesh-catalog's `entry`. +func (r *compositionRun) programs(half mcc.Half) (func(node string) (buildstep.RunProgram, error), error) { + engine, ok := nodeEngineEntry(half.Entries) + if !ok { + return nil, fmt.Errorf("the catalog revision %d holds no module claiming the seat %s", half.Revision.Revision, ne.Seat) + } + if trunk, ok := r.trunk(); !ok || func() bool { t, ok := nodeEngineEntry(trunk.Entries); return !ok || t.Fp != engine.Fp }() { + p, err := programOfEntry(engine) + return func(string) (buildstep.RunProgram, error) { return p, err }, nil + } + var mu sync.Mutex + byFp := map[string]buildstep.RunProgram{} + return func(node string) (buildstep.RunProgram, error) { + running, ok := r.running(node, engine.Module) + if !ok || running.Fingerprint == "" { + return buildstep.RunProgram{}, fmt.Errorf("no build of %s is heard running there", engine.Module) + } + mu.Lock() + p, ok := byFp[running.Fingerprint] + mu.Unlock() + if ok { + return p, nil + } + raw, err := r.call(entryVerbKey, meshcatalog.EntryInput{Fingerprint: running.Fingerprint}) + if err != nil { + return buildstep.RunProgram{}, fmt.Errorf("the entry of the node-engine's build %s is not read: %w", running.Fingerprint, err) + } + body, said, ok := buscalls.Unwrap(raw) + if !ok { + return buildstep.RunProgram{}, fmt.Errorf("the entry of the node-engine's build %s is not read: %s", running.Fingerprint, said) + } + var a meshcatalog.EntryAnswer + if err := json.Unmarshal(body, &a); err != nil { + return buildstep.RunProgram{}, fmt.Errorf("the entry of the node-engine's build %s is not read: %w", running.Fingerprint, err) + } + p, err = programOfEntry(a.Entry) + if err != nil { + return p, err + } + mu.Lock() + byFp[running.Fingerprint] = p + mu.Unlock() + return p, nil + }, nil +} + +// heardPresence keeps a machine-away (away true) or machine-back of the seat mesh-watchdogs (§389, §436): the machine it names. +func heardPresence(a *awayNodes, away bool, m jetstream.Msg, logf func(string, ...any)) { + var e struct { + Machine string `json:"machine"` + } + if err := json.Unmarshal(m.Data(), &e); err != nil || e.Machine == "" { + logf("%s holds no machine, left: %v", m.Subject(), err) + } else { + a.set(e.Machine, away) + if away { + logf("%s is away (machine-away): a composition step does not compose it until its machine-back", e.Machine) + } else { + logf("%s is back (machine-back): a composition step composes it again", e.Machine) + } + } + if err := m.Ack(); err != nil { + logf("acknowledging %s: %v", m.Subject(), err) + } +} + +// trunkOf is the trunk's catalog half the assigner's views hold, trunk naming the first watched branch, or false while none is +// held. +func trunkOf(iv *catalogViews, trunk func() string) func() (mcc.Half, bool) { + return func() (mcc.Half, bool) { + if iv == nil || iv.h == nil { + return mcc.Half{}, false + } + h, ok := iv.h.views()[trunk()] + return h, ok + } +} + +// runAskPublisher puts a run ask on the Go build seat's check queue under the assigner's name (`accept.check.assigner`, §600), its +// id the message id, so the stream takes it once. +func runAskPublisher(js jetstream.JetStream) func(ctx context.Context, a buildstep.StepAsk) error { + return func(ctx context.Context, a buildstep.StepAsk) error { + raw, err := buildstep.EncodeStepAsk(a) + if err != nil { + return err + } + subject, err := buildstep.AskSubjectBy(a.Language, a.Kind, a.Asker) + if err != nil { + return err + } + _, err = js.Publish(ctx, subject, raw, jetstream.WithMsgID(a.ID)) + return err + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositionrun_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositionrun_test.go new file mode 100644 index 00000000..fa0ca057 --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositionrun_test.go @@ -0,0 +1,176 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "strings" + "sync" + "testing" + "time" + + ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + mcc "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient" + meshcatalog "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" + meshwants "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/composer" +) + +const ( + commitE1 = "1111111111111111111111111111111111111111" + commitE2 = "2222222222222222222222222222222222222222" + commitE3 = "3333333333333333333333333333333333333333" +) + +// runStand is a composition run over stand-ins of the mesh-catalog seat, the machines' engines, the build seat and its step-ended. +type runStand struct { + mu sync.Mutex + asks map[string]buildstep.StepAsk // by node + requests map[string]composer.Request // by node + entries []string // the fingerprints `entry` was asked + r *compositionRun +} + +func newRunStand(t *testing.T, revisionEngine, trunkEngine, seatDigest string) *runStand { + s := &runStand{asks: map[string]buildstep.StepAsk{}, requests: map[string]composer.Request{}} + engine := func(fp, commit string) meshcatalog.CatalogEntry { + return meshcatalog.CatalogEntry{Module: "engine", Fp: fp, Repository: "novox/mesh-host", Commit: commit, Path: ".", Serves: []string{ne.Seat + ".composition"}, + Manifest: map[string]any{"module": "engine"}} + } + revisionCommit := commitE1 + if revisionEngine != "src2:e1" { + revisionCommit = commitE3 + } + pages := map[string]meshcatalog.RevisionAnswer{ + "": {Revision: &meshcatalog.CatalogRevision{Revision: 7, Branch: "main", Definitions: []meshcatalog.CatalogRevisionDefinitionsItem{{Seat: "s1", Digest: "d1"}}}, + Entries: []meshcatalog.CatalogEntry{engine(revisionEngine, revisionCommit), {Module: "bad", Verdict: mcc.VerdictRefused, Inputs: &meshcatalog.EntryInputs{Problems: []string{"no seat x"}}, + Manifest: map[string]any{"module": "bad"}}}, Next: "bad"}, + "bad": {Revision: &meshcatalog.CatalogRevision{Revision: 7}, Entries: []meshcatalog.CatalogEntry{{Module: "m", Verdict: mcc.VerdictAccepted, Manifest: map[string]any{"module": "m"}}}}, + } + call := func(key string, body any) (json.RawMessage, error) { + raw, _ := json.Marshal(body) + switch key { + case mcc.RevisionVerb: + var in meshcatalog.RevisionInput + _ = json.Unmarshal(raw, &in) + if in.Revision != 7 || in.Branch != "main" { + return nil, errors.New("asked another revision") + } + return json.Marshal(pages[in.After]) + case seatDefinitionsKey: + return json.Marshal(meshcatalog.SeatDefinitionsAnswer{Found: true, Entry: &meshcatalog.SeatDefinitionsAnswerEntry{Seat: "s1", Digest: seatDigest, Definition: map[string]any{"seat": "s1"}}}) + case entryVerbKey: + var in meshcatalog.EntryInput + _ = json.Unmarshal(raw, &in) + s.mu.Lock() + s.entries = append(s.entries, in.Fingerprint) + s.mu.Unlock() + commit := map[string]string{"src2:e1": commitE1, "src2:e2": commitE2}[in.Fingerprint] + return json.Marshal(meshcatalog.EntryAnswer{Entry: engine(in.Fingerprint, commit)}) + } + return nil, errors.New("no such verb " + key) + } + engines := func(node, _ string, _ any) (json.RawMessage, error) { + return json.RawMessage(`{"candidate":"main.7","node":"` + node + `","verdict":"composed","input":{"node":"` + node + `"},"modules":[],"own":{},"took":1,"v":1}`), nil + } + running := map[string]string{"ace": "src2:e1", "g14": "src2:e2"} + away := &awayNodes{} + away.set("shanks", true) + keeper := testKeeper(&objectsMem{objs: map[string][]byte{}}, time.Now) + ends := &runEnds{} + s.r = &compositionRun{ + inputs: &compositionInputs{machines: func(string, string) []string { return []string{"ace", "g14", "shanks"} }, call: engines, bound: time.Second}, + keeper: keeper, call: call, ends: ends, away: away, now: time.Now, logf: t.Logf, + trunk: func() (mcc.Half, bool) { + return mcc.Half{Entries: map[string]meshcatalog.CatalogEntry{"engine": engine(trunkEngine, commitE1)}}, true + }, + running: func(node, module string) (ne.Running, bool) { + fp, ok := running[node] + return ne.Running{Node: node, Module: module, Fingerprint: fp}, ok && module == "engine" + }, + put: func(_ context.Context, a buildstep.StepAsk) error { + body, err := keeper.body(a.Run.Input.Digest) + if err != nil { + return err + } + req, err := composer.Decode(body) + if err != nil { + return err + } + node := req.Machines[0] + s.mu.Lock() + s.asks[node], s.requests[node] = a, req + s.mu.Unlock() + m := composer.Machine{Composes: true} + if node == "g14" { + m = composer.Machine{Reason: "two modules own one path", Problems: []string{"/etc/x is m's and bad's"}} + } + v, _ := json.Marshal(composer.Verdict{V: composer.Version, Cases: []composer.CaseVerdict{{Name: changeCase, Machines: map[string]composer.Machine{node: m}}}}) + go ends.heard(buildstep.StepEnded{Ask: a.ID, Asker: runAsker, Node: "builder1", Outcome: buildstep.OutcomePassed, Output: string(v)}) + return nil + }, + } + return s +} + +// Design 47 G §354, §63, §64, A-226, A-238, A-240, A-243, A-252, A-254, A-261: one request and one run ask per machine, the request +// depth compose with the one machine and one case `change`, its manifests, verdicts and seats from the step's revision; the +// composer is the node-engine build each machine runs, its entry read through `entry`, or the revision's where it differs from the +// trunk's; the run's verdict is read from its step-ended; a machine away passes with a warning, one not composing fails the step. +func TestTheCompositionStepRunsOneAskPerMachine(t *testing.T) { + step := meshwants.StepRun{Module: "m", Branch: "main", Revision: 7, Step: meshwants.StepRunStepComposition} + s := newRunStand(t, "src2:e1", "src2:e1", "d1") + out := s.r.run(context.Background(), "ask-step", step) + if out.Outcome != meshwants.StepEndedOutcomeFailed || !strings.Contains(out.Said, "g14 does not compose") || !strings.Contains(out.Said, "/etc/x") || + strings.Contains(out.Said, "ace") || len(out.Warnings) != 1 || !strings.Contains(out.Warnings[0], "shanks is away") { + t.Fatalf("the step came to %+v", out) + } + if len(s.asks) != 2 { + t.Fatalf("asked %d runs, want one per machine not away", len(s.asks)) + } + a, req := s.asks["ace"], s.requests["ace"] + if a.Kind != buildstep.AskCheck || a.Language != buildstep.AskGo || a.Asker != runAsker || a.Run == nil || len(a.Run.Heads) != 0 || + a.Run.Input.Verb != "seat:mesh-wants.input" || a.Run.Program != (buildstep.RunProgram{Repository: "novox/mesh-host", Commit: commitE1, Dir: ".", Package: composer.Package}) { + t.Errorf("the run ask of ace %+v %+v", a, a.Run) + } + if s.asks["g14"].Run.Program.Commit != commitE2 { + t.Errorf("g14's composer is built at %s, want the build its node-engine runs", s.asks["g14"].Run.Program.Commit) + } + if req.Depth != composer.DepthCompose || len(req.Machines) != 1 || req.Machines[0] != "ace" || len(req.Cases) != 1 || req.Cases[0].Name != changeCase || + string(req.Input) != `{"node":"ace"}` || len(req.Seats) != 1 || string(req.Seats[0]) != `{"seat":"s1"}` { + t.Errorf("the request of ace %+v", req) + } + c := req.Cases[0] + if string(c.Manifests["m"].Raw) != `{"module":"m"}` || len(c.Manifests) != 3 || len(c.Verdicts) != 1 || !strings.Contains(string(c.Verdicts["bad"]), `"refused"`) || + !strings.Contains(string(c.Verdicts["bad"]), "no seat x") { + t.Errorf("the case %+v %s", c.Manifests, c.Verdicts) + } + if strings.Join(s.entries, " ") == "" { + t.Error("no node-engine entry is read through entry") + } + + // The revision's node-engine differs from the trunk's: that build composes every machine, and no entry is asked. + changed := newRunStand(t, "src2:e3", "src2:e1", "d1") + if out := changed.r.run(context.Background(), "ask-step", step); out.Outcome != meshwants.StepEndedOutcomeFailed { + t.Fatalf("the step came to %+v", out) + } + if changed.asks["ace"].Run.Program.Commit != commitE3 || changed.asks["g14"].Run.Program.Commit != commitE3 || len(changed.entries) != 0 { + t.Errorf("the change's node-engine does not compose every machine: %+v %+v %v", changed.asks["ace"].Run.Program, changed.asks["g14"].Run.Program, changed.entries) + } + + // A seat digest neither the revision nor the trunk holds: could not run, naming the seat and the digest (A-261). + other := newRunStand(t, "src2:e1", "src2:e1", "d2") + if out := other.r.run(context.Background(), "ask-step", step); out.Outcome != meshwants.StepEndedOutcomeCouldNotRun || + !strings.Contains(out.Said, "s1") || !strings.Contains(out.Said, "d1") || len(other.asks) != 0 { + t.Errorf("a seat digest not held came to %+v, %d asks", out, len(other.asks)) + } + + // At the inputs state's cap the ask is refused, naming the store (§354). + full := newRunStand(t, "src2:e1", "src2:e1", "d1") + full.r.keeper.used = func() (int64, error) { return inputsCap, nil } + if out := full.r.run(context.Background(), "ask-step", step); out.Outcome != meshwants.StepEndedOutcomeCouldNotRun || + !strings.Contains(out.Said, "the state inputs is at its cap") || len(full.asks) != 0 { + t.Errorf("an ask at the cap came to %+v, %d asks", out, len(full.asks)) + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositionstep.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositionstep.go new file mode 100644 index 00000000..48cedf7e --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositionstep.go @@ -0,0 +1,168 @@ +package main + +import ( + "context" + "encoding/json" + "strings" + "sync" + "time" + + "github.com/nats-io/nats.go" + + meshwants "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" + "git.novox.be/novox/mesh-sdk/go/process" + "git.novox.be/novox/mesh-sdk/go/seatdef" +) + +// The composition step (design 47 G §22, §354, §63): the assigner takes the seat mesh-wants' accepted kind `step`, the +// composition step of one module's pipeline that the runner asks, runs it, and says its end as the seat's event +// step-ended., the ask's id as the event's id and the event's `id`, which the runner holds the end to. + +// stepOutcome is what one composition step came to. +type stepOutcome struct { + Outcome string + Said string + Warnings []string +} + +// compositionSteps takes the composition steps and says their ends. +type compositionSteps struct { + node string + // say says one event of the seat mesh-wants (wantsPublisher). + say publisher + // run runs one step and answers what it came to. + run func(ctx context.Context, id string, run meshwants.StepRun) stepOutcome + now func() time.Time + logf func(string, ...any) + // asker is the name the step's progress names as its asker, read when it is said (stepAskerOf). + asker func() string + // ticks is a ticker of every d, its channel and its stop (time.NewTicker); nil is the clock's. A test hands its own. + ticks func(d time.Duration) (<-chan time.Time, func()) +} + +// stepAskerOf is the asker a composition step's progress names: the one caller the seat mesh-wants' definition lists for its +// accepted kind step, read from the seat list when it is said, since that subject names no asker (design 47 G A-294, which +// corrects A-267). Empty while the seat list holds no definition naming exactly one. +func stepAskerOf(definition func() (seatdef.Definition, bool)) func() string { + return func() string { + d, ok := definition() + if !ok { + return "" + } + var one string + for _, m := range d.Accepts { + if m.Name != meshwants.AcceptStep || m.Callers == nil { + continue + } + for _, e := range m.Callers.List { + name, isSeat := strings.CutPrefix(e, seatdef.CallerSeatPrefix) + if !isSeat || (one != "" && one != name) { + return "" + } + one = name + } + } + return one + } +} + +// progressEvery is how often a running composition step says its progress (design 47 G §496, A-262). +const progressEvery = time.Minute + +// stepPhase is the phase a running step is in: fetch, build, run or report (§496), set by the run through its context. +type stepPhase struct { + mu sync.Mutex + phase string +} + +type phaseKey struct{} + +// setPhase sets the phase of the step ctx runs, when it runs one. +func setPhase(ctx context.Context, phase string) { + if p, ok := ctx.Value(phaseKey{}).(*stepPhase); ok { + p.mu.Lock() + p.phase = phase + p.mu.Unlock() + } +} + +func (p *stepPhase) get() string { + p.mu.Lock() + defer p.mu.Unlock() + return p.phase +} + +// take takes one ask of the accepted kind `step` by its id and body: a step that is no composition step, or an ask with no +// id, is left with a log line; one taken is run in its own goroutine and its end said. +func (c *compositionSteps) take(ctx context.Context, id string, data []byte) bool { + var run meshwants.StepRun + if err := json.Unmarshal(data, &run); err != nil || id == "" || run.Step != meshwants.StepRunStepComposition { + c.logf("a step ask that is no composition step with an id is left: %v", err) + return false + } + go c.runOne(ctx, id, run) + return true +} + +// runOne runs one step and says its end. +func (c *compositionSteps) runOne(ctx context.Context, id string, run meshwants.StepRun) { + started := c.now().UTC() + phase := &stepPhase{phase: meshwants.StepProgressEventPhaseFetch} + stop := c.sayProgress(id, phase) + o := c.run(context.WithValue(ctx, phaseKey{}, phase), id, run) + stop() + ended := c.now().UTC() + end := meshwants.StepEnded{V: 1, ID: id, Pipeline: run.Pipeline, Step: meshwants.StepEndedStepComposition, Module: run.Module, + Commit: run.Commit, Fingerprint: run.Fingerprint, Node: c.node, Outcome: o.Outcome, Said: o.Said, Warnings: o.Warnings, + Rerun: run.Rerun, By: run.By, Started: &started, Ended: &ended} + if err := c.say(meshwants.EventStepEnded+"."+c.node, end, id); err != nil { + c.logf("saying the end of the composition step %s: %v", id, err) + } +} + +// sayProgress says the step's progress on the seat's step-progress. once a minute until the returned stop is called (design +// 47 G §496, A-262): a ticker inside the running step, as the build seats' step runner says its own beat. +func (c *compositionSteps) sayProgress(id string, phase *stepPhase) (stop func()) { + ticks := c.ticks + if ticks == nil { + ticks = func(d time.Duration) (<-chan time.Time, func()) { t := time.NewTicker(d); return t.C, t.Stop } + } + tick, stopTicks := ticks(progressEvery) + done := make(chan struct{}) + ended := make(chan struct{}) + go func() { + defer close(ended) + defer stopTicks() + for { + select { + case <-done: + return + case <-tick: + at := c.now().UTC() + p := meshwants.StepProgressEvent{Ask: id, Asker: c.asker(), Phase: phase.get(), At: at} + if err := c.say(meshwants.EventStepProgress+"."+c.node, p, id+".progress."+at.Format(time.RFC3339Nano)); err != nil { + c.logf("saying the progress of the composition step %s: %v", id, err) + } + } + } + }() + return func() { close(done); <-ended } +} + +// stepAskID is an ask's id as the asker names it: its event id, else its message id. +func stepAskID(m *nats.Msg) string { + if id := m.Header.Get("x-event-id"); id != "" { + return id + } + return m.Header.Get(nats.MsgIdHdr) +} + +// serveCompositionSteps hears the composition step asks on the seat mesh-wants' accept subject. +func serveCompositionSteps(ctx context.Context, conn process.Subscriber, c *compositionSteps) error { + sub, err := conn.Subscribe(meshwants.AcceptSubjectStep, func(m *nats.Msg) { c.take(ctx, stepAskID(m), m.Data) }) + if err != nil { + return err + } + go func() { <-ctx.Done(); _ = sub.Drain() }() + return nil +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositionstep_integration_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositionstep_integration_test.go new file mode 100644 index 00000000..daef24b8 --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositionstep_integration_test.go @@ -0,0 +1,224 @@ +//go:build integration + +package main + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + "time" + + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient/enginereads" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" + meshwants "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/nodebuildgo" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/composer" + "git.novox.be/novox/mesh-sdk/go/buscalls" + + "mesh-delivery-assigner/internal/buckets" +) + +// Design 47 G §354, §22, §63, §64, §389, A-240, A-252, A-254: the assigner started whole takes the runner's composition step of a +// module placed on three machines, one of them away: it asks the two others' node-engines for their inputs, puts one run ask per +// machine on the Go build seat's check queue under its name, its program the node-engine build each machine runs; the stand-in +// build seat reads each request by the seat mesh-wants' verb input in chunks and says its step-ended with the composer's verdict; +// the step's step-ended fails naming the machine that does not compose, warns of the machine away, and no input stays kept. +func TestTheCompositionStepComposesEachMachineThroughTheBuildSeat(t *testing.T) { + b := startBed(t, "") + nc, err := nats.Connect(b.srv.ClientURL()) + if err != nil { + t.Fatal(err) + } + t.Cleanup(nc.Close) + const engineCommit = "1111111111111111111111111111111111111111" + // The assigner's durable on EVENTS hears, beside the bed's, the build seat's step-ended and the watchman's machine-away and + // machine-back, as the grants part derives them from its consumes; what each node runs it hears on its kept stream (the bed's). + if _, err := b.js.CreateOrUpdateConsumer(b.ctx, "EVENTS", jetstream.ConsumerConfig{Durable: "n1_mesh-delivery-assigner", + FilterSubjects: []string{enginereads.SubjectPrefix + "*", "mesh.mod." + buckets.IndexerModule + ".event.views.>", meshcatalog.SubjectCatalogRevisionAll, + nodebuildgo.SubjectStepEndedAll, meshwatchdogs.SubjectMachineAwayAll, meshwatchdogs.SubjectMachineBackAll}, AckPolicy: jetstream.AckExplicitPolicy}); err != nil { + t.Fatal(err) + } + for _, node := range []string{"n1", "n2"} { + raw, _ := json.Marshal(nodeengine.Running{V: 1, Node: node, Module: "engine", Fingerprint: "src2:e1", At: time.Now().UTC()}) + if _, err := b.js.Publish(b.ctx, nodeengine.SubjectRunning(node, "engine"), raw); err != nil { + t.Fatal(err) + } + } + awaitKept(t, b, nodeengine.StreamRunning, 2) + away, _ := json.Marshal(meshwatchdogs.MachineAwayEvent{V: 1, Machine: "n3", At: time.Now().UTC(), LastHeard: time.Now().UTC()}) + if _, err := b.js.Publish(b.ctx, meshwatchdogs.SubjectMachineAway("n3"), away); err != nil { + t.Fatal(err) + } + // The trunk's catalog half holds the module and the node-engine, the module claiming the seat node-engine. + engine := meshcatalog.CatalogEntry{Module: "engine", Fp: "src2:e1", Repository: "novox/mesh-host", Commit: engineCommit, Path: ".", + Serves: []string{nodeengine.Seat + ".composition"}, Manifest: map[string]any{"module": "engine", "version": "0.1"}} + b.cat.mu.Lock() + b.cat.entries[trunkBranch] = map[string]meshcatalog.CatalogEntry{"engine": engine, + "m": {Module: "m", Fp: "src2:m1", Repository: "novox/m", Commit: engineCommit, Path: ".", Manifest: map[string]any{"module": "m", "version": "0.1"}}} + b.cat.rev[trunkBranch] = 3 + b.cat.mu.Unlock() + for _, node := range []string{"n1", "n2", "n3"} { + b.place(t, node, "m") + } + // The node-engines of n1 and n2 answer compose-candidate with their inputs; n3 is never asked. + asked := make(chan string, 8) + engines, err := nc.Subscribe("mesh.seat.node-engine.call.compose-candidate.*.*", func(m *nats.Msg) { + node := strings.Split(m.Subject, ".")[5] + asked <- node + raw, _ := json.Marshal(map[string]any{"result": map[string]any{"candidate": "main.3", "node": node, "verdict": "composed", "input": map[string]any{"node": node}, + "modules": []any{}, "own": map[string]any{}, "took": 1, "v": 1}, "node": node}) + _ = m.Respond(raw) + }) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = engines.Unsubscribe() }) + // mesh-catalog's entry answers the node-engine build's entry by its fingerprint. + entries, err := nc.Subscribe("mesh.seat.mesh-catalog.call.entry.*", func(m *nats.Msg) { + raw, _ := json.Marshal(map[string]any{"result": meshcatalog.EntryAnswer{Entry: engine}, "node": "n0"}) + _ = m.Respond(raw) + }) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = entries.Unsubscribe() }) + subscribed(t, nc) + // The runner hears the step's end. + ended := make(chan meshwants.StepEnded, 1) + ends, err := nc.Subscribe("mesh.seat.mesh-wants.event.step-ended.n1", func(m *nats.Msg) { + var e meshwants.StepEnded + _ = json.Unmarshal(m.Data, &e) + ended <- e + }) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = ends.Unsubscribe() }) + subscribed(t, nc) + + from := b.journal.mark() + b.start(t) + b.journal.await(t, from, "the composition step is heard on", "the composition step served", 30*time.Second) + b.journal.await(t, from, "n3 is away (machine-away)", "the machine-away of n3 heard", 30*time.Second) + + select { + case <-b.seatListRead: // the seat server admits a caller of input once the seat list is read (A-278) + case <-time.After(30 * time.Second): + t.Fatal("the assigner did not read the seat list within 30 s") + } + step, _ := json.Marshal(meshwants.StepRun{Pipeline: "main.3.m", Step: meshwants.StepRunStepComposition, Module: "m", Branch: trunkBranch, + Revision: 3, Commit: engineCommit, Fingerprint: "src2:m1"}) + msg := nats.NewMsg(meshwants.AcceptSubjectStep) + msg.Header.Set("x-event-id", "step-ask-1") + msg.Data = step + if err := nc.PublishMsg(msg); err != nil { + t.Fatal(err) + } + + // The stand-in Go build seat takes the run asks put under the assigner's name, reads each request by the verb input and says + // its end with the composer's verdict: n1 composes, n2 does not. + queue, _ := buildstep.AskStream(buildstep.AskGo) + // A-163: the assigner asks under its seat's own name. + subject := "mesh.seat.node-build-go.accept.check.mesh-wants" + worker, err := b.js.CreateOrUpdateConsumer(b.ctx, queue, jetstream.ConsumerConfig{Durable: "stand-in", FilterSubject: subject, AckPolicy: jetstream.AckExplicitPolicy}) + if err != nil { + t.Fatal(err) + } + programs := map[string]string{} + for range 2 { + got, err := worker.Next(jetstream.FetchMaxWait(30 * time.Second)) + if err != nil { + t.Fatalf("no run ask is put: %v", err) + } + _ = got.Ack() + a, err := buildstep.DecodeStepAsk(got.Data()) + if err != nil || a.Run == nil { + t.Fatalf("the run ask %s does not read: %v", got.Data(), err) + } + req, err := composer.Decode(readInput(t, nc, a.Run.Input)) + if err != nil || len(req.Machines) != 1 { + t.Fatalf("the request of %s: %+v %v", a.ID, req, err) + } + node := req.Machines[0] + programs[node] = a.Run.Program.Commit + m := composer.Machine{Composes: true} + if node == "n2" { + m = composer.Machine{Reason: "two modules own one path", Problems: []string{"/etc/x"}} + } + v, _ := json.Marshal(composer.Verdict{V: composer.Version, Cases: []composer.CaseVerdict{{Name: changeCase, Machines: map[string]composer.Machine{node: m}}}}) + end, _ := json.Marshal(buildstep.StepEnded{V: 1, ID: a.ID, Ask: a.ID, Asker: runAsker, Node: "b1", Outcome: buildstep.OutcomePassed, Output: string(v), At: time.Now().UTC()}) + if _, err := b.js.Publish(b.ctx, nodebuildgo.SubjectStepEnded("b1"), end); err != nil { + t.Fatal(err) + } + } + var e meshwants.StepEnded + select { + case e = <-ended: + case <-b.ctx.Done(): + t.Fatal("the composition step said no end") + } + if e.ID != "step-ask-1" || e.Outcome != meshwants.StepEndedOutcomeFailed || !strings.Contains(e.Said, "n2 does not compose") || + strings.Contains(e.Said, "n1") || len(e.Warnings) != 1 || !strings.Contains(e.Warnings[0], "n3 is away") { + t.Fatalf("the step ended %+v", e) + } + if programs["n1"] != engineCommit || programs["n2"] != engineCommit { + t.Errorf("the composers were built at %v, want the node-engine build each machine runs", programs) + } + close(asked) + for node := range asked { + if node == "n3" { + t.Error("the machine away was asked for its input") + } + } + keys, _ := b.buckets[buckets.Inputs()].Keys(b.ctx) + if len(keys) != 0 { + t.Errorf("the inputs stay kept past their step-ended: %v", keys) + } +} + +// readInput reads a run's input as a build seat does: the seat mesh-wants' verb input answers its digest and chunk count, then its +// chunks on the call's subjects, each acknowledged. +func readInput(t *testing.T, nc *nats.Conn, in buildstep.RunInput) []byte { + t.Helper() + call := "standin" + strings.Repeat("0", 16) + chunks := buscalls.SeatChunks{Seat: meshwants.Seat, Verb: meshwants.VerbInput, Call: call} + sub, err := nc.SubscribeSync(chunks.Chunks()) + if err != nil { + t.Fatal(err) + } + defer sub.Unsubscribe() + body, _ := json.Marshal(meshwants.InputInput{Digest: in.Digest, Call: call}) + // On the call's subject naming its caller, the stand-in build seat's account: the seat server admits no unproven caller (A-179). + reply, err := nc.Request("mesh.seat.mesh-wants.call.input.n1~mesh-build-go", body, 10*time.Second) + if err != nil { + t.Fatal(err) + } + raw, said, ok := buscalls.Unwrap(reply.Data) + var a meshwants.InputAnswer + if !ok || json.Unmarshal(raw, &a) != nil || a.Chunks < 1 { + t.Fatalf("the verb input answered %s: %s", reply.Data, said) + } + var out bytes.Buffer + next := func() (string, []byte, error) { + m, err := sub.NextMsg(buscalls.SeatAckWithin) + if err != nil { + return "", nil, err + } + return m.Subject, m.Data, nil + } + ack := func(subject string) error { _, err := nc.Request(subject, nil, 10*time.Second); return err } + if _, err := chunks.Receive(a.Chunks, next, ack, &out); err != nil { + t.Fatal(err) + } + if err := in.Verify(out.Bytes()); err != nil { + t.Fatal(err) + } + return out.Bytes() +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositionstep_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositionstep_test.go new file mode 100644 index 00000000..7467700d --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compositionstep_test.go @@ -0,0 +1,137 @@ +package main + +import ( + "context" + "encoding/json" + "git.novox.be/novox/mesh-sdk/go/seatdef" + "sync" + "testing" + "time" + + meshwants "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" +) + +// saidEvents keeps what a publisher says. +type saidEvents struct { + mu sync.Mutex + said map[string]any + ids map[string]string + heard chan string +} + +func newSaidEvents() *saidEvents { + return &saidEvents{said: map[string]any{}, ids: map[string]string{}, heard: make(chan string, 8)} +} + +func (s *saidEvents) say(event string, body any, id string) error { + s.mu.Lock() + s.said[event], s.ids[event] = body, id + s.mu.Unlock() + s.heard <- event + return nil +} + +// AS5 (§22, §354): the assigner takes mesh-wants' accepted kind `step` for the composition step and says the seat's +// step-ended. with the ask's id, what the step was asked at and what it came to; an ask of another step is left. +func TestTheCompositionStepIsTakenAndItsEndSaid(t *testing.T) { + said := newSaidEvents() + at := time.Date(2026, 11, 12, 9, 0, 0, 0, time.UTC) + var asked meshwants.StepRun + c := &compositionSteps{node: "n1", say: said.say, now: func() time.Time { return at }, logf: t.Logf, + run: func(_ context.Context, id string, run meshwants.StepRun) stepOutcome { + asked = run + return stepOutcome{Outcome: meshwants.StepEndedOutcomeFailed, Said: "a and b own /etc/x", Warnings: []string{"n3 asleep"}} + }} + other, _ := json.Marshal(map[string]any{"pipeline": "main.4.m", "step": "build", "module": "m"}) + if c.take(context.Background(), "ask-0", other) { + t.Fatal("a step ask of another step is taken") + } + body, _ := json.Marshal(meshwants.StepRun{Pipeline: "main.4.m", Step: meshwants.StepRunStepComposition, Module: "m", Branch: "main", + Commit: "c0ffee", Revision: 4, Fingerprint: "fp1", Rerun: true, By: "lead@n1"}) + if c.take(context.Background(), "", body) { + t.Fatal("a step ask with no id is taken") + } + if !c.take(context.Background(), "ask-1", body) { + t.Fatal("the composition step is not taken") + } + select { + case <-said.heard: + case <-time.After(5 * time.Second): + t.Fatal("no step-ended is said") + } + event := meshwants.EventStepEnded + ".n1" + end, ok := said.said[event].(meshwants.StepEnded) + if !ok || said.ids[event] != "ask-1" { + t.Fatalf("said %v under %v, want step-ended.n1 under the ask's id", said.said, said.ids) + } + if asked.Module != "m" || end.ID != "ask-1" || end.Pipeline != "main.4.m" || end.Step != meshwants.StepEndedStepComposition || + end.Module != "m" || end.Commit != "c0ffee" || end.Fingerprint != "fp1" || end.Node != "n1" || + end.Outcome != meshwants.StepEndedOutcomeFailed || end.Said != "a and b own /etc/x" || len(end.Warnings) != 1 || + !end.Rerun || end.By != "lead@n1" || end.Started == nil || end.Ended == nil { + t.Fatalf("step-ended %+v", end) + } +} + +// Design 47 G §496, A-262: a running composition step says step-progress. on every tick of its ticker, with the ask, its +// asker, the phase the run is in and the time, and says none once it has ended. +func TestARunningCompositionStepSaysItsProgress(t *testing.T) { + said := newSaidEvents() + tick := make(chan time.Time) + stopped := make(chan struct{}) + at := time.Date(2026, 11, 12, 9, 0, 0, 0, time.UTC) + release := make(chan struct{}) + // A-294: the asker is the one caller mesh-wants' definition lists for step, read from the seat list; none while unread + // or while it lists more than one. + runner := seatdef.Definition{Seat: meshwants.Seat, Accepts: []seatdef.Message{{Name: meshwants.AcceptStep, + Callers: &seatdef.Callers{List: []string{"seat:mesh-pipeline-runner"}}}}} + two := seatdef.Definition{Seat: meshwants.Seat, Accepts: []seatdef.Message{{Name: meshwants.AcceptStep, + Callers: &seatdef.Callers{List: []string{"seat:mesh-pipeline-runner", "seat:mesh-catalog"}}}}} + for _, d := range []struct { + def seatdef.Definition + read bool + want string + }{{runner, true, "mesh-pipeline-runner"}, {runner, false, ""}, {two, true, ""}} { + if got := stepAskerOf(func() (seatdef.Definition, bool) { return d.def, d.read })(); got != d.want { + t.Errorf("the asker of %+v (read %v) = %q, want %q", d.def.Accepts, d.read, got, d.want) + } + } + c := &compositionSteps{node: "n1", say: said.say, now: func() time.Time { return at }, logf: t.Logf, + asker: stepAskerOf(func() (seatdef.Definition, bool) { return runner, true }), + ticks: func(d time.Duration) (<-chan time.Time, func()) { + if d != time.Minute { + t.Errorf("the progress ticks every %s, want a minute", d) + } + return tick, func() { close(stopped) } + }, + run: func(ctx context.Context, _ string, _ meshwants.StepRun) stepOutcome { + setPhase(ctx, meshwants.StepProgressEventPhaseRun) + <-release + return stepOutcome{Outcome: meshwants.StepEndedOutcomePassed} + }} + body, _ := json.Marshal(meshwants.StepRun{Pipeline: "main.4.m", Step: meshwants.StepRunStepComposition, Module: "m", Branch: "main"}) + if !c.take(context.Background(), "ask-1", body) { + t.Fatal("the composition step is not taken") + } + progress := meshwants.EventStepProgress + ".n1" + for range 2 { + tick <- at + if e := <-said.heard; e != progress { + t.Fatalf("said %s on a tick, want %s", e, progress) + } + } + said.mu.Lock() + p, ok := said.said[progress].(meshwants.StepProgressEvent) + said.mu.Unlock() + if !ok || p.Ask != "ask-1" || p.Asker != "mesh-pipeline-runner" || p.Phase != meshwants.StepProgressEventPhaseRun || !p.At.Equal(at) { + t.Fatalf("step-progress %+v", p) + } + close(release) + if e := <-said.heard; e != meshwants.EventStepEnded+".n1" { + t.Fatalf("said %s at the end, want step-ended", e) + } + select { + case <-stopped: + default: + t.Fatal("the ticker is not stopped when the step has ended") + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/delivered.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/delivered.go new file mode 100644 index 00000000..c49f7e43 --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/delivered.go @@ -0,0 +1,451 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "slices" + "sort" + "strings" + "sync" + "time" + + "github.com/nats-io/nats.go/jetstream" + + wd "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" + "git.novox.be/novox/mesh-sdk/go/keys" + "git.novox.be/novox/mesh-sdk/go/process" + + "mesh-delivery-assigner/internal/assign" + "mesh-delivery-assigner/internal/assignment" + "mesh-delivery-assigner/internal/buckets" +) + +// A delivery is waited for mesh-wide (design 47 G §376, §389, A-189, A-246). At its start the assigner reads whole, from their first +// message, the streams of the catalog seat's version-released and of its own version-delivered (mesh-sdk process.FromFirst, A-233), +// and the watchman's machine-away and machine-back, never its state; then it hears them. A released version of a module is said +// version-delivered on the seat mesh-wants once, when every present machine placing it on the branch it is released from runs, healthy, +// the want the assigner wrote naming that version; a version no present machine places is not delivered. A dependent whose want +// changes fingerprint waits, by an edge of kind 2, 4 or 6, for its dependency's newest released version not yet said delivered, on +// the branch it is released from; on another branch no version is released and nothing waits. The wait is said wait-held, step +// delivery, bound 60 minutes from the version's release, kept in the state delivery-waits under the dependent's name, and said +// wait-ended when the version is delivered (ran) or released by hand (assigner_release, released). + +// deliveryWaitBound is a dependent's delivery wait's bound, from its dependency's version-released. +const deliveryWaitBound = 60 * time.Minute + +// Wait-ended's how of a delivery wait. +const ( + deliveryWaitRan = "ran" + deliveryWaitReleased = "released" +) + +// releaseHeard is one version-released heard: the version, each part's digest, the source revision and when it was said. +type releaseHeard struct { + Version string + Parts map[string]string + Source int64 + At time.Time +} + +// newestOf is the newest version of module released, the last heard of it in the stream's order. +func (r *releasedVersions) newestOf(module string) (releaseHeard, bool) { + r.mu.Lock() + defer r.mu.Unlock() + h, ok := r.newest[module] + return h, ok +} + +// deliveredVersions are the version-delivered said, the assigner's own, by module and version: each is said once. +type deliveredVersions struct { + mu sync.Mutex + said map[string]bool +} + +func (d *deliveredVersions) key(module, version string) string { return module + "@" + version } + +// put takes one version-delivered in. +func (d *deliveredVersions) put(data []byte) error { + var e meshwants.VersionDeliveredEvent + if err := json.Unmarshal(data, &e); err != nil { + return fmt.Errorf("a version-delivered that is none: %w", err) + } + if e.Module == "" || e.Version == "" { + return fmt.Errorf("a version-delivered naming no module or version: %s", data) + } + d.mark(e.Module, e.Version) + return nil +} + +func (d *deliveredVersions) mark(module, version string) { + d.mu.Lock() + defer d.mu.Unlock() + if d.said == nil { + d.said = map[string]bool{} + } + d.said[d.key(module, version)] = true +} + +// has says whether version-delivered of module's version was said. +func (d *deliveredVersions) has(module, version string) bool { + d.mu.Lock() + defer d.mu.Unlock() + return d.said[d.key(module, version)] +} + +// presenceOf reads one machine-away or machine-back: the machine and when it was said. +func presenceOf(data []byte) (string, time.Time, error) { + var e wd.MachineAwayEvent // machine-back has the same fields + if err := json.Unmarshal(data, &e); err != nil { + return "", time.Time{}, err + } + if e.Machine == "" { + return "", time.Time{}, fmt.Errorf("it names no machine: %s", data) + } + return e.Machine, e.At, nil +} + +// hearDelivery reads whole, then follows, the four streams a delivery is judged from: version-released, version-delivered, +// machine-away and machine-back. A stream not heard is said as the assigner's finding, and what it carries is not known until +// the assigner starts again. +func hearDelivery(ctx context.Context, js jetstream.JetStream, w *world, wake func(), findings assign.Findings, logf func(string, ...any)) { + acct := process.Account{Node: thisNode, Module: assignment.Module()} + nh := notHeardWriter(findings, logf) + for _, s := range []struct { + stream, words string + take func([]byte) error + }{ + {meshcatalog.StreamVersionReleased, "no want names a released version and no delivery is judged", w.released.put}, + {meshwants.StreamVersionDelivered, "a version said delivered before is said again once it is judged", w.delivered.put}, + {wd.StreamMachineAway, "no machine is known away, so a delivery waits for a machine that is away", func(data []byte) error { + m, at, err := presenceOf(data) + if err == nil { + w.heardPresence(m, true, at) + } + return err + }}, + {wd.StreamMachineBack, "a machine once away is not known back", func(data []byte) error { + m, at, err := presenceOf(data) + if err == nil { + w.heardPresence(m, false, at) + } + return err + }}, + } { + take := s.take + cc, err := acct.HearSeatEvents(ctx, js, s.stream, func(m jetstream.Msg) { + if err := take(m.Data()); err != nil { + logf("%s is not kept: %v", m.Subject(), err) + } + if err := m.Ack(); err != nil { + logf("acknowledging %s: %v", m.Subject(), err) + } + wake() + }, logf, process.FromFirst()) + if err != nil { + nh.NotHeard(s.stream, s.words+" until the assigner starts again", err) + continue + } + nh.Heard(s.stream) + go func() { + <-ctx.Done() + cc.Stop() + }() + } +} + +// releaseBranch is the branch module's versions are released from: the first watched branch of its registration (§371). +func (p *passer) releaseBranch(module string) string { + if p.ends.following() { + return p.ends.branchOf(module, "") + } + return p.w.trunkName() +} + +// placing is every machine placing module on the branch its versions are released from, sorted. +func (p *passer) placing(module string) []string { + branch := p.releaseBranch(module) + var out []string + for node, placed := range p.w.placedBranches()[module] { + if p.ends.following() { + placed = p.ends.branchOf(module, placed) + } else { + placed = p.w.trunkName() // the trunk-only form: every placement runs the trunk's + } + if placed == branch { + out = append(out, node) + } + } + sort.Strings(out) + return out +} + +// deliveryStanding is where module's version stands: the present placing machines not yet running it healthy as its want names +// it, and the placing machines away. +func (p *passer) deliveryStanding(module, version string) (notRunning, away []string, placing int) { + gone := p.w.awayNow() + n := assign.Nodes{Running: p.w.runningNow()} + wanted := p.w.wantsOf()[module] + nodes := p.placing(module) + for _, node := range nodes { + if gone[node] { + away = append(away, node) + continue + } + want, ok := wanted[node] + if !ok || want.Removed || want.Version != version || !n.RunsHealthy(node, module, want.Fp) { + notRunning = append(notRunning, node) + } + } + return notRunning, away, len(nodes) +} + +// judgeDelivered says version-delivered of each module's newest released version not said yet, once every present machine placing +// it runs it healthy; judged again at each pass, which each running, machine-away, machine-back and placement change wakes. +func (p *passer) judgeDelivered() { + for _, module := range p.releasedModules() { + rel, _ := p.w.released.newestOf(module) + if p.w.delivered.has(module, rel.Version) { + continue + } + notRunning, away, placing := p.deliveryStanding(module, rel.Version) + if placing == 0 || len(away) == placing || len(notRunning) > 0 { + continue + } + if err := p.sayDelivered(module, rel, away); err != nil { + p.logf("version-delivered of %s %s is not said, and is judged again at the next pass: %v", module, rel.Version, err) + } + } +} + +// sayDelivered says version-delivered of module's release rel, with the machines away, and keeps it said. +func (p *passer) sayDelivered(module string, rel releaseHeard, away []string) error { + at := p.clock().UTC() + body := meshwants.VersionDeliveredEvent{V: keys.Version, Module: module, Version: rel.Version, Parts: rel.Parts, Away: away, At: &at} + if err := p.wantsSay(meshwants.EventVersionDelivered, body, fmt.Sprintf("%s.%s.%s.%s", meshwants.EventVersionDelivered, module, rel.Version, at.Format(time.RFC3339Nano))); err != nil { + return err + } + p.w.delivered.mark(module, rel.Version) + return nil +} + +// releasedModules is every module a version-released was heard of, sorted. +func (p *passer) releasedModules() []string { + r := &p.w.released + r.mu.Lock() + defer r.mu.Unlock() + out := make([]string, 0, len(r.newest)) + for m := range r.newest { + out = append(out, m) + } + sort.Strings(out) + return out +} + +// deliveryWait is what a dependent waits for: its dependency, the version and the machines not yet running it. +type deliveryWait struct { + dependency string + rel releaseHeard + notRunning []string + away []string +} + +// waitsForDelivery says whether module, whose want on its nodes changes to fp on branch, waits for a dependency's delivery: by +// each edge of kind 2, 4 or 6 into it in branch's catalog half, the dependency's newest released version on branch not yet said +// delivered. A want that changes no fingerprint waits for nothing; on a branch no version is released from, nothing waits. +func (p *passer) waitsForDelivery(module, fp, branch string, nodes []string) (deliveryWait, bool) { + wanted := p.w.wantsOf()[module] + changes := false + for _, node := range nodes { + if w, ok := wanted[node]; !ok || w.Removed || w.Fp != fp { + changes = true + } + } + if !changes || fp == "" { + return deliveryWait{}, false + } + var deps []string + for _, e := range p.w.edgesOf(branch, module) { + if e.Kind != 2 && e.Kind != 4 && e.Kind != 6 { + continue + } + dep, _, _ := strings.Cut(e.From, ":") + if dep != "" && dep != module && !slices.Contains(deps, dep) { + deps = append(deps, dep) + } + } + sort.Strings(deps) + for _, dep := range deps { + if p.releaseBranch(dep) != branch { + continue + } + rel, ok := p.w.released.newestOf(dep) + if !ok || p.w.delivered.has(dep, rel.Version) { + continue + } + notRunning, away, _ := p.deliveryStanding(dep, rel.Version) + return deliveryWait{dependency: dep, rel: rel, notRunning: notRunning, away: away}, true + } + return deliveryWait{}, false +} + +// words is a delivery wait in plain words, as assigner_why_not and the status view say it. +func (d deliveryWait) words() string { + if len(d.notRunning) == 0 && len(d.away) > 0 { + return fmt.Sprintf("waiting for %s to be back", strings.Join(d.away, ", ")) + } + return fmt.Sprintf("waiting for delivery of %s %s on %s", d.dependency, d.rel.Version, strings.Join(d.notRunning, ", ")) +} + +// deliveryWaitBody is the wait-held of module's delivery wait d. +func deliveryWaitBody(module, fp string, d deliveryWait, since time.Time) meshwants.Wait { + from := d.rel.At.UTC() + if from.IsZero() { + from = since.UTC() + } + return meshwants.Wait{Step: meshwants.WaitStepDelivery, Owner: meshwants.Seat, Module: module, Version: fp, Part: d.dependency, + On: d.dependency + "@" + d.rel.Version, Machines: d.notRunning, Since: since.UTC(), ClockFrom: &from, Bound: deliveryWaitBound.String(), + Words: d.words()} +} + +// deliveryWaiting is one delivery wait the state delivery-waits keeps, by its dependent's name: the wait-held said, and a release +// by hand written by assigner_release. +type deliveryWaiting struct { + meshwants.Wait + // Released is set by assigner_release: the wait ends released and the dependent's want is written. + Released bool `json:"released,omitempty"` +} + +// saidDeliveryWaits is every delivery wait the state delivery-waits holds, by module; a value that does not decode is said and left. +func (p *passer) saidDeliveryWaits() map[string]deliveryWaiting { + out := map[string]deliveryWaiting{} + for key, value := range p.w.read(buckets.DeliveryWaits()) { + var w deliveryWaiting + if err := json.Unmarshal(value, &w); err != nil || w.Module == "" { + p.logf("%s %s holds no delivery wait, left: %v", buckets.DeliveryWaits(), key, err) + continue + } + out[w.Module] = w + } + return out +} + +// holdForDelivery says whether module's want, changing to fp on branch, is held for a dependency's delivery this pass: it says +// wait-held once and keeps it; a wait released by hand holds nothing; a wait that no longer holds is ended ran. +func (p *passer) holdForDelivery(ctx context.Context, module, fp, branch string, nodes []string) bool { + if p.kvs[buckets.DeliveryWaits()] == nil { + return false + } + key := keys.EscapeName(module) + said, was := p.saidDeliveryWaits()[module] + d, waits := p.waitsForDelivery(module, fp, branch, nodes) + switch { + case was && said.Released: + p.endDeliveryWait(ctx, key, said, deliveryWaitReleased) // the want is written now, so it changes nothing at the next pass + return false + case !waits: + if was { + p.endDeliveryWait(ctx, key, said, deliveryWaitRan) + } + return false + } + body := deliveryWaitBody(module, fp, d, p.clock()) + if was && said.On == body.On && said.Version == fp && slices.Equal(said.Machines, body.Machines) { + return true + } + if was { + body.Since = said.Since + } + id := fmt.Sprintf("%s.delivery.%s.%s.%s", key, fp, body.On, strings.Join(body.Machines, ",")) + if err := p.wantsSay(meshwants.EventWaitHeld, body, id); err != nil { + p.logf("the delivery wait of %s is not said, and is said at the next pass: %v", module, err) + return true + } + kept := deliveryWaiting{Wait: body} + p.put(ctx, buckets.DeliveryWaits(), key, func() ([]byte, error) { return json.Marshal(kept) }) + return true +} + +// endDeliveryWait says wait-ended of a delivery wait with how and removes it from the state. +func (p *passer) endDeliveryWait(ctx context.Context, key string, w deliveryWaiting, how string) { + body := w.Wait + body.How = how + if err := p.wantsSay(meshwants.EventWaitEnded, body, key+".delivery."+body.Version+"."+how); err != nil { + p.logf("the end of the delivery wait of %s is not said, and is said at the next pass: %v", w.Module, err) + return + } + p.remove(ctx, buckets.DeliveryWaits(), key) +} + +// againAsk is one ask of assigner_delivered_again, kept in the state delivered-again under its module (A-250): the version to say +// delivered again, who asked and why, and, while it does not hold, why not. +type againAsk struct { + Module string `json:"module"` + Version string `json:"version"` + By string `json:"by"` + Why string `json:"why"` + Waits string `json:"waits,omitempty"` +} + +// releaseOf is the release heard of module's version. +func (r *releasedVersions) releaseOf(module, version string) (releaseHeard, bool) { + r.mu.Lock() + defer r.mu.Unlock() + if h, ok := r.newest[module]; ok && h.Version == version { + return h, true + } + for key, k := range r.seen { + if strings.HasPrefix(key, module+"@") && k.Version == version { + return releaseHeard{Version: version, Parts: k.Parts}, true + } + } + return releaseHeard{}, false +} + +// deliveredAgain takes each ask of assigner_delivered_again, heard at once through the watch of the state delivered-again: it says +// version-delivered again when every present machine placing the module runs that version healthy and removes the ask, else keeps +// it with why it does not hold, which assigner_waits reads. +func (p *passer) deliveredAgain(ctx context.Context) { + if p.kvs[buckets.DeliveredAgain()] == nil { + return + } + for key, value := range p.w.read(buckets.DeliveredAgain()) { + var a againAsk + if err := json.Unmarshal(value, &a); err != nil || a.Version == "" { + p.logf("%s %s holds no ask, left: %v", buckets.DeliveredAgain(), key, err) + continue + } + module := a.Module + if module == "" { + module = key + } + waits := "" + rel, ok := p.w.released.releaseOf(module, a.Version) + notRunning, away, placing := p.deliveryStanding(module, a.Version) + switch { + case !ok: + waits = fmt.Sprintf("no version-released of %s %s is heard", module, a.Version) + case placing == 0: + waits = fmt.Sprintf("no machine places %s", module) + case len(away) == placing: + waits = fmt.Sprintf("every machine placing %s is away: %s", module, strings.Join(away, ", ")) + case len(notRunning) > 0: + waits = fmt.Sprintf("%s %s does not run healthy on %s", module, a.Version, strings.Join(notRunning, ", ")) + } + if waits == "" { + if err := p.sayDelivered(module, rel, away); err != nil { + p.logf("version-delivered of %s %s asked again is not said, and is asked at the next pass: %v", module, a.Version, err) + continue + } + p.logf("version-delivered of %s %s said again, asked by %s: %s", module, a.Version, a.By, a.Why) + p.remove(ctx, buckets.DeliveredAgain(), key) + continue + } + if a.Waits != waits { + a.Waits = waits + p.put(ctx, buckets.DeliveredAgain(), key, func() ([]byte, error) { return json.Marshal(a) }) + } + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/delivered_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/delivered_test.go new file mode 100644 index 00000000..a8d3827f --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/delivered_test.go @@ -0,0 +1,188 @@ +package main + +import ( + "context" + "encoding/json" + "strings" + "testing" + "time" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient" + mwc "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" + + "mesh-delivery-assigner/internal/buckets" +) + +// Design 47 G §376, §389, §498, A-189, A-246: a released version is said version-delivered once, when every present machine placing +// it runs healthy the want naming it, a machine away named and not waited for, none when every placing machine is away; a dependent +// whose want changes fingerprint waits by an edge of kind 2, 4 or 6 for its dependency's newest released version not yet said +// delivered, said wait-held (step delivery, 60 minutes from the release) once and kept in delivery-waits, and wait-ended ran once +// the version is delivered; a kind-1 edge, or a dependency with no version released, waits for nothing; one released by hand ends +// released and its want is written. +func TestADependentWaitsForItsDependencysDelivery(t *testing.T) { + ctx := context.Background() + at := time.Date(2026, 11, 12, 9, 0, 0, 0, time.UTC) + waits := &memKV{values: map[string][]byte{}} + again := &memKV{values: map[string][]byte{}} + p := newPasser(kvSet{buckets.Wants(): &memKV{values: map[string][]byte{}}, buckets.DeliveryWaits(): waits, buckets.DeliveredAgain(): again}, at) + p.logf = t.Logf + var events []said + p.publishWants = func(event string, body any, id string) error { + raw, _ := json.Marshal(body) + events = append(events, said{event, id, raw}) + return nil + } + p.w.runningHeard = true + placeOn(t, p.w, "ace", "db", "app") + placeOn(t, p.w, "g14", "db") + fp := func(c string) string { return meshcatalogclient.SourcePrefix2 + strings.Repeat(c, 64) } + want := func(node, module, fp, version string) { + key, _ := mwc.WantKey(node, module) + raw, err := mwc.EncodeWant(mwc.Want{Fp: fp, Version: version, Self: true}) + if err != nil { + t.Fatal(err) + } + p.w.put(buckets.Wants(), key, raw) + } + runs := func(node, module, fp string) { + raw, _ := json.Marshal(map[string]any{"v": 1, "node": node, "module": module, "fingerprint": fp, "at": at}) + if err := p.w.hearRunning("running", raw); err != nil { + t.Fatal(err) + } + } + source := int64(3) + release := func(module, version string) { + source++ + raw, _ := json.Marshal(meshcatalog.VersionReleasedEvent{V: 1, Module: module, Version: version, Source: source, At: &at, + Artifacts: []meshcatalog.VersionArtifact{{Artifact: "program", Digest: "sha256:d"}}}) + if err := p.w.released.put(raw); err != nil { + t.Fatal(err) + } + } + saidOf := func(event string) []said { + var out []said + for _, e := range events { + if e.event == event { + out = append(out, e) + } + } + return out + } + want("ace", "app", fp("1"), "") + + // No version of db is released: app's new fingerprint waits for nothing. + p.w.putEdges(trunkBranch, "app", []meshcatalog.Edge{{Kind: 4, From: "db:program", To: "app:program"}}) + if p.holdForDelivery(ctx, "app", fp("2"), trunkBranch, []string{"ace"}) { + t.Fatal("app waits with no version of db released") + } + release("db", "1.1.0") + want("ace", "db", fp("d"), "1.1.0") + want("g14", "db", fp("d"), "1.1.0") + runs("ace", "db", fp("d")) + // A kind-1 edge orders nothing. + p.w.putEdges(trunkBranch, "app", []meshcatalog.Edge{{Kind: 1, From: "db:program", To: "app:program"}}) + if p.holdForDelivery(ctx, "app", fp("2"), trunkBranch, []string{"ace"}) { + t.Fatal("a kind-1 edge holds app") + } + p.w.putEdges(trunkBranch, "app", []meshcatalog.Edge{{Kind: 4, From: "db:program", To: "app:program"}}) + // app's want unchanged waits for nothing. + if p.holdForDelivery(ctx, "app", fp("1"), trunkBranch, []string{"ace"}) { + t.Fatal("app's unchanged want waits") + } + // g14 places db and does not run 1.1.0 yet: not delivered, and app's new version waits, said once. + p.judgeDelivered() + if !p.holdForDelivery(ctx, "app", fp("2"), trunkBranch, []string{"ace"}) || !p.holdForDelivery(ctx, "app", fp("2"), trunkBranch, []string{"ace"}) { + t.Fatal("app does not wait for db 1.1.0") + } + if len(saidOf(meshwants.EventVersionDelivered)) != 0 { + t.Fatalf("db is said delivered while g14 does not run it: %+v", events) + } + held := saidOf(meshwants.EventWaitHeld) + if len(held) != 1 { + t.Fatalf("wait-held said %d times, want once", len(held)) + } + var w meshwants.Wait + _ = json.Unmarshal(held[0].body, &w) + if w.Step != meshwants.WaitStepDelivery || w.Part != "db" || w.Module != "app" || w.Version != fp("2") || w.Bound != "1h0m0s" || + w.ClockFrom == nil || !w.ClockFrom.Equal(at) || len(w.Machines) != 1 || w.Machines[0] != "g14" || w.Words != "waiting for delivery of db 1.1.0 on g14" { + t.Errorf("the wait said is %+v", w) + } + if _, ok := waits.values["app"]; !ok { + t.Errorf("the wait is not kept in delivery-waits: %v", waits.values) + } + // g14 away: every present machine runs it, so it is delivered, g14 named away; once only. + p.w.heardPresence("g14", true, at) + p.judgeDelivered() + p.judgeDelivered() + delivered := saidOf(meshwants.EventVersionDelivered) + var d meshwants.VersionDeliveredEvent + if len(delivered) != 1 || json.Unmarshal(delivered[0].body, &d) != nil || d.Module != "db" || d.Version != "1.1.0" || + len(d.Away) != 1 || d.Away[0] != "g14" || d.Parts["db:program"] != "sha256:d" { + t.Fatalf("version-delivered said %+v", delivered) + } + // The wait ends ran, and app's want goes on. + if p.holdForDelivery(ctx, "app", fp("2"), trunkBranch, []string{"ace"}) { + t.Fatal("app still waits once db 1.1.0 is delivered") + } + ended := saidOf(meshwants.EventWaitEnded) + if len(ended) != 1 || json.Unmarshal(ended[0].body, &w) != nil || w.How != deliveryWaitRan { + t.Errorf("wait-ended said %+v", ended) + } + if _, ok := waits.values["app"]; ok { + t.Error("the ended wait is kept") + } + // A newer release with every placing machine away is not delivered, and its wait says the machines it waits to be back. + release("db", "1.2.0") + p.w.heardPresence("ace", true, at) + p.judgeDelivered() + if len(saidOf(meshwants.EventVersionDelivered)) != 1 { + t.Error("a version is said delivered while every placing machine is away") + } + if !p.holdForDelivery(ctx, "app", fp("3"), trunkBranch, []string{"ace"}) { + t.Fatal("app does not wait for db 1.2.0") + } + held = saidOf(meshwants.EventWaitHeld) + if json.Unmarshal(held[len(held)-1].body, &w) != nil || w.Words != "waiting for ace, g14 to be back" { + t.Errorf("the wait while every machine is away says %q", w.Words) + } + // Released by hand (assigner_release writes it): the wait ends released and app's want is written. + var kept deliveryWaiting + _ = json.Unmarshal(waits.values["app"], &kept) + kept.Released = true + raw, _ := json.Marshal(kept) + p.w.put(buckets.DeliveryWaits(), "app", raw) + if p.holdForDelivery(ctx, "app", fp("3"), trunkBranch, []string{"ace"}) { + t.Fatal("a released wait holds app") + } + ended = saidOf(meshwants.EventWaitEnded) + if json.Unmarshal(ended[len(ended)-1].body, &w) != nil || w.How != deliveryWaitReleased { + t.Errorf("the released wait ended %+v", w) + } + // Presence: the later of machine-away and machine-back holds, whichever stream is read first. + p.w.heardPresence("ace", false, at.Add(time.Minute)) + p.w.heardPresence("ace", true, at) + if p.w.awayNow()["ace"] || !p.w.online()["ace"] { + t.Error("an older machine-away read after a newer machine-back leaves ace away") + } + // A-250: an ask of assigner_delivered_again of 1.1.0, which ace (present) runs and g14 (away) does not, is said again and taken + // away; one of 1.2.0, which no machine runs, is kept with why. + ask := func(version string) { + raw, _ := json.Marshal(againAsk{Module: "db", Version: version, By: "op", Why: "the catalog missed it"}) + p.w.put(buckets.DeliveredAgain(), "db", raw) + } + before := len(saidOf(meshwants.EventVersionDelivered)) + ask("1.1.0") + p.deliveredAgain(ctx) + if len(saidOf(meshwants.EventVersionDelivered)) != before+1 || p.w.has(buckets.DeliveredAgain(), "db") { + t.Errorf("the ask of 1.1.0 said %d version-delivered and is kept %v", len(saidOf(meshwants.EventVersionDelivered))-before, p.w.has(buckets.DeliveredAgain(), "db")) + } + ask("1.2.0") + p.deliveredAgain(ctx) + var a againAsk + if json.Unmarshal(p.w.value(buckets.DeliveredAgain(), "db"), &a) != nil || a.Waits != "db 1.2.0 does not run healthy on ace" || + len(saidOf(meshwants.EventVersionDelivered)) != before+1 { + t.Errorf("the ask of 1.2.0 = %+v", a) + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graph_tools.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graph_tools.go new file mode 100644 index 00000000..9a6abb17 --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graph_tools.go @@ -0,0 +1,82 @@ +package main + +import ( + "context" + "encoding/json" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" + "git.novox.be/novox/mesh-sdk/go/busstate" + + "mesh-delivery-assigner/internal/assigngraph" +) + +// runtimeObjects is the kept data assignment-revisions as the bundle reaches it through its runtime (the old objects +// section of a foundation part, design 47 lines 1362-1366). +type runtimeObjects struct{ o busstate.Objects } + +func (r runtimeObjects) Put(_ context.Context, name string, data []byte) error { + _, err := r.o.Put(name, data, "") + return err +} + +func (r runtimeObjects) Get(_ context.Context, name string) ([]byte, error) { + d, err := r.o.Get(name) + if err != nil { + return nil, err + } + if d.Info.Name == "" || d.Info.Deleted { + return nil, assigngraph.ErrNoObject + } + return d.Data, nil +} + +func (r runtimeObjects) Names(context.Context) ([]string, error) { + infos, err := r.o.List() + if err != nil { + return nil, err + } + names := []string{} + for _, i := range infos { + if !i.Deleted { + names = append(names, i.Name) + } + } + return names, nil +} + +// graphVerbs are the seat mesh-wants' verbs revision, entry and difference (design 47 lines 1365-1366: an assignment graph +// revision is served by the seat's verbs), each reading only the kept revisions. +func graphVerbs(o assigngraph.Objects) map[string]func(map[string]any) (any, error) { + return map[string]func(map[string]any) (any, error){ + meshwants.VerbRevision: func(a map[string]any) (any, error) { + var in meshwants.RevisionInput + if err := decodeArgs(a, &in); err != nil { + return nil, err + } + return assigngraph.Revision(context.Background(), o, in) + }, + meshwants.VerbEntry: func(a map[string]any) (any, error) { + var in meshwants.EntryInput + if err := decodeArgs(a, &in); err != nil { + return nil, err + } + return assigngraph.Entry(context.Background(), o, in) + }, + meshwants.VerbDifference: func(a map[string]any) (any, error) { + var in meshwants.DifferenceInput + if err := decodeArgs(a, &in); err != nil { + return nil, err + } + return assigngraph.Difference(context.Background(), o, in) + }, + } +} + +// decodeArgs reads a verb's arguments into its input type; a caller's own fields beside them are the runtime's. +func decodeArgs(a map[string]any, into any) error { + raw, err := json.Marshal(a) + if err != nil { + return err + } + return json.Unmarshal(raw, into) +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphfill.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphfill.go new file mode 100644 index 00000000..a3b79267 --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphfill.go @@ -0,0 +1,82 @@ +package main + +import ( + "context" + "fmt" + + meshwants "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" + + "mesh-delivery-assigner/internal/assigngraph" +) + +// A store filling is said before it is full (design 47 G §181, in the assigner's part): the assignment graph revisions' object store +// says `filling` of the seat mesh-wants when it passes 80 % of its cap, and `full` when the next revision does not fit: the assigner then keeps +// its last revision and writes no new want, and says `room` when the store is back below 80 % and goes on. Nothing is dropped to make room. + +// fillOf is the store's fill now: its bytes and cap, when the bucket says them and has a cap. +func (g *graphKeeper) fillOf(ctx context.Context) (used, capacity int64, ok bool) { + f, is := g.objects.(assigngraph.Filler) + if !is { + return 0, 0, false + } + used, capacity, err := f.Fill(ctx) + return used, capacity, err == nil && capacity > 0 +} + +func (p *passer) sayFill(event, about string, used, capacity int64, refused string) { + at := p.clock().UTC() + body := meshwants.Fill{Store: assigngraph.RevisionsObjects, Bytes: used, Cap: capacity, Refused: refused, At: &at} + if err := p.wantsSay(event, body, event+"-"+about); err != nil { + p.logf("%s of %s is not said: %v", event, assigngraph.RevisionsObjects, err) + } +} + +// fillAtStart judges the store once at the process's first pass, when no event is pending: a store past 80 % says filling (full when at +// its cap), so a fill reached while the process was down is said too. +func (p *passer) fillAtStart(ctx context.Context) { + g := p.graph + used, capacity, ok := g.fillOf(ctx) + if !ok { + return + } + switch { + case used >= capacity: + g.full = true + p.sayFill(meshwants.EventFull, "start", used, capacity, "the next revision") + case used*5 >= capacity*4: + p.sayFill(meshwants.EventFilling, "start", used, capacity, "") + } +} + +// fullBefore says the next revision does not fit its store, and keeps nothing: it says `full` once, and `room` once the store is below +// 80 % again. It answers true while the store is full. +func (p *passer) fullBefore(ctx context.Context, c assigngraph.Computed) bool { + g := p.graph + used, capacity, ok := g.fillOf(ctx) + if !ok { + return false + } + if g.full && used*5 < capacity*4 { + g.full = false + p.sayFill(meshwants.EventRoom, fmt.Sprint(used), used, capacity, "") + } + if used+assigngraph.NewBytes(ctx, g.objects, c) <= capacity { + g.full = false + return false + } + if !g.full { + g.full = true + p.sayFill(meshwants.EventFull, fmt.Sprint(used), used, capacity, "the next revision") + } + p.logf("the assignment graph revision is not kept: %s is full (%d of %d bytes), the last revision stays and no want is written until there is room", assigngraph.RevisionsObjects, used, capacity) + return true +} + +// fillingAfter says `filling` when keeping a revision took the store past 80 % of its cap. +func (p *passer) fillingAfter(ctx context.Context, before int64) { + g := p.graph + used, capacity, ok := g.fillOf(ctx) + if ok && before*5 < capacity*4 && used*5 >= capacity*4 { + p.sayFill(meshwants.EventFilling, fmt.Sprint(used), used, capacity, "") + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphfill_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphfill_test.go new file mode 100644 index 00000000..af05f547 --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphfill_test.go @@ -0,0 +1,83 @@ +package main + +import ( + "context" + "testing" + "time" + + mcc "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient" + meshwants "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" + + "mesh-delivery-assigner/internal/assigngraph" +) + +// filled is an object bucket with a fill: the bytes it holds count beside what is stored. +type filled struct { + objs map[string][]byte + base int64 + cap int64 +} + +func (f *filled) Put(_ context.Context, name string, data []byte) error { + f.objs[name] = data + return nil +} +func (f *filled) Get(_ context.Context, name string) ([]byte, error) { + if d, ok := f.objs[name]; ok { + return d, nil + } + return nil, assigngraph.ErrNoObject +} +func (f *filled) Names(context.Context) ([]string, error) { + out := []string{} + for n := range f.objs { + out = append(out, n) + } + return out, nil +} +func (f *filled) Fill(context.Context) (int64, int64, error) { + used := f.base + for _, d := range f.objs { + used += int64(len(d)) + } + return used, f.cap, nil +} + +// Design 47 G §181 (AS13): the assignment graph revisions' store says filling past 80 % of its cap, refuses a revision that does not fit +// with `full` (the last revision stays, no want is written), and says `room` below 80 % again; at the start a store already past 80 % is said. +func TestTheAssignmentGraphStoreSaysFillingFullAndRoom(t *testing.T) { + ctx := context.Background() + p := newPasser(kvSet{}, time.Now()) + var events []string + p.publishWants = func(event string, body any, id string) error { + if _, ok := body.(meshwants.Fill); ok { + events = append(events, event) + } + return nil + } + o := &filled{objs: map[string][]byte{}, base: 500, cap: 1000} + p.graph = &graphKeeper{objects: o, halves: func() map[string]mcc.Half { return nil }} + p.keepGraph(ctx) // the first pass judges at the start: 500 of 1000 is below 80 % + if len(events) != 0 { + t.Fatalf("events %v", events) + } + o.base = 700 + p.graph.started = false + p.keepGraph(ctx) + if len(events) != 1 || events[0] != meshwants.EventFilling { + t.Fatalf("at the start past 80 %%: %v", events) + } + c := assigngraph.Computed{Revision: meshwants.AssignmentRevision{Entries: []meshwants.NodeEntryRef{}}} + o.base = 990 + if !p.fullBefore(ctx, c) || !p.graph.full || events[len(events)-1] != meshwants.EventFull { + t.Fatalf("a revision that does not fit: events %v", events) + } + p.fullBefore(ctx, c) + if n := len(events); n != 2 { + t.Errorf("full is said once: %v", events) + } + o.base = 100 + if p.fullBefore(ctx, c) || p.graph.full || events[len(events)-1] != meshwants.EventRoom { + t.Errorf("room: events %v", events) + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go new file mode 100644 index 00000000..d8a20de6 --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go @@ -0,0 +1,192 @@ +package main + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "maps" + "slices" + "strings" + "time" + + mcc "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" + "git.novox.be/novox/mesh-sdk/go/keys" + + "mesh-delivery-assigner/internal/assign" + "mesh-delivery-assigner/internal/assigngraph" +) + +// graphKeeper is the pass's assignment graph (design 47 G §292, A-67): per catalog revision taken in it computes the revision +// from the catalog halves it follows, the placements, the first branch of each registration and each node's capability reading, +// keeps it in the objects assignment-revisions and says the seat mesh-wants' event assignment-revision; the wants of the pass +// that follows are written from the revision it holds. +type graphKeeper struct { + objects assigngraph.Objects + // halves is every followed branch's catalog graph revision held now. + halves func() map[string]mcc.Half + // last is the revision last computed, so a pass whose inputs did not change keeps and says nothing. + last meshwants.AssignmentRevision + has bool + // kept is the revision the wants follow, and kept's source the trunk's source revision it carries; ok is false until one is kept. + kept meshwants.AssignmentRevision + source int64 + ok bool + said string + // cycled is the cycles last said (assigngraph.Cycles). + cycled string + // full says the next revision did not fit the store, so no want is written (graphfill.go); started says the first pass judged the fill. + full, started bool + // pins removes a revision that ceased to be current once nothing pins it (pins.go); nil keeps every revision. + pins *pinKeeper + // computed is the revision computed by the last pass, with its entries: the edges the wants wait on (actingwaits.go). + computed assigngraph.Computed +} + +// keepGraph computes the revision of this pass and keeps it when the pass's inputs differ from the last computed. +func (p *passer) keepGraph(ctx context.Context) { + g := p.graph + if g == nil || g.objects == nil || g.halves == nil { + return + } + c := assigngraph.Compute(p.graphInputs()) + g.computed = c + if p.ends != nil { + p.ends.setRunner(assigngraph.Runner(slices.Collect(maps.Values(c.Entries)))) // the runner's member of a group goes first (A-334) + } + for _, u := range c.Uncomputed { + if g.said != u { + g.said = u + p.logf("the assignment graph leaves out %s", u) + } + } + if cycles := strings.Join(c.Cycles, "; "); cycles != g.cycled { + g.cycled = cycles + if cycles != "" { + p.logf("the assignment graph holds a cycle among the acting waits, within one step or across steps: %s", cycles) + } + } + if !g.started { + g.started = true + p.fillAtStart(ctx) + } + if g.has && g.ok && assigngraph.Same(g.last, c.Revision) { + return + } + used, _, _ := g.fillOf(ctx) + if p.fullBefore(ctx, c) { + return + } + rev, fresh, err := assigngraph.Keep(ctx, g.objects, c, meshwants.EventAssignmentRevision, p.clock()) + if err != nil { + p.logf("the assignment graph revision is not kept, so no want is said as wanted until it is: %v", err) + return + } + g.last, g.has, g.kept, g.ok = c.Revision, true, rev, true + g.source = 0 + for _, cat := range rev.Catalogs { + if cat.Branch == p.w.trunkName() { + g.source = cat.Source + } + } + if !fresh { + return + } + p.fillingAfter(ctx, used) + if g.pins != nil { + g.pins.judge() // the revision before ceased to be current + } + id := fmt.Sprintf("%s-%s-%d", meshwants.EventAssignmentRevision, rev.Kind, rev.Revision) + if err := p.wantsSay(meshwants.EventAssignmentRevision, assigngraph.Event(rev), id); err != nil { + p.logf("the assignment revision %d is kept but not said: %v", rev.Revision, err) + } +} + +// graphInputs are what the pass's assignment graph is computed from: every followed branch's catalog half, the placements, each +// node's capability reading and the first branch of each registration. +func (p *passer) graphInputs() assigngraph.Inputs { + in := assigngraph.Inputs{Halves: p.graph.halves(), Placements: p.w.assignments(), Capabilities: p.capabilityDigests()} + // The seat definitions read through the seat mesh-catalog's verb seat-definitions say which seat delivers a requirement + // (design 47 G §708). + if r := p.seats.Reader(); r != nil { + in.Definitions = r.Definitions() + } + // With no pipeline ends kept, the wants are the trunk-only form (pass.go): a placement naming no branch runs the trunk's. + first := p.w.trunkName() + in.FirstBranch = func(string) string { return first } + if p.ends != nil { + in.FirstBranch = func(module string) string { return p.ends.branchOf(module, "") } + } + return in +} + +// capabilityDigests is each node's capability reading, by node: the statement its engine made of what it reads, and the +// capabilities its tool runner states with the fingerprint of the runner that announced them, as the node's record carries +// them (design 46 approval mark §6, lead answer A-203), the digest over all of it. +func (p *passer) capabilityDigests() []meshwants.AssignmentRevisionCapabilitiesItem { + records := p.w.records() + known := p.w.knownNodes() + slices.Sort(known) + var out []meshwants.AssignmentRevisionCapabilitiesItem + for _, node := range known { + read := p.reads != nil && p.reads.Known(node) + rec, recorded := records[node] + if !read && !recorded { + continue + } + var reads []string + if read { + reads = slices.Clone(p.reads.Reads([]string{node})[node]) + slices.Sort(reads) + } + item := meshwants.AssignmentRevisionCapabilitiesItem{Node: node, Readings: slices.Clone(rec.RunnerCapabilities), + Fingerprint: rec.RunnerFingerprint} + slices.Sort(item.Readings) + raw, _ := json.Marshal(struct { + Reads []string `json:"reads"` + Readings []string `json:"readings"` + Fingerprint string `json:"fingerprint"` + }{reads, item.Readings, item.Fingerprint}) + sum := sha256.Sum256(raw) + item.Digest = "sha256:" + hex.EncodeToString(sum[:]) + out = append(out, item) + } + return out +} + +// sayWanted says `wanted` for a want written or removed now, from the revision the pass keeps (A-61, A-67); with none kept, it is +// said in the log that it was not said. fp is the want's, and for a removal the fingerprint last wanted there. +func (p *passer) sayWanted(node, module, fp string, removed bool) { + if p.publishWants == nil { + return + } + g := p.graph + if g == nil || !g.ok { + p.logf("the want of %s on %s is written, but no assignment graph revision is kept, so wanted is not said", module, node) + return + } + at := p.clock().UTC() + body := meshwants.WantedEvent{V: keys.Version, Node: node, Module: module, Fp: fp, Removed: removed, Revision: g.kept.Revision, Source: g.source, At: &at} + event := strings.TrimPrefix(meshwants.SubjectWanted(keys.EscapeName(node), keys.EscapeName(module)), "mesh.seat."+meshwants.Seat+".event.") + id := fmt.Sprintf("wanted.%s.%s.%s.%t.%d.%s", keys.EscapeName(node), keys.EscapeName(module), fp, removed, g.kept.Revision, at.Format(time.RFC3339Nano)) + if err := p.wantsSay(event, body, id); err != nil { + p.logf("wanted of %s on %s is not said: %v", module, node, err) + } +} + +// namedBuild answers what a want of module at fp names of its build (design 47 G §379, §526, A-96): a kept build's or another watched +// branch's parts and keeping node from the builder's `build.ended`; for the trunk's entry (fp is the module's, trunkFP), the released +// version and the parts of the `version-released` whose source is the one the kept assignment graph revision was computed from (A-67). +func (p *passer) namedBuild(module, trunkFP string) func(fp string) (assign.KeptBuild, bool) { + return func(fp string) (assign.KeptBuild, bool) { + if k, ok := p.w.ends.partsOf(module, fp); ok { + return k, true + } + if fp != trunkFP || p.graph == nil || !p.graph.ok { + return assign.KeptBuild{}, false + } + return p.w.released.of(module, p.graph.source) + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept_test.go new file mode 100644 index 00000000..225a98df --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept_test.go @@ -0,0 +1,134 @@ +package main + +import ( + "context" + "encoding/json" + "slices" + "strings" + "testing" + "time" + + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + mcc "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient" + mwc "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" + + "mesh-delivery-assigner/internal/assigngraph" + "mesh-delivery-assigner/internal/buckets" +) + +// said is one event the passer said on the seat mesh-wants. +type said struct { + event, id string + body json.RawMessage +} + +// Design 47 G A-67, §292: the pass keeps an assignment graph revision per catalog revision taken in, before it writes any want, and +// says assignment-revision; A-61, §693: a want written or removed is said `wanted` on the subject of its node and module in the same +// write, from the kept revision and its catalog's source, a put that wrote nothing says nothing, and with no revision kept none is said. +func TestTheWantsAreSaidWantedFromTheKeptAssignmentGraphRevision(t *testing.T) { + ctx := context.Background() + at := time.Date(2026, 11, 12, 9, 0, 0, 0, time.UTC) + kv := &memKV{values: map[string][]byte{}} + p := newPasser(kvSet{buckets.Wants(): kv}, at) + p.logf = t.Logf + var events []said + p.publishWants = func(event string, body any, id string) error { + raw, _ := json.Marshal(body) + events = append(events, said{event, id, raw}) + return nil + } + placeOn(t, p.w, "ace", "web") + // Design 46 approval mark §6: a node's reading keeps the fingerprint of the runner that announced it, as its record says. + rec, _ := json.Marshal(nodeengine.HaveByNodeNodeValue{V: 1, Record: &nodeengine.NodeRecord{V: 1, Node: "ace", + RunnerCapabilities: []string{"bus-state-calls", "approval-mark/1"}, RunnerFingerprint: "sha256:runner"}}) + p.w.put(buckets.Have, "ace.node", rec) + if c := p.capabilityDigests(); len(c) != 1 || c[0].Node != "ace" || c[0].Fingerprint != "sha256:runner" || + !slices.Equal(c[0].Readings, []string{"approval-mark/1", "bus-state-calls"}) || !strings.HasPrefix(c[0].Digest, "sha256:") { + t.Errorf("the capability readings are %+v", c) + } + half := mcc.Half{Revision: meshcatalog.CatalogRevision{Branch: trunkBranch, Kind: "trunk", Revision: 4, Source: 9, + Entries: []meshcatalog.EntryRef{{Module: "web", Digest: "sha256:" + strings.Repeat("a", 64)}}}, + Entries: map[string]meshcatalog.CatalogEntry{"web": {Module: "web", Fp: fpTest}}} + objects := memObjects{} + p.graph = &graphKeeper{objects: objects, halves: func() map[string]mcc.Half { return map[string]mcc.Half{trunkBranch: half} }} + want := mwc.Want{Fp: fpTest, Self: true} + + p.putWant(ctx, "ace", "web", want) // no revision kept yet: written, and not said + if len(events) != 0 || kv.puts != 1 { + t.Fatalf("with no revision kept: %d events, %d puts", len(events), kv.puts) + } + key, _ := mwc.WantKey("ace", "web") + delete(p.w.read(buckets.Wants()), key) // the world forgets the want, as before the first pass + kv.puts = 0 + p.w.put(buckets.Wants(), key, nil) + + p.keepGraph(ctx) + if len(events) != 1 || events[0].event != meshwants.EventAssignmentRevision || len(objects) == 0 { + t.Fatalf("the pass kept %d objects and said %+v", len(objects), events) + } + p.keepGraph(ctx) // the same inputs: nothing kept, nothing said + if len(events) != 1 { + t.Fatalf("a pass of the same inputs said %d events", len(events)) + } + p.putWant(ctx, "ace", "web", want) + p.putWant(ctx, "ace", "web", want) // unchanged: not written, not said + if len(events) != 2 || kv.puts != 1 { + t.Fatalf("a want put twice: %d events, %d puts", len(events), kv.puts) + } + if wantEvent := "wanted.ace.web"; events[1].event != wantEvent { + t.Errorf("wanted said on %q, want %q", events[1].event, wantEvent) + } + var w meshwants.WantedEvent + if err := json.Unmarshal(events[1].body, &w); err != nil || w.Node != "ace" || w.Module != "web" || w.Fp != fpTest || w.Removed || w.Revision != 1 || w.Source != 9 { + t.Errorf("wanted = %+v (%v)", w, err) + } + p.putWant(ctx, "ace", "web", mwc.Want{Removed: true}) + if err := json.Unmarshal(events[len(events)-1].body, &w); err != nil || len(events) != 3 || !w.Removed || w.Fp != fpTest { + t.Errorf("a removing want said %+v (%v), %d events", w, err, len(events)) + } + + // A-201, A-235: a run's candidate catalog revision, read whole through the seat's verb revision, keeps the run's assignment + // graph under candidate.. in place of its branch's half; it says nothing and writes no want, a try's candidate + // keeps none, and the run's group-ended drops it. + before, puts := len(events), kv.puts + ev := meshcatalog.CatalogRevisionEvent{Kind: "candidate", Branch: trunkBranch, Group: "run-7", Heads: "h1", Revision: 5} + var asked []meshcatalog.RevisionInput + call := func(key string, body any) (json.RawMessage, error) { + in := body.(meshcatalog.RevisionInput) + asked = append(asked, in) + rev := meshcatalog.CatalogRevision{Branch: trunkBranch, Kind: "candidate", Group: "run-7", Heads: "h1", Revision: 5, Source: 9, + Entries: []meshcatalog.EntryRef{{Module: "web", Digest: "sha256:" + strings.Repeat("b", 64)}}} + a := meshcatalog.RevisionAnswer{Revision: &rev, Entries: []meshcatalog.CatalogEntry{{Module: "web", Fp: "src2:w2"}}} + return json.Marshal(a) + } + if err := p.keepCandidate(ctx, call, meshcatalog.CatalogRevisionEvent{Kind: "candidate", Branch: trunkBranch, Group: "try-3", Heads: "h", Revision: 2}); err != nil || len(asked) != 0 { + t.Errorf("a try's candidate asked %v: %v", asked, err) + } + if err := p.keepCandidate(ctx, call, ev); err != nil { + t.Fatal(err) + } + if len(asked) != 1 || asked[0].Group != "run-7" || asked[0].Heads != "h1" || asked[0].Revision != 5 { + t.Errorf("the candidate's revision was asked as %+v", asked) + } + a, err := assigngraph.Revision(ctx, objects, meshwants.RevisionInput{Group: "run-7"}) + if err != nil || a.Revision.Kind != "candidate" || len(a.Revision.Catalogs) != 1 || a.Revision.Catalogs[0].Revision != 5 { + t.Errorf("the run's assignment graph is %+v: %v", a.Revision, err) + } + if e, err := assigngraph.Entry(ctx, objects, meshwants.EntryInput{Group: "run-7", Node: "ace", Module: "web"}); err != nil || e.Entry.Fp != "src2:w2" { + t.Errorf("the run's entry of web is %+v: %v", e.Entry, err) + } + if len(events) != before || kv.puts != puts { + t.Errorf("a candidate said %d events and wrote %d wants", len(events)-before, kv.puts-puts) + } + if err := p.dropCandidate(ctx, "run-7"); err != nil { + t.Fatal(err) + } + if _, ok := objects[assigngraph.CandidateObject("run-7", "h1")]; ok { + t.Error("the run's group-ended left its candidate kept") + } + if _, err := assigngraph.Revision(ctx, objects, meshwants.RevisionInput{}); err != nil { + t.Errorf("dropping the run's candidate dropped trunk: %v", err) + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/halves.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/halves.go new file mode 100644 index 00000000..558657ac --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/halves.go @@ -0,0 +1,163 @@ +package main + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "slices" + "sync" + "time" + + mcc "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/graph" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/views" +) + +// halves follows the catalog half of each watched branch through the seat mesh-catalog (design 47 G §14, §184, §195, §507, §532): the +// branch follower of meshcatalogclient, adopted on the module's views reader (A-26), follows the branches the registrations name +// and, before the first registrations, the branches the gate's verb `source` answers and its event `source-moved` brings; each +// branch's view asks the verb `revision`, hears each `catalog-revision` of that branch and raises and clears the follower's findings; +// each entry is taken into the world as the value and the graph node the rules read. Of the view the assigner reads only the catalog +// half, never its own output back (§195). +type halves struct { + w *world + call mcc.Call + reader *views.ProcessReader + wake func() + logf func(string, ...any) + ctx context.Context + + branches *mcc.Branches + mu sync.Mutex + // settled is told once when the first watched branch's first view is answered, for the first pass to wait on. + settled chan<- struct{} + told bool +} + +// follow makes the branch follower on the reader; the reader's start asks the first views. +func (h *halves) follow() error { + b, err := mcc.FollowBranchesProcess(h.reader, h.call) + if err != nil { + return err + } + b.Logf = h.logf + b.Changed = func(branch, module string, entry json.RawMessage, deleted bool, _ time.Time) { + h.changed(branch, module, entry, deleted) + } + b.Adopted = h.adopted + h.mu.Lock() + h.branches = b + h.mu.Unlock() + return nil +} + +// first is the first watched branch followed, the trunk's: "" while none is. +func (h *halves) first() string { + h.mu.Lock() + b := h.branches + h.mu.Unlock() + if b == nil { + return "" + } + if f := b.Followed(); len(f) > 0 { + return f[0] + } + return "" +} + +// adopted is told of each branch whose view the follower adopted: the first branch is the trunk's, and the first pass waits for its +// first view. +func (h *halves) adopted(branch string, c *mcc.Catalog) { + first := h.first() + if first == "" { + return + } + h.w.setTrunk(first) + h.wake() + if branch != first || h.settled == nil { + return + } + h.mu.Lock() + told := h.told + h.told = true + h.mu.Unlock() + if told { + return + } + go func() { + select { + case <-c.Ready(): + h.settled <- struct{}{} + case <-h.ctx.Done(): + } + }() +} + +// changed takes one entry of branch's half into the world, or a module taken out of it. +func (h *halves) changed(branch, module string, raw json.RawMessage, deleted bool) { + defer h.wake() + if deleted { + h.w.dropModule(branch, module) + return + } + var e meshcatalog.CatalogEntry + if err := json.Unmarshal(raw, &e); err != nil { + h.logf("the entry of %s on branch %q does not read: %v", module, branch, err) + return + } + v, n, m := fromEntry(e) + h.w.putModule(branch, module, v, n, m) + h.w.putEdges(branch, module, e.Edges) +} + +// fromEntry is a catalog entry as the value and the graph node the rules read, and its manifest parsed once under the digest of +// its words. +func fromEntry(e meshcatalog.CatalogEntry) (meshcatalog.ModulesByModuleValue, graph.Node, *parsed) { + v := meshcatalog.ModulesByModuleValue{Commit: e.Commit, Fp2: e.Fp, Needs: e.Needs, None2: e.None, Packages: e.Packages, + Path: e.Path, Repository: e.Repository, Verdict: e.Verdict} + n := graph.Node{FP: e.Fp, None: e.None, Repository: e.Repository, Path: e.Path, Bases: e.Bases, Provides: e.Provides, Requires: e.Requires, + Awaits: e.Awaits, Invokes: e.Invokes, Serves: e.Serves, LeftOut: e.LeftOut} + for p := range e.Packages { + n.Packages = append(n.Packages, p) + } + slices.Sort(n.Packages) + if len(e.Manifest) == 0 { + return v, n, nil + } + raw, err := json.Marshal(e.Manifest) + if err != nil { + return v, n, nil + } + sum := sha256.Sum256(raw) + v.Manifest = "sha256:" + hex.EncodeToString(sum[:]) + man, err := catalogue.Decode(raw) + if err != nil { + return v, n, &parsed{module: e.Module, err: "it does not decode: " + err.Error()} + } + n.Serves = graph.ServesOf(man) + // The entry's requires name the optional ones too; the node means those required, not optionally (A-229). + n.Requires = slices.DeleteFunc(slices.Clone(n.Requires), man.IsOptional) + return v, n, &parsed{module: e.Module, manifest: man} +} + +// views is the catalog graph revision held now of each followed branch whose first view is answered. +func (h *halves) views() map[string]mcc.Half { + h.mu.Lock() + b := h.branches + h.mu.Unlock() + out := map[string]mcc.Half{} + if b == nil { + return out + } + for _, branch := range b.Followed() { + if c, ok := b.Catalog(branch); ok { + if half, ok := c.View(); ok { + out[branch] = half + } + } + } + return out +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/inputs.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/inputs.go new file mode 100644 index 00000000..211bb25e --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/inputs.go @@ -0,0 +1,377 @@ +package main + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "regexp" + "slices" + "strings" + "sync" + "time" + + "github.com/nats-io/nats.go/jetstream" + + meshwants "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep" + stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/buscalls" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/keys" + "git.novox.be/novox/mesh-sdk/go/process" + "git.novox.be/novox/mesh-sdk/go/stateobjects" +) + +// A step's input by its digest (design 47 G §354, §712, A-237, A-243): the assigner keeps the input of each composition step +// request it asked a build seat to run in its state `inputs`, one key `