From 4087280895228c20454d9e6365b7ec46d9f35132 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 12:39:29 +0100 Subject: [PATCH 01/15] A manifest whose judge does not know a field has the verdict waits, held like a refusal by every reader (ADR 0578); in a modules value it is written as a refusal naming the fields, so a reader pinned before holds it too --- go/delivery/source.go | 9 +++++ go/delivery/verdicts.go | 64 ++++++++++++++++++++++++++++++++---- go/delivery/verdicts_test.go | 37 +++++++++++++++++++++ 3 files changed, 103 insertions(+), 7 deletions(-) diff --git a/go/delivery/source.go b/go/delivery/source.go index dac8338..c872d5a 100644 --- a/go/delivery/source.go +++ b/go/delivery/source.go @@ -431,6 +431,10 @@ type Module struct { // that state. Empty in a value written before the indexer judged, which a reader takes as accepted (TakeManifestVerdict). Verdict string `json:"verdict,omitempty"` Problems []string `json:"problems,omitempty"` + // Unknown are the fields of Manifest the judge does not know. A manifest that waits for them (VerdictManifestWaits) is + // written here as a refusal whose Problems say what it waits on and whose Unknown names the fields: a reader pinned + // before the verdict waits holds it as a refused one, and VerdictOf gives a reader that knows it the verdict waits. + Unknown []string `json:"unknown,omitempty"` // Warnings are the problems of an accepted manifest a stricter judge outgrew (novox/hq ADR 0579 §1): Verdict is then // empty (accepted), and the watchman raises delivery.ManifestOutgrown until the manifest is mended. Warnings []string `json:"warnings,omitempty"` @@ -444,6 +448,9 @@ func (m Module) VerdictOf(module string) *ManifestVerdict { if m.Verdict == "" { return &ManifestVerdict{Module: module, State: VerdictAccepted, Warnings: m.Warnings} } + if m.Verdict == VerdictRefused && len(m.Unknown) > 0 { + return &ManifestVerdict{Module: module, State: VerdictManifestWaits, Unknown: m.Unknown} + } return &ManifestVerdict{Module: module, State: m.Verdict, Problems: m.Problems} } @@ -538,6 +545,8 @@ func (m Module) check() error { switch { case m.Verdict != "" && m.Verdict != VerdictAccepted && m.Verdict != VerdictRefused: return fmt.Errorf("delivery: the module's verdict %q is neither %s nor %s", m.Verdict, VerdictAccepted, VerdictRefused) + case len(m.Unknown) > 0 && m.Verdict != VerdictRefused: + return fmt.Errorf("delivery: a module's unknown fields are named only with the refusal a wait is written as") case m.Verdict != "" && m.Manifest == "": return fmt.Errorf("delivery: a module carries a verdict and no manifest digest") case (m.Verdict == VerdictRefused) != (len(m.Problems) > 0): diff --git a/go/delivery/verdicts.go b/go/delivery/verdicts.go index a747897..18e9c3d 100644 --- a/go/delivery/verdicts.go +++ b/go/delivery/verdicts.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "slices" + "strings" "time" "git.novox.be/novox/mesh-sdk/go/catalogue" @@ -26,6 +27,14 @@ const ( // VerdictRefused is the state of a manifest the judge found a problem in; Problems says each. VerdictRefused = "refused" + // VerdictManifestWaits is the state of a MANIFEST's verdict (not a proposal's: VerdictWaits, in placement_proposal.go, + // has the same word for a different thing, which is why this one carries Manifest in its name). It is the state of a + // manifest that carries a field the indexer's judge does not know (novox/hq ADR 0578 §1, amended): the manifest is not + // refused, because a newer judge may know the field; Unknown names each field. A reader holds a module whose manifest + // waits as it holds a refused one (ManifestVerdict.Held) and keeps what runs. In a modules value it is written as a + // refusal naming the fields (Module.Unknown), for readers that do not know this state. + VerdictManifestWaits = "waits" + // ManifestRefusedEvent is the indexer's event saying a manifest was refused (ManifestRefused), so the watchman // raises it as a condition beside the indexer's finding. ManifestRefusedEvent = "manifest-refused" @@ -39,6 +48,10 @@ const ( // (novox/hq ADR 0579 §1): it stays accepted, its problems are the verdict's Warnings, and the watchman raises it as a // warning until the manifest is mended. ManifestOutgrown = "manifest-outgrown" + + // ManifestWaits is the finding of a manifest whose verdict waits (VerdictManifestWaits): the watchman raises it as a warning + // until a judge knows the fields or the manifest is mended. + ManifestWaits = "manifest-waits" ) // ManifestVerdict is the value of a verdicts key: the module whose manifest it is, accepted or refused, every problem the judge @@ -48,6 +61,8 @@ type ManifestVerdict struct { Module string `json:"module"` State string `json:"state"` Problems []string `json:"problems,omitempty"` + // Unknown are the fields of a manifest the judge does not know, named when the state is VerdictManifestWaits and nowhere else. + Unknown []string `json:"unknown,omitempty"` // Warnings are the problems a stricter judge found in a manifest it accepted before, digest unchanged (novox/hq ADR // 0579 §1): the verdict stays accepted. Warnings []string `json:"warnings,omitempty"` @@ -58,16 +73,36 @@ type ManifestVerdict struct { // Refused is whether v refuses its manifest. func (v ManifestVerdict) Refused() bool { return v.State == VerdictRefused } -// Same is whether v and o say the same of a manifest: the module, the state and the problems, whoever judged and when. +// Waits is whether v says the manifest waits for a judge that knows its fields. +func (v ManifestVerdict) Waits() bool { return v.State == VerdictManifestWaits } + +// Held is whether a reader holds the module as refused: the manifest is refused or waits. +func (v ManifestVerdict) Held() bool { return v.Refused() || v.Waits() } + +// AsRefused is v as a reader pinned before the verdict waits takes it: a waiting verdict is a refusal naming what it waits +// on, any other verdict is v. For a part that hands a verdict to a program it does not deliver with itself (the checker's +// sandbox composer). +func (v ManifestVerdict) AsRefused() ManifestVerdict { + if !v.Waits() { + return v + } + v.State, v.Problems, v.Unknown = VerdictRefused, []string{v.WaitsOn()}, nil + return v +} + +// Same is whether v and o say the same of a manifest: the module, the state, the problems and the unknown fields, whoever judged and when. // The indexer writes a verdict only when it is not the same as the one it holds. func (v ManifestVerdict) Same(o ManifestVerdict) bool { - return v.Module == o.Module && v.State == o.State && slices.Equal(v.Problems, o.Problems) && slices.Equal(v.Warnings, o.Warnings) + return v.Module == o.Module && v.State == o.State && slices.Equal(v.Problems, o.Problems) && slices.Equal(v.Unknown, o.Unknown) && slices.Equal(v.Warnings, o.Warnings) } func (v ManifestVerdict) check() error { if err := checkName("module", v.Module); err != nil { return err } + if v.State != VerdictManifestWaits && len(v.Unknown) > 0 { + return fmt.Errorf("delivery: the verdict on %s names unknown fields and does not wait", v.Module) + } switch v.State { case VerdictAccepted: if len(v.Problems) > 0 { @@ -80,8 +115,12 @@ func (v ManifestVerdict) check() error { if len(v.Warnings) > 0 { return fmt.Errorf("delivery: the verdict on %s refuses its manifest and warns: a refusal names problems", v.Module) } + case VerdictManifestWaits: + if len(v.Unknown) == 0 || len(v.Problems) > 0 || len(v.Warnings) > 0 { + return fmt.Errorf("delivery: the verdict on %s waits and names no unknown field, or names problems or warnings: a wait names the fields", v.Module) + } default: - return fmt.Errorf("delivery: the verdict on %s is %q, neither %s nor %s", v.Module, v.State, VerdictAccepted, VerdictRefused) + return fmt.Errorf("delivery: the verdict on %s is %q, none of %s, %s and %s", v.Module, v.State, VerdictAccepted, VerdictRefused, VerdictManifestWaits) } if v.Judge == "" { return fmt.Errorf("delivery: the verdict on %s names no judge", v.Module) @@ -120,26 +159,37 @@ func DecodeManifestVerdict(data []byte) (ManifestVerdict, error) { return v, nil } -// TakeManifestVerdict is how a reader takes the verdict on a manifest (ADR 0578 §3): refused, with its problems, only when the -// indexer wrote a refusal. A missing verdict (nil) is taken as accepted, because every manifest the indexer holds before +// TakeManifestVerdict is how a reader takes the verdict on a manifest (ADR 0578 §3): held, with its problems, when the +// indexer wrote a refusal, or, for a manifest that waits, with the words that say which fields it waits on. A missing verdict (nil) is taken as accepted, because every manifest the indexer holds before // it has judged it was taken in under the judges that ran until then; that lasts until the indexer's first pass after its // delivery has written every verdict. A reader holds a refused module and names it with Why, and reads the rest. func TakeManifestVerdict(v *ManifestVerdict) (refused bool, problems []string) { - if v == nil || !v.Refused() { + if v == nil || !v.Held() { return false, nil } + if v.Waits() { + return true, []string{v.WaitsOn()} + } return true, v.Problems } +// WaitsOn says, in words, what a waiting manifest waits on. +func (v ManifestVerdict) WaitsOn() string { + return "it carries " + strings.Join(v.Unknown, ", ") + ", which the indexer's judge does not know yet" +} + // Why is a refusal in a reader's words: the module, that the indexer's judge refused its manifest, and each problem. func (v ManifestVerdict) Why() string { - if !v.Refused() { + if !v.Held() { return "" } judge := "the indexer's judge" if v.Judge != "" { judge += " (" + v.Judge + ")" } + if v.Waits() { + return fmt.Sprintf("%s holds the manifest of %s: %s", judge, v.Module, v.WaitsOn()) + } why := fmt.Sprintf("%s refused the manifest of %s:", judge, v.Module) for _, p := range v.Problems { why += " " + p + ";" diff --git a/go/delivery/verdicts_test.go b/go/delivery/verdicts_test.go index ca6cf2f..0e70049 100644 --- a/go/delivery/verdicts_test.go +++ b/go/delivery/verdicts_test.go @@ -33,6 +33,9 @@ func TestAManifestVerdictIsTheContractReadersTake(t *testing.T) { "refused, no problem": {Module: "m", State: VerdictRefused, Judge: "j"}, "accepted, a problem": {Module: "m", State: VerdictAccepted, Problems: []string{"x"}, Judge: "j"}, "refused, a warning": {Module: "m", State: VerdictRefused, Problems: []string{"x"}, Warnings: []string{"y"}, Judge: "j"}, + "waits, no field": {Module: "m", State: VerdictManifestWaits, Judge: "j"}, + "waits, a problem": {Module: "m", State: VerdictManifestWaits, Unknown: []string{"f"}, Problems: []string{"x"}, Judge: "j"}, + "fields, accepted": {Module: "m", State: VerdictAccepted, Unknown: []string{"f"}, Judge: "j"}, } { if _, err := EncodeManifestVerdict(bad); err == nil { t.Errorf("%s: encoded", name) @@ -100,4 +103,38 @@ func TestAManifestVerdictIsTheContractReadersTake(t *testing.T) { if id := heard.EventID(); id != "manifest-accepted-"+digest+"-src2:j" { t.Fatalf("the accepted event's id is %q", id) } + // A manifest with a field the judge does not know waits (clause 3 of the record on a release's Approve): readers hold it + // as a refused one, it round-trips through a modules value, an old value decodes unchanged, and it is not a refusal. + waits := ManifestVerdict{Module: "nats", State: VerdictManifestWaits, Unknown: []string{"core-credentials", "tool-runner"}, Judge: "j"} + data, err = EncodeManifestVerdict(waits) + if err != nil { + t.Fatal(err) + } + if back, err = DecodeManifestVerdict(data); err != nil || !back.Same(waits) || back.Same(ManifestVerdict{Module: "nats", State: VerdictManifestWaits, Unknown: []string{"x"}, Judge: "j"}) { + t.Fatalf("a waiting verdict: %+v %v", back, err) + } + if r, p := TakeManifestVerdict(&waits); !r || waits.Refused() || !waits.Held() || !strings.Contains(p[0], "core-credentials, tool-runner") || + !strings.Contains(waits.Why(), "holds the manifest of nats") { + t.Fatalf("a waiting verdict is held like a refusal: %v %v %q", r, p, waits.Why()) + } + if r := waits.AsRefused(); !r.Refused() || r.Waits() || len(r.Problems) != 1 || len(r.Unknown) != 0 || refused.AsRefused().State != VerdictRefused { + t.Fatalf("a waiting verdict as a reader before it takes it: %+v", r) + } + // In a modules value a wait is written as a refusal naming the fields (an older reader holds it as refused). + w := Module{FP2: "src2:" + strings.Repeat("b", 64), Manifest: strings.Repeat("c", 64), Repository: "novox/x", Commit: "c", + Verdict: VerdictRefused, Problems: []string{waits.WaitsOn()}, Unknown: []string{"f"}} + data, err = EncodeModule(w) + if err != nil { + t.Fatal(err) + } + if got, err = DecodeModule(data); err != nil || got.VerdictOf("x").Unknown[0] != "f" || !got.VerdictOf("x").Waits() || got.VerdictOf("x").Refused() { + t.Fatalf("the module's waiting verdict: %+v %v", got, err) + } + if r, p := TakeManifestVerdict(got.VerdictOf("x")); !r || !strings.Contains(p[0], "which the indexer's judge does not know yet") { + t.Fatalf("a waiting module is held: %v %v", r, p) + } + w.Verdict, w.Problems = "", nil + if _, err := EncodeModule(w); err == nil { + t.Fatal("unknown fields with no wait were encoded") + } } From b6cc9572f21c11538199405590f0819b877fabf4 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 11:36:05 +0100 Subject: [PATCH 02/15] A manifest may say its own secrets are the core's credentials (core-credentials), so a reader asks the module rather than a list here (issue 774, the operator 2026-11-03) --- go/catalogue/invokes_test.go | 13 +++++++++++++ go/catalogue/manifest.go | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/go/catalogue/invokes_test.go b/go/catalogue/invokes_test.go index c43bf47..3fcb3bd 100644 --- a/go/catalogue/invokes_test.go +++ b/go/catalogue/invokes_test.go @@ -1,6 +1,8 @@ package catalogue import ( + "encoding/json" + "strings" "testing" ) @@ -15,4 +17,15 @@ func TestAManifestMaySayWhatItInvokes(t *testing.T) { if len(m.Invokes) != 2 || m.Invokes[1] != "*" { t.Fatalf("invokes not read: %v", m.Invokes) } + // And that its own secrets are the core's credentials (issue 774): read, and written back as said. + for raw, core := range map[string]bool{`{"module":"bus","version":"1","core-credentials":true}`: true, `{"module":"app","version":"1"}`: false} { + m, err := decodeForTest([]byte(raw)) + if err != nil || m.CoreCredentials != core || m.UnknownField() != "" { + t.Errorf("%s: core credentials %v, %v, %q", raw, m.CoreCredentials, err, m.UnknownField()) + } + back, _ := json.Marshal(m) + if strings.Contains(string(back), "core-credentials") != core { + t.Errorf("%s written back as %s", raw, back) + } + } } diff --git a/go/catalogue/manifest.go b/go/catalogue/manifest.go index 897d053..adaea5a 100644 --- a/go/catalogue/manifest.go +++ b/go/catalogue/manifest.go @@ -801,6 +801,12 @@ type Manifest struct { // start, and is then replaced. OwnSecrets OwnSecrets `json:"own-secrets,omitempty"` + // CoreCredentials says this module's own secrets are the core's credentials (novox/hq ADR 0349 §16, issue 755): the + // vault seals each to the machine, and the node-engine writes the vault's box into its file, never a value of its + // own (Rendering.VaultSealed). Said by the module, never by a list in mesh-sdk (the operator, 2026-11-03: mesh-sdk names + // no module): the bus and the provisioner and its client say it. + CoreCredentials bool `json:"core-credentials,omitempty"` + // SecretsOwner is who the files holding this module's secrets belong to on the machine — // `uid:gid`, or a name — when its process is not root. // From f6e214533be3d57e7518f3d27605c501924b3ad9 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 11:36:43 +0100 Subject: [PATCH 03/15] mesh-sdk names no module: the core's credentials are what a manifest says (core-credentials), and the bus's upgrade policy is the one its manifest declares (issue 774, the operator 2026-11-03) --- go/catalogue/amqp_is_not_a_provision_test.go | 21 ++++++++++++++++++++ go/catalogue/core_credentials.go | 9 ++++----- go/catalogue/declaration.go | 2 +- go/catalogue/invokes_test.go | 20 +++++++++++++++++++ go/catalogue/manifest.go | 2 +- go/catalogue/upgrade.go | 15 ++++---------- go/delivery/composed.go | 5 +++-- 7 files changed, 54 insertions(+), 20 deletions(-) diff --git a/go/catalogue/amqp_is_not_a_provision_test.go b/go/catalogue/amqp_is_not_a_provision_test.go index adbeb2d..eb0744c 100644 --- a/go/catalogue/amqp_is_not_a_provision_test.go +++ b/go/catalogue/amqp_is_not_a_provision_test.go @@ -3,6 +3,7 @@ package catalogue import ( "os" "path/filepath" + "regexp" "strings" "testing" @@ -31,4 +32,24 @@ func TestNoCatalogueManifestNamesAmqp(t *testing.T) { filepath.Base(filepath.Dir(path))) } } + // Nor does this package name the modules that once had a rule of their own here (issue 774, the operator 2026-11-03): + // what each is, its manifest says. mesh-provisioner is also a seat's name, named where the seat is. + t.Run("the package names no module", func(t *testing.T) { + named := regexp.MustCompile(`"(nats|mesh-provisioner-client|mesh-provisioner)"`) + files, _ := filepath.Glob("*.go") + for _, f := range files { + if strings.HasSuffix(f, "_test.go") { + continue + } + raw, err := os.ReadFile(f) + if err != nil { + t.Fatal(err) + } + for i, line := range strings.Split(string(raw), "\n") { + if named.MatchString(line) && !strings.Contains(line, "{Name: "+named.FindString(line)+", Scope:") { + t.Errorf("%s:%d names a module: %s", f, i+1, strings.TrimSpace(line)) + } + } + } + }) } diff --git a/go/catalogue/core_credentials.go b/go/catalogue/core_credentials.go index bf8a374..e28cc0c 100644 --- a/go/catalogue/core_credentials.go +++ b/go/catalogue/core_credentials.go @@ -4,11 +4,10 @@ import ( "encoding/json" ) -// CoreCredentialModules are the modules whose own secrets are the core's credentials: the bus, mesh-provisioner and -// its client. The node-engine and the controller hold no own secret of a module's. **Fixed here, in the controller's -// code, as the clause says** — and the vault keeps the same list (mesh-vault's CoreModules), answering the controller -// for these alone. -var CoreCredentialModules = []string{"mesh-provisioner", "mesh-provisioner-client", "nats"} +// CarriesCoreCredentials says whether a module's own secrets are the core's credentials, as its manifest says +// (Manifest.CoreCredentials): the vault seals them to the machine, and the node-engine and the vault hold no list of +// such modules. +func CarriesCoreCredentials(m Manifest) bool { return m.CoreCredentials } // VaultSealedField is the field of a file the vault sealed to its machine (mesh-host's File.VaultSealed). const VaultSealedField = "vault-sealed" diff --git a/go/catalogue/declaration.go b/go/catalogue/declaration.go index 49e7254..0da1418 100644 --- a/go/catalogue/declaration.go +++ b/go/catalogue/declaration.go @@ -84,7 +84,7 @@ type Rendering struct { // makes. Nil on every composition that is sent, and on every other question, which then refuse // a secret not made with a *NotMadeError. Foreseen map[string]map[string]bool - // VaultSealed is, for a module on the core's list (CoreCredentialModules), each own secret as the vault sealed it + // VaultSealed is, for a module whose manifest says its own secrets are the core's credentials (core-credentials), each own secret as the vault sealed it // to this machine (the mesh's sealed box, as the vault answered it), keyed like Needed (novox/hq ADR 0349 §16, // issue 755). Given only for a machine whose engine opens the box (VaultBoxContract): its file then carries the // box and never the controller's own sealed value. Nil everywhere else, and the composition is as it was. diff --git a/go/catalogue/invokes_test.go b/go/catalogue/invokes_test.go index 3fcb3bd..32e79d4 100644 --- a/go/catalogue/invokes_test.go +++ b/go/catalogue/invokes_test.go @@ -27,5 +27,25 @@ func TestAManifestMaySayWhatItInvokes(t *testing.T) { if strings.Contains(string(back), "core-credentials") != core { t.Errorf("%s written back as %s", raw, back) } + if CarriesCoreCredentials(m) != core { + t.Errorf("%s: carries the core's credentials %v", raw, !core) + } + } + // And its upgrade policy, which holds for the bus as for any module: providing the bus derives nothing (issue 774). + for raw, want := range map[string]string{ + `{"module":"bus","version":"1","provides":[{"name":"mesh-bus"}],"upgrade":{"policy":"record","why":"its streams"}}`: PolicyRecord + " " + FromModule, + `{"module":"bus","version":"1","provides":[{"name":"mesh-bus"}]}`: PolicyRoll + " " + FromDefault, + } { + m, err := decodeForTest([]byte(raw)) + if err != nil { + t.Fatal(err) + } + if policy, from, _ := DerivedUpgrade(m); policy+" "+from != want { + t.Errorf("%s: derived %s %s, want %s", raw, policy, from, want) + } + chosen := &PersonPolicy{Policy: PolicyTogether, Why: "w", By: "a person"} + if policy, from, _, _ := Upgrade(m, chosen); policy != PolicyTogether || from != FromPerson { + t.Errorf("%s: a person's choice gave %s %s", raw, policy, from) + } } } diff --git a/go/catalogue/manifest.go b/go/catalogue/manifest.go index adaea5a..e552842 100644 --- a/go/catalogue/manifest.go +++ b/go/catalogue/manifest.go @@ -640,7 +640,7 @@ type Manifest struct { // Upgrade is how this module's new builds reach its machines (novox/hq ADR 0236): rolled out one // machine first and gated when unsaid; `together`, or `record` — wait for a person's push — with - // why. A person's choice through the `upgrade` verb stands over it; the bus records whatever it says. + // why. A person's choice through the `upgrade` verb stands over it. The bus says `record` itself. Upgrade *UpgradePolicy `json:"upgrade,omitempty"` // Unhealthy is how loud this module's unhealthy condition is (novox/hq ADR 0355): a warning when unsaid; diff --git a/go/catalogue/upgrade.go b/go/catalogue/upgrade.go index a89e994..07feb24 100644 --- a/go/catalogue/upgrade.go +++ b/go/catalogue/upgrade.go @@ -30,25 +30,19 @@ type UpgradePolicy struct { const ( FromPerson = "person" FromModule = "module" - FromBus = "the bus" FromData = "irreplaceable data" FromDefault = "default" ) // DerivedUpgrade is the policy a module's manifest gives it when no person has chosen one, with where -// it came from and why (ADR 0236): +// it came from and why (ADR 0236), from what the manifest says alone (the operator, 2026-11-03: no module is +// treated apart here; the bus says `record` in its own manifest): // -// - **the bus is never rolled**: a module that provides the mesh's bus records, whatever it says — its -// upgrade is a planned step a person starts (to-be 45 §8); // - a module that says its policy has it; // - a module that keeps irreplaceable data records — sending the previous build cannot undo what a new // one did to data that cannot be had again, so a person takes it, after a backup; // - everything else rolls out, one machine first. func DerivedUpgrade(m Manifest) (policy, from, why string) { - if ProvidesBus(m) { - return PolicyRecord, FromBus, "the bus is replaced only as a planned step a person starts (`bus upgrade`): " + - "its streams are snapshotted first and checked after" - } if m.Upgrade != nil && m.Upgrade.Policy != "" { return m.Upgrade.Policy, FromModule, m.Upgrade.Why } @@ -126,11 +120,10 @@ func (p PersonPolicy) Check() error { // Upgrade is a module's policy and where it comes from (ADR 0565 clause 7): the one derivation every reader takes, // `mesh-wants.upgrade` and each node agent's judgement of a move alike. A person's choice over the module's word, its -// irreplaceable data and the default; the bus is never rolled, whoever says so (ADR 0236). By names the person when one -// chose. +// irreplaceable data and the default. By names the person when one chose. func Upgrade(m Manifest, chosen *PersonPolicy) (policy, from, why, by string) { policy, from, why = DerivedUpgrade(m) - if from == FromBus || chosen == nil { + if chosen == nil { return policy, from, why, "" } return chosen.Policy, FromPerson, chosen.Why, chosen.By diff --git a/go/delivery/composed.go b/go/delivery/composed.go index 5fdb5f5..20c04ac 100644 --- a/go/delivery/composed.go +++ b/go/delivery/composed.go @@ -164,8 +164,9 @@ type ComposedDigest struct { } // The upgrade policies and their sources a composition's value names: catalogue.PolicyRoll, PolicyTogether and -// PolicyRecord, and catalogue.FromPerson, FromModule, FromBus, FromData and FromDefault, the same words (the compose -// package's test holds them equal; this package does not import the catalogue). +// PolicyRecord, and catalogue.FromPerson, FromModule, FromData and FromDefault, the same words (the compose package's +// test holds them equal; this package does not import the catalogue). "the bus" is the word an older reader derived +// for the bus module, still read from a composition it wrote; nothing derives it now. var ( composedPolicies = []string{"roll", "together", "record"} composedPolicyFroms = []string{"person", "module", "the bus", "irreplaceable data", "default"} From 189f28e4e134fe56cc06482e28e25ab366cac524 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 11:52:30 +0100 Subject: [PATCH 04/15] mesh-sdk's catalogue package names no module: the tool runner says it is one (tool-runner), the foundation is what is computed, the kept streams are the judge's, and the ended controller's dependence check goes with it (issue 774, the operator 2026-11-03) --- go/buscompose/health_grants.go | 5 +- go/catalogue/amqp_is_not_a_provision_test.go | 46 +++++-- go/catalogue/controller_dependence.go | 137 ------------------- go/catalogue/controller_dependence_test.go | 58 -------- go/catalogue/invokes_test.go | 21 +++ go/catalogue/manifest.go | 5 + go/catalogue/runtime.go | 16 ++- go/catalogue/seat_dependencies.go | 22 +-- go/catalogue/streams.go | 16 --- 9 files changed, 85 insertions(+), 241 deletions(-) delete mode 100644 go/catalogue/controller_dependence.go delete mode 100644 go/catalogue/controller_dependence_test.go diff --git a/go/buscompose/health_grants.go b/go/buscompose/health_grants.go index a06f64d..cf66b51 100644 --- a/go/buscompose/health_grants.go +++ b/go/buscompose/health_grants.go @@ -30,6 +30,9 @@ func ControllerLeftoverConsumersOn(nodes []string) []string { return out } +// endedController is the name the ended controller had as a module: its buckets are named for it (ADR 0590 §6). +const endedController = "mesh-controller" + // ControllerLeftoverBuckets are the ended controller's buckets, each by its local name (`KV_mesh-controller_`). var ControllerLeftoverBuckets = []string{"asked", "assignments", "builds", "calls", "checks-running", "composed", "condition-history", "conditions", "declared-config", "declared-packages", "delivery-merges", "grants", "hand-acts", @@ -40,7 +43,7 @@ var ControllerLeftoverBuckets = []string{"asked", "assignments", "builds", "call func ControllerLeftoverStreams(nodes []string) []string { out := []string{"CONTROL", AssignmentsStream} for _, b := range ControllerLeftoverBuckets { - out = append(out, "KV_"+BucketName(catalogue.EndedControllerName, b)) + out = append(out, "KV_"+BucketName(endedController, b)) } for _, n := range nodes { if safeSubject.MatchString(n) { diff --git a/go/catalogue/amqp_is_not_a_provision_test.go b/go/catalogue/amqp_is_not_a_provision_test.go index eb0744c..01aba6e 100644 --- a/go/catalogue/amqp_is_not_a_provision_test.go +++ b/go/catalogue/amqp_is_not_a_provision_test.go @@ -1,9 +1,12 @@ package catalogue import ( + "go/ast" + "go/parser" + "go/token" "os" "path/filepath" - "regexp" + "strconv" "strings" "testing" @@ -19,6 +22,7 @@ import ( // design 28 task 5.4, not converted. func TestNoCatalogueManifestNamesAmqp(t *testing.T) { modules, err := filepath.Glob(filepath.Join(beside.Catalogue(t), "*", "module.json")) + modules0 := modules if err != nil || len(modules) == 0 { t.Fatalf("no manifests in the catalogue, so this proved nothing: %v", err) } @@ -32,24 +36,48 @@ func TestNoCatalogueManifestNamesAmqp(t *testing.T) { filepath.Base(filepath.Dir(path))) } } - // Nor does this package name the modules that once had a rule of their own here (issue 774, the operator 2026-11-03): - // what each is, its manifest says. mesh-provisioner is also a seat's name, named where the seat is. + // Nor does this package's code name any module (issue 774, the operator 2026-11-03): what a module is, its manifest + // says. Every string literal of its non-test files is read; a module's name is refused there unless it is also a + // seat's or a provision's name, or one of the words below, which name something else. t.Run("the package names no module", func(t *testing.T) { - named := regexp.MustCompile(`"(nats|mesh-provisioner-client|mesh-provisioner)"`) + modules := map[string]bool{} + for _, path := range modules0 { + modules[filepath.Base(filepath.Dir(path))] = true + } + // The mesh's modules held outside the catalogue: the node-engine, the private network, the tool runner, the + // ended controller and the delivery's parts. + for _, m := range []string{"mesh-host", "mesh-wireguard", "node-tools", "mesh-controller", "mesh-delivery-assigner", + "mesh-delivery-builder", "mesh-delivery-checker", "mesh-delivery-forge-bridge", "mesh-delivery-indexer", + "mesh-delivery-teller", "mesh-delivery-view", "mesh-delivery-watchman", "mesh-enrolment", "mesh-grants"} { + modules[m] = true + } + // Words that are a module's name and name something else here: a language, an environment's rendering, a package. + elsewhere := map[string]bool{"go": true, "systemd": true, "nftables": true} + fset := token.NewFileSet() files, _ := filepath.Glob("*.go") for _, f := range files { if strings.HasSuffix(f, "_test.go") { continue } - raw, err := os.ReadFile(f) + parsed, err := parser.ParseFile(fset, f, nil, 0) if err != nil { t.Fatal(err) } - for i, line := range strings.Split(string(raw), "\n") { - if named.MatchString(line) && !strings.Contains(line, "{Name: "+named.FindString(line)+", Scope:") { - t.Errorf("%s:%d names a module: %s", f, i+1, strings.TrimSpace(line)) + ast.Inspect(parsed, func(n ast.Node) bool { + lit, ok := n.(*ast.BasicLit) + if !ok || lit.Kind != token.STRING { + return true } - } + word, err := strconv.Unquote(lit.Value) + if err != nil || !modules[word] || elsewhere[word] { + return true + } + if _, seat := SeatNamed(word); seat { + return true + } + t.Errorf("%s names the module %s", fset.Position(lit.Pos()), word) + return true + }) } }) } diff --git a/go/catalogue/controller_dependence.go b/go/catalogue/controller_dependence.go deleted file mode 100644 index 8c598b0..0000000 --- a/go/catalogue/controller_dependence.go +++ /dev/null @@ -1,137 +0,0 @@ -package catalogue - -import ( - "fmt" - "slices" - "sort" - "strings" -) - -// EndedControllerName is the name the ended controller had, as its module and as its seat: no manifest may name it. -const EndedControllerName = "mesh-controller" - -// The kinds of a declaration that names the controller. -const ( - DependsRead = "read" - DependsInvoke = "invoke" - DependsConsume = "consume" - DependsUses = "uses" - DependsWaits = "waits" - DependsClaims = "claims" - DependsRequires = "requires" - DependsIs = "is" -) - -// Dependency is one declaration of a manifest that names the controller. -type Dependency struct { - Module string `json:"module"` - // By is the account declaring it when it is not the module's own: `process ` or `tools `. - By string `json:"by,omitempty"` - Kind string `json:"kind"` - What string `json:"what"` -} - -// onController says whether a name `.` is the controller's. -func onController(name string) bool { return strings.HasPrefix(name, EndedControllerName+".") } - -// invokesController says whether an invoke names a verb of the controller's seat or a tool of its module. `*` names none: -// it is the console's grant to every tool. -func invokesController(invoke string) bool { - if seat, _, ok := SeatInvoke(invoke); ok { - return seat == EndedControllerName - } - return onController(invoke) -} - -// Dependencies are every declaration naming the controller in the manifests given, by module. -func Dependencies(manifests map[string]Manifest) []Dependency { - var out []Dependency - add := func(module, by, kind, what string) { - if d := (Dependency{Module: module, By: by, Kind: kind, What: what}); !slices.Contains(out, d) { - out = append(out, d) - } - } - for name, m := range manifests { - if name == EndedControllerName || m.Module == EndedControllerName { - add(name, "", DependsIs, EndedControllerName) - } - for _, r := range m.Reads { - if onController(r) { - add(name, "", DependsRead, r) - } - } - for _, i := range m.Invokes { - if invokesController(i) { - add(name, "", DependsInvoke, i) - } - } - for _, c := range m.Consumes { - if onController(c) { - add(name, "", DependsConsume, c) - } - } - for _, w := range m.Waits { - if onController(w) { - add(name, "", DependsWaits, w) - } - } - for _, u := range m.Uses { - if u == EndedControllerName { - add(name, "", DependsUses, u) - } - } - for _, c := range m.Claims { - if c.Name == EndedControllerName { - add(name, "", DependsClaims, c.Name) - } - } - for _, r := range m.Requires { - if r == EndedControllerName || onController(r) { - add(name, "", DependsRequires, r) - } - } - for _, a := range m.BusAccounts() { - by := "process " + a.ID - if a.Tools { - by = "tools " + a.ID - } - for _, i := range a.Bus.Invokes { - if invokesController(i) { - add(name, by, DependsInvoke, i) - } - } - } - } - sort.Slice(out, func(i, j int) bool { - a, b := out[i], out[j] - if a.Module != b.Module { - return a.Module < b.Module - } - if a.Kind != b.Kind { - return a.Kind < b.Kind - } - if a.What != b.What { - return a.What < b.What - } - return a.By < b.By - }) - return out -} - -// ManifestRefusals is the check over a repository's manifests, by module name: one reason per declaration naming the -// controller, none when it passes. None given fails it, since nothing read can hide a declaration. -func ManifestRefusals(manifests map[string]Manifest) []string { - if len(manifests) == 0 { - return []string{"no manifest was read: with nothing read, nothing naming the controller can be judged absent"} - } - var out []string - for _, d := range Dependencies(manifests) { - who := d.Module - if d.By != "" { - who += " (" + d.By + ")" - } - out = append(out, fmt.Sprintf("%s declares %s %s: the controller ended by its unassignment and nothing may name it "+ - "(ADR 0590 clause 5)", who, d.Kind, d.What)) - } - return out -} diff --git a/go/catalogue/controller_dependence_test.go b/go/catalogue/controller_dependence_test.go deleted file mode 100644 index e6d2ce3..0000000 --- a/go/catalogue/controller_dependence_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package catalogue - -import ( - "slices" - "strings" - "testing" -) - -// Issue 1126 B1/AC1, B2/AC1 (ADR 0590 clause 5): the seat set holds no mesh-controller, and the dependence check refuses -// every manifest that reads (the lease too), invokes, consumes, waits on, uses, claims, requires or is mesh-controller. -func TestNothingMayNameTheController(t *testing.T) { - for _, s := range DefaultSeats() { - if s.Name == EndedControllerName { - t.Errorf("the seat set still holds %s", s.Name) - } - } - if _, ok := SeatNamed(EndedControllerName); ok { - t.Errorf("SeatNamed still finds %s", EndedControllerName) - } - for _, row := range []struct { - name, manifest string - want []string // " " - }{ - {"a read of the lease", `{"module":"mod-l","version":"1","reads":["mesh-controller.lease"]}`, []string{"read mesh-controller.lease"}}, - {"the controller's own manifest", `{"module":"mesh-controller","version":"1"}`, []string{"is mesh-controller"}}, - {"a claim of its seat", `{"module":"a-controller","version":"1","claims":[{"name":"mesh-controller","scope":"mesh"}]}`, - []string{"claims mesh-controller"}}, - {"each declaration kind; every tool (*) is none", - `{"module":"mod-i","version":"1","invokes":["seat:mesh-controller.root-free","mesh-controller.status","*","nats.nats_users"],` + - `"consumes":["mesh-controller.artifacts-gone","mod-x.thing"],"uses":["mesh-controller"],"waits":["mesh-controller.checked"],` + - `"requires":["mesh-controller"]}`, - []string{"consume mesh-controller.artifacts-gone", "invoke mesh-controller.status", "invoke seat:mesh-controller.root-free", - "requires mesh-controller", "uses mesh-controller", "waits mesh-controller.checked"}}, - {"a process's own invoke", `{"module":"mod-p","version":"1","resources":[{"id":"account","type":"user","name":"mod-p"},` + - `{"id":"worker","type":"process","run":["./w"],"user":"mod-p","bus":{"invokes":["seat:mesh-controller.held"]}}]}`, - []string{"invoke seat:mesh-controller.held"}}, - {"a module on its own seats", `{"module":"mod-ok","version":"1","reads":["mesh-grants.accounts"],"invokes":["*"]}`, nil}, - } { - m, err := Decode([]byte(row.manifest)) - if err != nil { - t.Fatalf("%s: %v", row.name, err) - } - var got []string - for _, d := range Dependencies(map[string]Manifest{m.Module: m}) { - got = append(got, d.Kind+" "+d.What) - } - if !slices.Equal(got, row.want) { - t.Errorf("%s: dependencies %q, want %q", row.name, got, row.want) - } - refused := ManifestRefusals(map[string]Manifest{m.Module: m}) - if len(refused) != len(row.want) || len(refused) > 0 && !strings.Contains(refused[0], "ADR 0590") { - t.Errorf("%s: refused %q", row.name, refused) - } - } - if got := ManifestRefusals(map[string]Manifest{}); len(got) != 1 || !strings.Contains(got[0], "no manifest") { - t.Errorf("no manifest given: %q", got) - } -} diff --git a/go/catalogue/invokes_test.go b/go/catalogue/invokes_test.go index 32e79d4..e7725b7 100644 --- a/go/catalogue/invokes_test.go +++ b/go/catalogue/invokes_test.go @@ -31,6 +31,27 @@ func TestAManifestMaySayWhatItInvokes(t *testing.T) { t.Errorf("%s: carries the core's credentials %v", raw, !core) } } + // And that it is the tool runner (issue 774): read, written back, and found among others by that alone. + runner, err := decodeForTest([]byte(`{"module":"any-name","version":"1","tool-runner":true}`)) + if err != nil || !IsToolRunner(runner) || runner.UnknownField() != "" { + t.Fatalf("a tool runner: %v %q", err, runner.UnknownField()) + } + if back, _ := json.Marshal(runner); !strings.Contains(string(back), `"tool-runner":true`) { + t.Errorf("a tool runner written back as %s", back) + } + other, _ := decodeForTest([]byte(`{"module":"node-tools","version":"1"}`)) + if got, ok := ToolRunnerOf([]Manifest{other, runner}); !ok || got.Module != "any-name" { + t.Errorf("the tool runner found is %q, %v: by what it says, never by its name", got.Module, ok) + } + // A module's seats are derived from what it declares, whatever its name: a service on a machine is the service + // manager's, the node-engine's name exempts nothing; a module whose resources are computed depends on none. + host, _ := decodeForTest([]byte(`{"module":"mesh-host","version":"1","resources":[{"id":"s","type":"service","name":"s.service"}]}`)) + if got := DependsOn(nil, host); len(got) != 1 || got[0] != ServiceManagerSeat { + t.Errorf("a service by the node-engine's name depends on %v", got) + } + if got := DependsOn(nil, Manifest{Module: "net", Computed: "net", Resources: host.Resources}); len(got) != 0 { + t.Errorf("a computed module depends on %v", got) + } // And its upgrade policy, which holds for the bus as for any module: providing the bus derives nothing (issue 774). for raw, want := range map[string]string{ `{"module":"bus","version":"1","provides":[{"name":"mesh-bus"}],"upgrade":{"policy":"record","why":"its streams"}}`: PolicyRecord + " " + FromModule, diff --git a/go/catalogue/manifest.go b/go/catalogue/manifest.go index e552842..485e190 100644 --- a/go/catalogue/manifest.go +++ b/go/catalogue/manifest.go @@ -801,6 +801,11 @@ type Manifest struct { // start, and is then replaced. OwnSecrets OwnSecrets `json:"own-secrets,omitempty"` + // ToolRunner says this module is the node's tool runner (issue 774): the one program on each node that loads every + // assigned module's tools bundle, served by its runtime process and the only module that may declare `waits`. Said by + // the module, never by a name in mesh-sdk. + ToolRunner bool `json:"tool-runner,omitempty"` + // CoreCredentials says this module's own secrets are the core's credentials (novox/hq ADR 0349 §16, issue 755): the // vault seals each to the machine, and the node-engine writes the vault's box into its file, never a value of its // own (Rendering.VaultSealed). Said by the module, never by a list in mesh-sdk (the operator, 2026-11-03: mesh-sdk names diff --git a/go/catalogue/runtime.go b/go/catalogue/runtime.go index 1d75f9a..65dd795 100644 --- a/go/catalogue/runtime.go +++ b/go/catalogue/runtime.go @@ -1,8 +1,18 @@ package catalogue -// RuntimeModule is the module that is the node's tool runtime. Mirrored in the broker package, -// which composes a principal of its own for it; the agreement test there holds the two to one string. -const RuntimeModule = "node-tools" +// IsToolRunner says whether a manifest is the node's tool runner's, as it says itself (`tool-runner`, issue 774): the +// one program on each node that loads every assigned module's tools bundle. Never known here by its name. +func IsToolRunner(m Manifest) bool { return m.ToolRunner } + +// ToolRunnerOf is the module among manifests that says it is the tool runner, and whether one does. +func ToolRunnerOf(manifests []Manifest) (Manifest, bool) { + for _, m := range manifests { + if IsToolRunner(m) { + return m, true + } + } + return Manifest{}, false +} // BundleRoot is where a machine keeps the tools bundles the mesh delivers to it: under the mesh's // own directory, beside the daemons the host unpacks there, and never where a package manager also diff --git a/go/catalogue/seat_dependencies.go b/go/catalogue/seat_dependencies.go index e136a71..3e3d53a 100644 --- a/go/catalogue/seat_dependencies.go +++ b/go/catalogue/seat_dependencies.go @@ -24,24 +24,12 @@ var appliedThrough = map[string]string{ "container": ContainerRuntimeSeat, } -// foundationModules are the pieces genesis lays before any module exists (novox/hq ADR 0207 §5): -// the host and the private network. Registered as modules so they can be assigned, but what they -// declare is the installation's, not a module's, so it is never judged. The third piece, the -// bootstrap container runtime, is not a module at all: its package and service are in the genesis -// bundle the host applies itself, and never pass through a resolution here. -// -// The private network's module is overlay.Name, written out because the overlay package composes -// on top of this one; its resources are computed, which exempts it by the rule below as well. -var foundationModules = map[string]bool{ - "mesh-host": true, - "mesh-wireguard": true, -} - -// isFoundation is whether a module's declarations are the foundation's rather than its own. A -// module whose resources are computed is the mesh's by construction — the private network's peer -// list and its tools are the controller's, written per node — so it counts whatever its name. +// isFoundation is whether a module's declarations are the foundation's rather than its own: a module whose resources are +// computed (`computed`, the private network's peer list written per node) is the mesh's by construction. Read from the +// manifest alone, never from its name (issue 774, the operator 2026-11-03): the node-engine's own manifest declares no +// resource a seat applies, so it depends on none by the rules below. func isFoundation(m Manifest) bool { - return foundationModules[m.Module] || m.Computed != "" + return m.Computed != "" } // DependsOn is every seat a module needs held on its node: derived from the resource types it diff --git a/go/catalogue/streams.go b/go/catalogue/streams.go index f47111f..d33a2df 100644 --- a/go/catalogue/streams.go +++ b/go/catalogue/streams.go @@ -370,19 +370,3 @@ func uncovered(patterns []string) []string { slices.Sort(out) return out } - -// **A module keeps its data in the bus's predefined forms** (the lead and the operator, 2026-10-29; the operator's rule of -// 2026-10-22, "data storage is done using our predefined nats bus options"): a state, a log or an object store, which the -// runtime reads and writes for its tools and other modules read by a declared `reads`. A stream of its own is outside -// them and refused, naming it. StreamsKeptUntilMoved are the streams declared before this check, each allowed by name -// until its module moves it; the list only shrinks. -var StreamsKeptUntilMoved = map[string][]string{ - "brother": {"device-asks"}, - "mesh-build-go": {"toolchains-go"}, - "mesh-build-node": {"toolchains-node"}, - "mesh-mail": {"mail", "mail-sends"}, - "only-office": {"convert-asks"}, - "mesh-delivery-assigner": {"placement"}, - "mesh-delivery-checker": {"checks"}, - "mesh-delivery-forge-bridge": {"forge"}, -} From 6655bdff509b4bbb70a3e2a1923444d7d1905a56 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 13:16:49 +0100 Subject: [PATCH 05/15] =?UTF-8?q?The=20check=20that=20the=20catalogue=20pa?= =?UTF-8?q?ckage=20names=20no=20module=20reads=20the=20names=20from=20the?= =?UTF-8?q?=20catalogue's=20module=20directories=20alone,=20no=20list=20be?= =?UTF-8?q?side=20them=20(ADR=200601=20=C2=A74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/catalogue/amqp_is_not_a_provision_test.go | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/go/catalogue/amqp_is_not_a_provision_test.go b/go/catalogue/amqp_is_not_a_provision_test.go index 01aba6e..0b0191c 100644 --- a/go/catalogue/amqp_is_not_a_provision_test.go +++ b/go/catalogue/amqp_is_not_a_provision_test.go @@ -36,21 +36,15 @@ func TestNoCatalogueManifestNamesAmqp(t *testing.T) { filepath.Base(filepath.Dir(path))) } } - // Nor does this package's code name any module (issue 774, the operator 2026-11-03): what a module is, its manifest - // says. Every string literal of its non-test files is read; a module's name is refused there unless it is also a - // seat's or a provision's name, or one of the words below, which name something else. + // Nor does this package's code name any module the catalogue holds (ADR 0601 §4): what a module is, its manifest + // says. The names are read from the catalogue's module directories, never from a list here; every string literal of + // the package's non-test files is read, and a module's name is refused unless it is also a seat's name or one of the + // words below, which name something else. t.Run("the package names no module", func(t *testing.T) { modules := map[string]bool{} for _, path := range modules0 { modules[filepath.Base(filepath.Dir(path))] = true } - // The mesh's modules held outside the catalogue: the node-engine, the private network, the tool runner, the - // ended controller and the delivery's parts. - for _, m := range []string{"mesh-host", "mesh-wireguard", "node-tools", "mesh-controller", "mesh-delivery-assigner", - "mesh-delivery-builder", "mesh-delivery-checker", "mesh-delivery-forge-bridge", "mesh-delivery-indexer", - "mesh-delivery-teller", "mesh-delivery-view", "mesh-delivery-watchman", "mesh-enrolment", "mesh-grants"} { - modules[m] = true - } // Words that are a module's name and name something else here: a language, an environment's rendering, a package. elsewhere := map[string]bool{"go": true, "systemd": true, "nftables": true} fset := token.NewFileSet() From 439b77e07daa4a53cfb7c1cf11570e827bce6c2e Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 13:17:33 +0100 Subject: [PATCH 06/15] =?UTF-8?q?A=20node=20the=20sandbox=20did=20not=20ju?= =?UTF-8?q?dge=20is=20a=20field=20of=20the=20contract:=20composer.Machine.?= =?UTF-8?q?NotJudged=20and=20the=20answer's=20verdict=20not-judged,=20so?= =?UTF-8?q?=20the=20checker=20decides=20by=20them=20and=20never=20by=20wor?= =?UTF-8?q?ds=20(ADR=200593=20=C2=A79;=20the=20prepare's=20seams=20finding?= =?UTF-8?q?=20on=20"not=20judged")?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/composer/composer.go | 16 ++++++++++------ go/delivery/compose_candidate.go | 16 +++++++++++----- go/delivery/compose_candidate_test.go | 17 +++++++++++++++++ 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/go/composer/composer.go b/go/composer/composer.go index 69b729e..dd224cf 100644 --- a/go/composer/composer.go +++ b/go/composer/composer.go @@ -135,13 +135,17 @@ type ManifestRead struct { } // Machine is one machine composed: whether it composes (resolves, at DepthResolve), why not, the node-engine -// validator's problems and the composition's refusals (DepthCompose), and its declared paths when asked. +// validator's problems and the composition's refusals (DepthCompose), and its declared paths when asked. NotJudged says, +// in words, why the composer did not judge the machine (an older node-engine's input that does not say how it composes, +// novox/hq ADR 0593 §9): the machine is then neither composed nor refused, and a reader decides by this field alone, +// never by the words of Reason. type Machine struct { - Composes bool `json:"composes"` - Reason string `json:"reason,omitempty"` - Problems []string `json:"problems,omitempty"` - Refusals []string `json:"refusals,omitempty"` - Declared []Path `json:"declared,omitempty"` + Composes bool `json:"composes"` + NotJudged string `json:"not_judged,omitempty"` + Reason string `json:"reason,omitempty"` + Problems []string `json:"problems,omitempty"` + Refusals []string `json:"refusals,omitempty"` + Declared []Path `json:"declared,omitempty"` } // Path is one path a declaration puts on the machine, with its kind (the resource's type), owner, group and mode as diff --git a/go/delivery/compose_candidate.go b/go/delivery/compose_candidate.go index 25d2a17..84cdefd 100644 --- a/go/delivery/compose_candidate.go +++ b/go/delivery/compose_candidate.go @@ -31,6 +31,12 @@ const ( // starting): no module is named, Refused says why in words, and the checker takes it as a node that did not answer // (ComposeNoAnswer, ADR 0593 §9), deciding by this verdict and never by the words. ComposeCandidateUnready = "unready" + // ComposeCandidateNotJudged is the verdict for a node the checker's sandbox composed and could not judge (an older + // node-engine's input that does not say how it composes, novox/hq ADR 0593 §9): no module is named, Refused says why in + // words, and the checker names the node with them and passes the check with that warning, never silent and never a + // failure of the change. The checker decides by this verdict, never by the words (ADR 0601 §4's kind: a contract is a + // field, not a phrase). + ComposeCandidateNotJudged = "not-judged" // The states of one module's composition with the candidate: composed (Digest set), refused by the candidate // (Words set), or held, refused without the candidate too (Words set), which counts toward no verdict. @@ -115,13 +121,13 @@ func (a ComposeCandidateAnswer) check() error { if a.Took < 0 { return fmt.Errorf("delivery: the answer of %s took %d ms", a.Node, a.Took) } - if a.Verdict == ComposeCandidateUnready && a.Refused == "" { - return fmt.Errorf("delivery: %s is not ready to compose and its answer says no why", a.Node) + if (a.Verdict == ComposeCandidateUnready || a.Verdict == ComposeCandidateNotJudged) && a.Refused == "" { + return fmt.Errorf("delivery: %s's answer is %s and says no why", a.Node, a.Verdict) } if a.Refused != "" { - if len(a.Modules) > 0 || a.Verdict != ComposeCandidateRefused && a.Verdict != ComposeCandidateUnready { - return fmt.Errorf("delivery: %s could not compose the candidate (%s), and its answer names modules or is neither %s nor %s", - a.Node, a.Refused, ComposeCandidateRefused, ComposeCandidateUnready) + if len(a.Modules) > 0 || a.Verdict != ComposeCandidateRefused && a.Verdict != ComposeCandidateUnready && a.Verdict != ComposeCandidateNotJudged { + return fmt.Errorf("delivery: %s could not compose the candidate (%s), and its answer names modules or is neither %s, %s nor %s", + a.Node, a.Refused, ComposeCandidateRefused, ComposeCandidateUnready, ComposeCandidateNotJudged) } return nil } diff --git a/go/delivery/compose_candidate_test.go b/go/delivery/compose_candidate_test.go index 7c66dce..63e4e1d 100644 --- a/go/delivery/compose_candidate_test.go +++ b/go/delivery/compose_candidate_test.go @@ -78,6 +78,23 @@ func TestComposeCandidateAndComposedPlanRoundTripAndRefuse(t *testing.T) { if _, err := EncodeComposeCandidateAnswer(unready); err == nil { t.Error("an unready answer naming modules was encoded") } + // A node the sandbox did not judge: its own verdict, with words and no module (ADR 0593 §9, ADR 0601 §4). + notJudged := ComposeCandidateAnswer{Node: "ace", Candidate: key, Verdict: ComposeCandidateNotJudged, Refused: "its node-engine sends no running state"} + if raw, err := EncodeComposeCandidateAnswer(notJudged); err != nil { + t.Errorf("a not-judged answer was not encoded: %v", err) + } else if back, err := DecodeComposeCandidateAnswer(raw); err != nil || back.Verdict != ComposeCandidateNotJudged { + t.Errorf("a not-judged answer read back as %+v, %v", back, err) + } + for name, mutate := range map[string]func(*ComposeCandidateAnswer){ + "no why": func(a *ComposeCandidateAnswer) { a.Refused = "" }, + "modules too": func(a *ComposeCandidateAnswer) { a.Modules = good.Modules }, + } { + a := notJudged + mutate(&a) + if _, err := EncodeComposeCandidateAnswer(a); err == nil { + t.Errorf("a not-judged answer with %s was encoded", name) + } + } if _, err := DecodeComposeCandidateAnswer([]byte(strings.Replace(string(raw), `"took"`, `"other":1,"took"`, 1))); err == nil { t.Error("an answer with an unknown key was read") } From 636c2c1b0c7624204cfb9794ad6ec5050bb5f06c Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 13:18:08 +0100 Subject: [PATCH 07/15] =?UTF-8?q?A=20want=20names=20the=20release=20whose?= =?UTF-8?q?=20Approve=20answered=20its=20build's=20record=20ask,=20and=20a?= =?UTF-8?q?=20have=20waiting=20for=20a=20yes=20says=20whether=20the=20move?= =?UTF-8?q?=20changes=20the=20build=20alone=20(ADR=200601=20=C2=A71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/delivery/assigner.go | 6 +++++- go/delivery/delivery.go | 9 +++++++-- go/delivery/delivery_test.go | 15 ++++++++++++--- go/delivery/want_composed_test.go | 3 +++ 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/go/delivery/assigner.go b/go/delivery/assigner.go index 1c9cdf0..3fbe5be 100644 --- a/go/delivery/assigner.go +++ b/go/delivery/assigner.go @@ -193,6 +193,10 @@ type Want struct { // of policy record that the node agent judged to recreate a container or move its data is applied only when its // want names the composition so allowed. Empty when none was asked. Yes string `json:"yes,omitempty"` + // Release names the release whose Approve answered the record ask of this want's build (novox/hq ADR 0601 §1): the + // group of pull requests mesh-delivery's gate merged on that Approve. Set with the yes it gave; empty when a person's + // answer to a record ask gave it, or none was needed. + Release string `json:"release,omitempty"` // Self says the node composes this module itself (ADR 0547 §2): the want names the build alone, and no composition. // A want that is not Self names the one composition it allows (ADR 0539 clause 1), as before. Self bool `json:"self,omitempty"` @@ -200,7 +204,7 @@ type Want struct { func (w Want) check() error { if w.Removed { - if w.FP != "" || w.Composed != "" || w.Newest != "" || w.Rule != 0 || w.Backup != "" || w.Put || w.Hold || w.Self || w.Yes != "" { + if w.FP != "" || w.Composed != "" || w.Newest != "" || w.Rule != 0 || w.Backup != "" || w.Put || w.Hold || w.Self || w.Yes != "" || w.Release != "" { return fmt.Errorf("delivery: a removed want names no fingerprint, composition, rule, backup answer or put") } return nil diff --git a/go/delivery/delivery.go b/go/delivery/delivery.go index 3b72bb7..10d23a9 100644 --- a/go/delivery/delivery.go +++ b/go/delivery/delivery.go @@ -351,6 +351,11 @@ type Have struct { // composition that waits for a person's yes (ADR 0547 §3); empty otherwise. Judgement Judgement `json:"judgement,omitempty"` Waits string `json:"waits,omitempty"` + // BuildAlone says, in phase waiting-yes, that the move changes the module's build alone (novox/hq ADR 0601 §1): the + // node composed the build it runs again with today's settings and secrets and got the composition it runs, so the + // release that carries the new build answers the yes. False when a setting or a secret changed too, or the node could + // not tell: the move is then asked of a person as before. + BuildAlone bool `json:"build-alone,omitempty"` // Sealed is, for a module the node composes itself, each value its running composition carries sealed to the node's // key (a secret, a credential, a vault box), as applied (ADR 0547 §3, issue 1126): what the node's composer reuses // after a restart while it opens to the vault's value, so a value sealed again moves no composition. Openable by the @@ -476,8 +481,8 @@ func decode(data []byte, into any) error { func (h Have) check() error { switch h.Phase { case PhaseBackingUp, PhaseApplying, PhaseChecking, PhaseRunning, PhaseFailed, PhaseRemoved, PhaseHeld: - if h.Judgement != "" || h.Waits != "" { - return fmt.Errorf("delivery: only a have waiting for a yes names a judgement and the composition waiting") + if h.Judgement != "" || h.Waits != "" || h.BuildAlone { + return fmt.Errorf("delivery: only a have waiting for a yes names a judgement, the composition waiting and whether it changes the build alone") } case PhaseWaitingYes: if h.Judgement != JudgedRecreates && h.Judgement != JudgedMovesData && h.Judgement != JudgedCannot { diff --git a/go/delivery/delivery_test.go b/go/delivery/delivery_test.go index 328fad7..1d341a6 100644 --- a/go/delivery/delivery_test.go +++ b/go/delivery/delivery_test.go @@ -380,10 +380,19 @@ func testPhaseHealthRefused(t *testing.T) { } else if back, err := DecodeHave(raw); err != nil || back.Judgement != JudgedRecreates || back.Waits != waits.Waits { t.Errorf("a have waiting for a yes came back %+v, %v", back, err) } + // ADR 0601 §1: and whether the move changes the build alone, read back as said. + alone := waits + alone.BuildAlone = true + if raw, err := EncodeHave(alone); err != nil { + t.Errorf("a have waiting for a yes to its build alone: %v", err) + } else if back, err := DecodeHave(raw); err != nil || !back.BuildAlone { + t.Errorf("a have waiting for a yes to its build alone came back %+v, %v", back, err) + } for name, h := range map[string]Have{ - "neither judged": {Phase: PhaseWaitingYes, Health: HealthOK, Judgement: JudgedNeither, Waits: waits.Waits}, - "no waiting digest": {Phase: PhaseWaitingYes, Health: HealthOK, Judgement: JudgedMovesData}, - "running judged": {Phase: PhaseRunning, Health: HealthOK, Judgement: JudgedRecreates}, + "running build alone": {Phase: PhaseRunning, Health: HealthOK, BuildAlone: true}, + "neither judged": {Phase: PhaseWaitingYes, Health: HealthOK, Judgement: JudgedNeither, Waits: waits.Waits}, + "no waiting digest": {Phase: PhaseWaitingYes, Health: HealthOK, Judgement: JudgedMovesData}, + "running judged": {Phase: PhaseRunning, Health: HealthOK, Judgement: JudgedRecreates}, } { if _, err := EncodeHave(h); err == nil { t.Errorf("%s: encoded", name) diff --git a/go/delivery/want_composed_test.go b/go/delivery/want_composed_test.go index f206367..eec127f 100644 --- a/go/delivery/want_composed_test.go +++ b/go/delivery/want_composed_test.go @@ -24,6 +24,8 @@ func TestAWantNamesItsComposition(t *testing.T) { // ADR 0547 §2, §3 (issue 1126): from the second milestone a want names the build alone, and the yes a person gave. {FP: fp2of('a'), Self: true}, {FP: fp2of('a'), Self: true, Yes: hashOf('3')}, + // ADR 0601 §1: the yes the release's Approve gave names the release. + {FP: fp2of('a'), Self: true, Yes: hashOf('3'), Release: "feat/l3b-bus"}, } { raw, err := EncodeWant(w) if err != nil { @@ -43,6 +45,7 @@ func TestAWantNamesItsComposition(t *testing.T) { {Want{FP: fp2of('a'), Self: true, Composed: hashOf('1')}, "names a composition"}, {Want{FP: fp2of('a'), Self: true, Yes: "sha256:12"}, "a want's yes allows"}, {Want{Removed: true, Self: true}, "removed want names no"}, + {Want{Removed: true, Release: "feat/l3b-bus"}, "removed want names no"}, {Want{FP: fp2of('a'), Composed: "sha256:12"}, "not sha256"}, {Want{Removed: true, Composed: hashOf('1')}, "removed want names no"}, {Want{FP: fp2of('a'), Composed: hashOf('1'), Backup: "later"}, `backup answer "later"`}, From 493e69e883d70437f345db986d37f791a56a2b36 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 13:54:35 +0100 Subject: [PATCH 08/15] =?UTF-8?q?A=20module=20value=20carries=20the=20sour?= =?UTF-8?q?ce=20revision=20at=20which=20its=20fingerprint=20last=20changed?= =?UTF-8?q?,=20a=20have=20says=20what=20the=20node=20keeps=20as=20it=20run?= =?UTF-8?q?s=20for=20a=20manifest=20that=20does=20not=20yet=20say=20it,=20?= =?UTF-8?q?and=20a=20want=20names=20a=20release=20only=20beside=20the=20ye?= =?UTF-8?q?s=20it=20gave=20(ADR=200601=20=C2=A71,=20=C2=A74;=20review=20ro?= =?UTF-8?q?und=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/delivery/assigner.go | 2 ++ go/delivery/delivery.go | 4 ++++ go/delivery/delivery_test.go | 6 ++++++ go/delivery/source.go | 13 +++++++++---- go/delivery/source_test.go | 2 +- go/delivery/want_composed_test.go | 1 + 6 files changed, 23 insertions(+), 5 deletions(-) diff --git a/go/delivery/assigner.go b/go/delivery/assigner.go index 3fbe5be..3f439a2 100644 --- a/go/delivery/assigner.go +++ b/go/delivery/assigner.go @@ -234,6 +234,8 @@ func (w Want) check() error { if err := checkDigest(w.Yes, "the digest a want's yes allows"); err != nil { return err } + } else if w.Release != "" { + return fmt.Errorf("delivery: the want of %s names the release %s and no yes it gave", w.FP, w.Release) } switch w.Backup { case "", BackupAgain, BackupSkip: diff --git a/go/delivery/delivery.go b/go/delivery/delivery.go index 10d23a9..bd84f9c 100644 --- a/go/delivery/delivery.go +++ b/go/delivery/delivery.go @@ -356,6 +356,10 @@ type Have struct { // release that carries the new build answers the yes. False when a setting or a secret changed too, or the node could // not tell: the move is then asked of a person as before. BuildAlone bool `json:"build-alone,omitempty"` + // Kept says, in words, what the node keeps of the module as it runs because its manifest does not yet say what shared + // code no longer infers (that it is the tool runner, that its own secrets are the core's credentials; novox/hq ADR 0601 + // §4): the watchman raises a condition from it, cleared once the have says none. Empty in every other have. + Kept []string `json:"kept,omitempty"` // Sealed is, for a module the node composes itself, each value its running composition carries sealed to the node's // key (a secret, a credential, a vault box), as applied (ADR 0547 §3, issue 1126): what the node's composer reuses // after a restart while it opens to the vault's value, so a value sealed again moves no composition. Openable by the diff --git a/go/delivery/delivery_test.go b/go/delivery/delivery_test.go index 1d341a6..c54fdbd 100644 --- a/go/delivery/delivery_test.go +++ b/go/delivery/delivery_test.go @@ -388,6 +388,12 @@ func testPhaseHealthRefused(t *testing.T) { } else if back, err := DecodeHave(raw); err != nil || !back.BuildAlone { t.Errorf("a have waiting for a yes to its build alone came back %+v, %v", back, err) } + // ADR 0601 §4: what the node keeps as it runs, read back as said. + if raw, err := EncodeHave(Have{Phase: PhaseRunning, Health: HealthOK, Kept: []string{"runs the tool runner"}}); err != nil { + t.Errorf("a have keeping what runs: %v", err) + } else if back, err := DecodeHave(raw); err != nil || len(back.Kept) != 1 { + t.Errorf("a have keeping what runs came back %+v, %v", back, err) + } for name, h := range map[string]Have{ "running build alone": {Phase: PhaseRunning, Health: HealthOK, BuildAlone: true}, "neither judged": {Phase: PhaseWaitingYes, Health: HealthOK, Judgement: JudgedNeither, Waits: waits.Waits}, diff --git a/go/delivery/source.go b/go/delivery/source.go index c872d5a..efa2e7f 100644 --- a/go/delivery/source.go +++ b/go/delivery/source.go @@ -404,10 +404,15 @@ type Module struct { Manifest string `json:"manifest,omitempty"` // Inputs2 are the new-form inputs the merge gave the module, kept so the indexer computes FP2 again when a build // module's toolchain versions appear, with no merge (novox/hq ADR 0465 clause 2). It needs Manifest. - Inputs2 *KeptInputs2 `json:"inputs2,omitempty"` - Src uint64 `json:"src,omitempty"` - Repository string `json:"repository"` - Commit string `json:"commit"` + Inputs2 *KeptInputs2 `json:"inputs2,omitempty"` + Src uint64 `json:"src,omitempty"` + // FPSrc is the source revision at which FP2 last changed (novox/hq ADR 0601 §1): Src when a merge of the module's own + // repository moved it, the source revision of the moment when a recompute with no such merge moved it (a toolchain, + // a package, a base). The release that moved the source to it is the one that carries the build; 0 is not known, and + // no release is taken to carry it. Src and Commit stay where the module was last merged and say nothing of that. + FPSrc uint64 `json:"fp-src,omitempty"` + Repository string `json:"repository"` + Commit string `json:"commit"` // Path is the module's directory in its repository, trimmed of slashes, and RootPath (".") when its manifest is the // repository's root (novox/hq ADR 0478 clause 9, issue 1615): the build tools need it. Empty means not known: a value // written before the indexer learned paths has none. diff --git a/go/delivery/source_test.go b/go/delivery/source_test.go index 498a8a9..33d1c12 100644 --- a/go/delivery/source_test.go +++ b/go/delivery/source_test.go @@ -179,7 +179,7 @@ func TestSourceValuesRoundTrip(t *testing.T) { // 1200/AC3 t.Errorf("candidate round trip: %+v %v", got, err) } for _, m := range []Module{ - {FP: "src1:abc", Src: 5, Repository: "novox/mesh-sdk", Commit: "cccc"}, + {FP: "src1:abc", Src: 5, FPSrc: 5, Repository: "novox/mesh-sdk", Commit: "cccc"}, {None: "a build that resolves packages when built", Repository: "novox/mesh-sdk", Commit: "cccc"}, } { data, err = EncodeModule(m) diff --git a/go/delivery/want_composed_test.go b/go/delivery/want_composed_test.go index eec127f..77b9bf6 100644 --- a/go/delivery/want_composed_test.go +++ b/go/delivery/want_composed_test.go @@ -46,6 +46,7 @@ func TestAWantNamesItsComposition(t *testing.T) { {Want{FP: fp2of('a'), Self: true, Yes: "sha256:12"}, "a want's yes allows"}, {Want{Removed: true, Self: true}, "removed want names no"}, {Want{Removed: true, Release: "feat/l3b-bus"}, "removed want names no"}, + {Want{FP: fp2of('a'), Self: true, Release: "feat/l3b-bus"}, "and no yes it gave"}, {Want{FP: fp2of('a'), Composed: "sha256:12"}, "not sha256"}, {Want{Removed: true, Composed: hashOf('1')}, "removed want names no"}, {Want{FP: fp2of('a'), Composed: hashOf('1'), Backup: "later"}, `backup answer "later"`}, From 1b9475e15f21a5ce8c562471bc52468dc0eaad68 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 14:10:08 +0100 Subject: [PATCH 09/15] =?UTF-8?q?FPSrc=20is=200=20when=20a=20recompute=20w?= =?UTF-8?q?ith=20no=20merge=20moved=20the=20fingerprint:=20no=20release=20?= =?UTF-8?q?carries=20that=20build=20(ADR=200601=20=C2=A71;=20review=20roun?= =?UTF-8?q?d=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/delivery/source.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/go/delivery/source.go b/go/delivery/source.go index efa2e7f..b39596a 100644 --- a/go/delivery/source.go +++ b/go/delivery/source.go @@ -407,9 +407,9 @@ type Module struct { Inputs2 *KeptInputs2 `json:"inputs2,omitempty"` Src uint64 `json:"src,omitempty"` // FPSrc is the source revision at which FP2 last changed (novox/hq ADR 0601 §1): Src when a merge of the module's own - // repository moved it, the source revision of the moment when a recompute with no such merge moved it (a toolchain, - // a package, a base). The release that moved the source to it is the one that carries the build; 0 is not known, and - // no release is taken to carry it. Src and Commit stay where the module was last merged and say nothing of that. + // repository moved it, and 0 when a recompute with no merge moved it (a toolchain, a package, a base), since no release + // carries that build. The release that moved the source to it is the one that carries the build; 0 is not known, and no + // release is taken to carry it. Src and Commit stay where the module was last merged and say nothing of that. FPSrc uint64 `json:"fp-src,omitempty"` Repository string `json:"repository"` Commit string `json:"commit"` From 413dd13fe35d6ac97f00b76fe584ea1da6c37274 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 16:34:43 +0100 Subject: [PATCH 10/15] Accept a merged message with no trees, so a merge that leaves a repository with no module.json still reaches the indexer (issue 1126) --- go/delivery/source.go | 7 ++++--- go/delivery/source_test.go | 10 ++++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/go/delivery/source.go b/go/delivery/source.go index b39596a..99a4f68 100644 --- a/go/delivery/source.go +++ b/go/delivery/source.go @@ -688,9 +688,10 @@ func (f Forge) check() error { if f.Root == "" { return fmt.Errorf("delivery: a merged message needs the repository's root tree") } - if len(f.Trees) == 0 { - return fmt.Errorf("delivery: a merged message needs the tree of at least one directory holding a module.json") - } + // No trees is a merge that leaves the repository with no module.json anywhere (a documents repository, or one + // whose last module was removed): it is written all the same, so the indexer takes the merge in and drops the + // repository's modules (novox/hq issue 1126). An older writer always sent at least one tree, so none is unambiguous; + // such a message carries no manifests, locks or paths (checkInputs below: each names a directory of its trees). for dir, tree := range f.Trees { if tree == "" { return fmt.Errorf("delivery: a merged message's tree of the directory %q is empty", dir) diff --git a/go/delivery/source_test.go b/go/delivery/source_test.go index 33d1c12..1c90a60 100644 --- a/go/delivery/source_test.go +++ b/go/delivery/source_test.go @@ -272,9 +272,15 @@ func TestSourceValuesRefused(t *testing.T) { // 1200/AC4 _, err = EncodeForge(f) wantErr("merged without root", err, "root") f = sampleForges()[1] - f.Trees = nil + f.Trees, f.Manifests, f.Locks, f.Paths = nil, nil, nil, nil + if data, err := EncodeForge(f); err != nil { + t.Errorf("a merged message of a repository with no module.json is refused: %v", err) + } else if got, err := DecodeForge(data); err != nil || len(got.Trees) != 0 { + t.Errorf("a merged message with no trees comes back as %+v, %v", got, err) + } + f.Manifests = map[string]string{"modules/a": "{}"} _, err = EncodeForge(f) - wantErr("merged without trees", err, "tree") + wantErr("merged with a manifest and no trees", err, "modules/a") f = sampleForges()[1] f.Trees = map[string]string{"modules/a": ""} _, err = EncodeForge(f) From 9ab75bab490c1f9a2f57b7f5cf888e320c93e3fe Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 16:23:34 +0100 Subject: [PATCH 11/15] =?UTF-8?q?The=20seat=20mesh-catalog's=20contract:?= =?UTF-8?q?=20four=20verbs=20read=20from=20its=20holder's=20states,=20modu?= =?UTF-8?q?les,=20module,=20provides=20and=20dependents,=20each=20optional?= =?UTF-8?q?=20while=20the=20module=20that=20claimed=20it=20before=20still?= =?UTF-8?q?=20stands=20(ADR=200604=20=C2=A71,=20=C2=A76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/catalogue/catalog_seat.go | 30 ++++++++++++++++++++++++++++++ go/catalogue/seats.go | 3 ++- go/catalogue/verbs_test.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 go/catalogue/catalog_seat.go diff --git a/go/catalogue/catalog_seat.go b/go/catalogue/catalog_seat.go new file mode 100644 index 0000000..63b380b --- /dev/null +++ b/go/catalogue/catalog_seat.go @@ -0,0 +1,30 @@ +package catalogue + +// CatalogSeat is the seat of the catalog: what the mesh holds of every module (novox/hq ADR 0604 §1). Its states are +// its holder's: the modules the trunk holds, their manifests by digest and the module graph (the indexer's states +// `modules`, `manifests` and `graph`, delivery.ModulesBucket, ManifestsBucket and GraphBucket), announced with who may +// read them; a reader that only watches reads them and calls no verb. It names no event. What must be built again is +// no verb: a module whose fingerprint differs from its newest build, which the delivery view derives. +const CatalogSeat = "mesh-catalog" + +// catalogVerbs are the seat's four verbs, each read from its holder's states. Each is optional until the holder that +// runs serves it (design 33 §7): the module that claimed the seat before serves none of them, and a judge holding them +// required would read it as outgrown for as long as both claims stand (novox/hq ADR 0604 §6). +func catalogVerbs() []Verb { + return []Verb{ + {Name: "modules", Optional: true, 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.", + Input: schema(map[string]string{}, nil)}, + {Name: "module", Optional: true, 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.", + Input: schema(map[string]string{"module": "the module's name"}, []string{"module"})}, + {Name: "provides", Optional: true, 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.", + Input: schema(map[string]string{"provision": "the provision, such as postgres-database"}, []string{"provision"})}, + {Name: "dependents", Optional: true, 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.", + Input: schema(map[string]string{"module": "the module's name"}, []string{"module"})}, + } +} diff --git a/go/catalogue/seats.go b/go/catalogue/seats.go index b382b3e..15154bb 100644 --- a/go/catalogue/seats.go +++ b/go/catalogue/seats.go @@ -272,7 +272,8 @@ var defaultSeats = append([]Seat{ // images and archives, by digest — which is why the provision is the artifact store and not an // image registry. {Name: "mesh-artifact-store", Scope: ScopeMesh, Delivers: "artifact-store", Decision: "novox/hq ADR 0075"}, - {Name: "mesh-catalog", Scope: ScopeMesh, Decision: "novox/hq ADR 0121"}, + // The catalog (novox/hq ADR 0604): what the mesh holds of every module, its verbs read from its holder's states (CatalogSeat). + {Name: CatalogSeat, Scope: ScopeMesh, Serves: catalogVerbs(), Decision: "novox/hq ADR 0604"}, // The delivery's owner (novox/hq ADR 0239): one commit's journey from its pull request's head to every // machine, and a group of them, recorded and ordered by its holder, which asks the controller for each act. // What it says it says as its module (`mesh-delivery.transition`), through the runtime that launches it. diff --git a/go/catalogue/verbs_test.go b/go/catalogue/verbs_test.go index da85cec..af45955 100644 --- a/go/catalogue/verbs_test.go +++ b/go/catalogue/verbs_test.go @@ -50,6 +50,35 @@ func TestHoldingAMeshSeatRequiresServingItsVerbs(t *testing.T) { t.Errorf("a holder saying it serves mint: %v", err) } + // L3: the seat mesh-catalog promises four verbs, each optional while two claimants stand (ADR 0604 §1, §6). + catalog, ok := SeatNamed(CatalogSeat) + if !ok || catalog.Scope != ScopeMesh || catalog.Decision != "novox/hq ADR 0604" || len(catalog.Serves) != 4 { + t.Fatalf("mesh-catalog: in the set %v, %+v", ok, catalog) + } + four := []string{"modules", "module", "provides", "dependents"} + for i, v := range catalog.Serves { + if v.Name != four[i] || !v.Optional || v.Input == nil { + t.Errorf("mesh-catalog's verb %d is %+v, want %s, optional, with its input", i, v, four[i]) + } + } + cataloguer := func(serves ...string) Manifest { + return Manifest{Module: "a-catalog", Tools: serves, Claims: []Claim{{Name: CatalogSeat, Scope: ScopeMesh, Serves: serves}}} + } + for name, c := range map[string]struct { + serves []string + word string + }{ + "serving the four": {four, ""}, + "serving three, optional": {four[:3], ""}, + "serving none, the old one": {nil, ""}, + "serving a fifth": {append(slices.Clone(four), "rebuild"), "rebuild"}, + } { + err := CanHold(cataloguer(c.serves...), catalog) + if (c.word == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.word)) { + t.Errorf("a mesh-catalog holder %s: %v", name, err) + } + } + was := Seats() t.Cleanup(func() { UseSeats(was) }) UseSeats([]Seat{{Name: "mesh-controller", Scope: ScopeMesh, Decision: "test", From 6e2f08f96a62fef91e71d0e2cfc5be8851cb58c4 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 16:24:07 +0100 Subject: [PATCH 12/15] =?UTF-8?q?provisioner=5Fdelete's=20description=20na?= =?UTF-8?q?mes=20no=20removed=20controller=20verb:=20it=20asks=20nobody,?= =?UTF-8?q?=20so=20the=20operator=20is=20asked=20on=20the=20operator=20cha?= =?UTF-8?q?nnel=20first=20and=20named=20in=20why=20and=20by=20(ADR=200604?= =?UTF-8?q?=20=C2=A78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/provider/harness/retirement.go | 3 ++- go/provider/harness/retirement_test.go | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/go/provider/harness/retirement.go b/go/provider/harness/retirement.go index f9162f2..b134d98 100644 --- a/go/provider/harness/retirement.go +++ b/go/provider/harness/retirement.go @@ -541,7 +541,8 @@ func RetirementTools(h *Harness) []stdio.Tool { }}, {Name: "provisioner_delete", Description: "Delete one RETIRED consumer — its login and its data — for good. Refused for anything active or " + - "asked for. Use the controller's `cleanup delete`, which records the hand act.", + "asked for. It asks nobody: ask the operator on the operator channel first, and name that answer in why and " + + "by, which the hand-act log keeps (novox/hq ADR 0604 §8).", Input: withOne, Run: func(args map[string]any) (any, error) { consumer := argString(args, "consumer") diff --git a/go/provider/harness/retirement_test.go b/go/provider/harness/retirement_test.go index 0c59f84..c2533d0 100644 --- a/go/provider/harness/retirement_test.go +++ b/go/provider/harness/retirement_test.go @@ -482,6 +482,10 @@ func TestTheToolsAnswer(t *testing.T) { tools := map[string]func(map[string]any) (any, error){} for _, tool := range RetirementTools(w.h) { tools[tool.Name] = tool.Run + // L3: the delete names no removed controller verb, and says who must be asked first (ADR 0604 §8). + if tool.Name == "provisioner_delete" && (strings.Contains(tool.Description, "controller") || !strings.Contains(tool.Description, "operator channel")) { + t.Errorf("provisioner_delete says %q", tool.Description) + } } if len(tools) != 4 { t.Fatalf("%d tools", len(tools)) From fdedf290d574b9314c867158379df36aa8ec6410 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 16:41:30 +0100 Subject: [PATCH 13/15] =?UTF-8?q?provisioner=5Fretire=5Fapprove=20and=20pr?= =?UTF-8?q?ovisioner=5Fretire=5Freject=20name=20no=20removed=20controller?= =?UTF-8?q?=20verb=20either:=20they=20ask=20nobody,=20and=20the=20operator?= =?UTF-8?q?'s=20answer=20on=20the=20operator=20channel=20is=20named=20in?= =?UTF-8?q?=20why=20and=20by=20(ADR=200570=20=C2=A75,=20ADR=200604=20?= =?UTF-8?q?=C2=A78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/provider/harness/retirement.go | 12 +++++++----- go/provider/harness/retirement_test.go | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/go/provider/harness/retirement.go b/go/provider/harness/retirement.go index b134d98..180213a 100644 --- a/go/provider/harness/retirement.go +++ b/go/provider/harness/retirement.go @@ -482,7 +482,7 @@ func (h *Harness) retirementBody(change string, consumers []map[string]any, extr } // RetirementTools are the four tools every provider serves for retirement, the same names in every -// module: the controller's `retire` and `cleanup` verbs ask them on the provider's machine. +// module: a caller asks them on the provider's machine, with the operator's answer named in why and by. func RetirementTools(h *Harness) []stdio.Tool { if h == nil { return nil @@ -493,7 +493,7 @@ func RetirementTools(h *Harness) []stdio.Tool { who := map[string]any{ "why": map[string]any{"type": "string", "description": "why — required, kept in the hand-act log"}, "by": map[string]any{"type": "string", "description": "who decided"}, - "via": map[string]any{"type": "string", "description": "mesh-controller when asked through its verbs"}, + "via": map[string]any{"type": "string", "description": "the part or session that asks, when not the caller itself"}, } withSet := map[string]any{"consumers": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "the set, exactly as provisioner_retirement names it"}} @@ -518,7 +518,8 @@ func RetirementTools(h *Harness) []stdio.Tool { }}, {Name: "provisioner_retire_approve", Description: "Retire exactly the set waiting for a person (or the set rejected earlier): access disabled, data " + - "kept and marked to delete. Use the controller's `retire approve`, which records the hand act.", + "kept and marked to delete. It asks nobody: the operator's answer on the operator channel to the provisioner's " + + "ask (novox/hq ADR 0570 §5) is named in why and by, which the hand-act log keeps.", Input: withSet, Run: func(args map[string]any) (any, error) { ctx, cancel := ask() @@ -530,8 +531,9 @@ func RetirementTools(h *Harness) []stdio.Tool { return map[string]any{"retired": orEmptyList(done)}, nil }}, {Name: "provisioner_retire_reject", - Description: "Keep the set waiting for a person active. Use the controller's `retire reject`.", - Input: withSet, + Description: "Keep the set waiting for a person active. It asks nobody: the operator's answer on the operator channel " + + "(novox/hq ADR 0570 §5) is named in why and by, which the hand-act log keeps.", + Input: withSet, Run: func(args map[string]any) (any, error) { kept, err := h.Reject(strs(args["consumers"]), argString(args, "why"), argString(args, "by"), argString(args, "via")) if err != nil { diff --git a/go/provider/harness/retirement_test.go b/go/provider/harness/retirement_test.go index c2533d0..5b38920 100644 --- a/go/provider/harness/retirement_test.go +++ b/go/provider/harness/retirement_test.go @@ -482,9 +482,9 @@ func TestTheToolsAnswer(t *testing.T) { tools := map[string]func(map[string]any) (any, error){} for _, tool := range RetirementTools(w.h) { tools[tool.Name] = tool.Run - // L3: the delete names no removed controller verb, and says who must be asked first (ADR 0604 §8). - if tool.Name == "provisioner_delete" && (strings.Contains(tool.Description, "controller") || !strings.Contains(tool.Description, "operator channel")) { - t.Errorf("provisioner_delete says %q", tool.Description) + // L3: the delete, approve and reject name no removed controller verb, and say where the operator answers (ADR 0604 §8). + if tool.Name != "provisioner_retirement" && (strings.Contains(tool.Description, "controller") || !strings.Contains(tool.Description, "operator channel")) { + t.Errorf("%s says %q", tool.Name, tool.Description) } } if len(tools) != 4 { From cf5812101b0bccd9811e79ac51a4810e2d989fdb Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 16:51:54 +0100 Subject: [PATCH 14/15] The seat mesh-catalog's verbs' answers are its contract: CatalogModules, CatalogOne, CatalogProvides and CatalogDependents, with the field names of the catalog module's tools they replace; the module graph is no part of mesh-sdk (the operator, 2026-11-03: only contracts) --- go/catalogue/catalog_seat.go | 57 ++++++++++++++++++++++++++++++++++-- go/catalogue/verbs_test.go | 19 ++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/go/catalogue/catalog_seat.go b/go/catalogue/catalog_seat.go index 63b380b..09db316 100644 --- a/go/catalogue/catalog_seat.go +++ b/go/catalogue/catalog_seat.go @@ -1,10 +1,13 @@ package catalogue +import "encoding/json" + // CatalogSeat is the seat of the catalog: what the mesh holds of every module (novox/hq ADR 0604 §1). Its states are // its holder's: the modules the trunk holds, their manifests by digest and the module graph (the indexer's states -// `modules`, `manifests` and `graph`, delivery.ModulesBucket, ManifestsBucket and GraphBucket), announced with who may -// read them; a reader that only watches reads them and calls no verb. It names no event. What must be built again is -// no verb: a module whose fingerprint differs from its newest build, which the delivery view derives. +// `modules`, `manifests` and `graph`), announced with who may read them; a reader that only watches reads them and calls +// no verb. It names no event. What must be built again is no verb: a module whose fingerprint differs from its newest +// build, which the delivery view derives. The verbs' answers are CatalogModules, CatalogOne, CatalogProvides and +// CatalogDependents, whose field names are those of the catalog module's tools they replace. const CatalogSeat = "mesh-catalog" // catalogVerbs are the seat's four verbs, each read from its holder's states. Each is optional until the holder that @@ -28,3 +31,51 @@ func catalogVerbs() []Verb { Input: schema(map[string]string{"module": "the module's name"}, []string{"module"})}, } } + +// CatalogEntry is one module of the catalog: its repository, its directory there ("." for the root), the merge commit it +// was last changed at, and its new-form fingerprint or why it has none. +type CatalogEntry struct { + Module string `json:"module"` + Repository string `json:"repository"` + Path string `json:"path"` + Commit string `json:"commit"` + FP string `json:"fp,omitempty"` + None string `json:"none,omitempty"` +} + +// CatalogModule is one module whole: its entry and its manifest as the trunk holds it. +type CatalogModule struct { + CatalogEntry + Manifest json.RawMessage `json:"manifest"` +} + +// CatalogEnd is a module that provides, requires or depends on something, with its commit; Scope is a provider's scope +// (a module provides its own name at node scope). +type CatalogEnd struct { + Module string `json:"module"` + Commit string `json:"commit"` + Scope string `json:"scope,omitempty"` +} + +// CatalogModules is the answer of the verb modules: every module the trunk holds, sorted by name. +type CatalogModules struct { + Modules []CatalogEntry `json:"modules"` +} + +// CatalogOne is the answer of the verb module. +type CatalogOne struct { + Module CatalogModule `json:"module"` +} + +// CatalogProvides is the answer of the verb provides: the modules that provide the provision and those that require it. +type CatalogProvides struct { + Provision string `json:"provision"` + Provides []CatalogEnd `json:"provides"` + Requires []CatalogEnd `json:"requires"` +} + +// CatalogDependents is the answer of the verb dependents: the modules whose build edges reach the module. +type CatalogDependents struct { + Module string `json:"module"` + Dependents []CatalogEnd `json:"dependents"` +} diff --git a/go/catalogue/verbs_test.go b/go/catalogue/verbs_test.go index af45955..7e7f4c7 100644 --- a/go/catalogue/verbs_test.go +++ b/go/catalogue/verbs_test.go @@ -1,6 +1,7 @@ package catalogue import ( + "encoding/json" "slices" "strings" "testing" @@ -61,6 +62,24 @@ func TestHoldingAMeshSeatRequiresServingItsVerbs(t *testing.T) { t.Errorf("mesh-catalog's verb %d is %+v, want %s, optional, with its input", i, v, four[i]) } } + // The answers keep the field names of the catalog module's tools they replace (catalog_modules, catalog_module, + // catalog_provides, catalog_dependents, as mesh_call answered them on 2026-11-03). + for _, c := range []struct { + answer any + want []string + }{ + {CatalogModules{Modules: []CatalogEntry{{}}}, []string{`"modules":[{"module":`, `"repository":`, `"path":`, `"commit":`}}, + {CatalogOne{Module: CatalogModule{Manifest: json.RawMessage(`{}`)}}, []string{`"module":{"module":`, `"manifest":{}`}}, + {CatalogProvides{Provides: []CatalogEnd{{Scope: "mesh"}}, Requires: []CatalogEnd{{}}}, []string{`"provides":[{"module":"","commit":"","scope":"mesh"}]`, `"requires":[{"module":"","commit":""}]`}}, + {CatalogDependents{Module: "m", Dependents: []CatalogEnd{}}, []string{`"module":"m"`, `"dependents":[]`}}, + } { + raw, _ := json.Marshal(c.answer) + for _, w := range c.want { + if !strings.Contains(string(raw), w) { + t.Errorf("the answer %s does not hold %s", raw, w) + } + } + } cataloguer := func(serves ...string) Manifest { return Manifest{Module: "a-catalog", Tools: serves, Claims: []Claim{{Name: CatalogSeat, Scope: ScopeMesh, Serves: serves}}} } From b7abe203a92920d86798329c5d0462a579582716 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 18:42:08 +0100 Subject: [PATCH 15/15] =?UTF-8?q?Keep,=20deprecated,=20the=20exported=20na?= =?UTF-8?q?mes=20the=20group=20removed=20while=20running=20parts=20use=20t?= =?UTF-8?q?hem:=20the=20checker=20builds=20the=20running=20node-engine's?= =?UTF-8?q?=20composer=20beside=20this=20mesh-sdk=20(ADR=200548=20=C2=A76)?= =?UTF-8?q?;=20the=20second=20group=20removes=20them=20once=20their=20user?= =?UTF-8?q?s=20are=20delivered?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- go/catalogue/amqp_is_not_a_provision_test.go | 4 +- go/catalogue/kept_until_delivered.go | 188 +++++++++++++++++++ 2 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 go/catalogue/kept_until_delivered.go diff --git a/go/catalogue/amqp_is_not_a_provision_test.go b/go/catalogue/amqp_is_not_a_provision_test.go index 0b0191c..ed82b61 100644 --- a/go/catalogue/amqp_is_not_a_provision_test.go +++ b/go/catalogue/amqp_is_not_a_provision_test.go @@ -50,7 +50,9 @@ func TestNoCatalogueManifestNamesAmqp(t *testing.T) { fset := token.NewFileSet() files, _ := filepath.Glob("*.go") for _, f := range files { - if strings.HasSuffix(f, "_test.go") { + // kept_until_delivered.go keeps, deprecated, the names removed while running parts still use them; the second + // group removes it and this exemption together. + if strings.HasSuffix(f, "_test.go") || f == "kept_until_delivered.go" { continue } parsed, err := parser.ParseFile(fset, f, nil, 0) diff --git a/go/catalogue/kept_until_delivered.go b/go/catalogue/kept_until_delivered.go new file mode 100644 index 0000000..c5205e1 --- /dev/null +++ b/go/catalogue/kept_until_delivered.go @@ -0,0 +1,188 @@ +package catalogue + +// This file keeps, deprecated, the exported names feat/l3b-bus removed from this package while a running part still +// uses them: an exported name is removed only after every user of it is delivered. The checker builds the running +// node-engine's composer (mesh-host main) beside this mesh-sdk (ADR 0548 §6), and mesh-host main, mesh-delivery main +// (the indexer's judge, the assigner's and mesh-grants' code and tests) and mesh-tools main (node-tools' tests) name +// them. Nothing in L3's own heads uses any of them. The second group removes this file, and its exemption in +// TestNoCatalogueManifestNamesAmqp, once mesh-host a24a1d72 and L3's mesh-delivery and mesh-tools heads run. + +import ( + "fmt" + "slices" + "sort" + "strings" +) + +// Deprecated: kept until its users are delivered (the head of this file). +// +// EndedControllerName is the name the ended controller had, as its module and as its seat: no manifest may name it. +const EndedControllerName = "mesh-controller" + +// The kinds of a declaration that names the controller. +// +// Deprecated: kept until their users are delivered (the head of this file). +const ( + DependsRead = "read" + DependsInvoke = "invoke" + DependsConsume = "consume" + DependsUses = "uses" + DependsWaits = "waits" + DependsClaims = "claims" + DependsRequires = "requires" + DependsIs = "is" +) + +// Deprecated: kept until its users are delivered (the head of this file). +// +// Dependency is one declaration of a manifest that names the controller. +type Dependency struct { + Module string `json:"module"` + // By is the account declaring it when it is not the module's own: `process ` or `tools `. + By string `json:"by,omitempty"` + Kind string `json:"kind"` + What string `json:"what"` +} + +// onController says whether a name `.` is the controller's. +func onController(name string) bool { return strings.HasPrefix(name, EndedControllerName+".") } + +// invokesController says whether an invoke names a verb of the controller's seat or a tool of its module. `*` names none: +// it is the console's grant to every tool. +func invokesController(invoke string) bool { + if seat, _, ok := SeatInvoke(invoke); ok { + return seat == EndedControllerName + } + return onController(invoke) +} + +// Deprecated: kept until its users are delivered (the head of this file). +// +// Dependencies are every declaration naming the controller in the manifests given, by module. +func Dependencies(manifests map[string]Manifest) []Dependency { + var out []Dependency + add := func(module, by, kind, what string) { + if d := (Dependency{Module: module, By: by, Kind: kind, What: what}); !slices.Contains(out, d) { + out = append(out, d) + } + } + for name, m := range manifests { + if name == EndedControllerName || m.Module == EndedControllerName { + add(name, "", DependsIs, EndedControllerName) + } + for _, r := range m.Reads { + if onController(r) { + add(name, "", DependsRead, r) + } + } + for _, i := range m.Invokes { + if invokesController(i) { + add(name, "", DependsInvoke, i) + } + } + for _, c := range m.Consumes { + if onController(c) { + add(name, "", DependsConsume, c) + } + } + for _, w := range m.Waits { + if onController(w) { + add(name, "", DependsWaits, w) + } + } + for _, u := range m.Uses { + if u == EndedControllerName { + add(name, "", DependsUses, u) + } + } + for _, c := range m.Claims { + if c.Name == EndedControllerName { + add(name, "", DependsClaims, c.Name) + } + } + for _, r := range m.Requires { + if r == EndedControllerName || onController(r) { + add(name, "", DependsRequires, r) + } + } + for _, a := range m.BusAccounts() { + by := "process " + a.ID + if a.Tools { + by = "tools " + a.ID + } + for _, i := range a.Bus.Invokes { + if invokesController(i) { + add(name, by, DependsInvoke, i) + } + } + } + } + sort.Slice(out, func(i, j int) bool { + a, b := out[i], out[j] + if a.Module != b.Module { + return a.Module < b.Module + } + if a.Kind != b.Kind { + return a.Kind < b.Kind + } + if a.What != b.What { + return a.What < b.What + } + return a.By < b.By + }) + return out +} + +// Deprecated: kept until its users are delivered (the head of this file). +// +// ManifestRefusals is the check over a repository's manifests, by module name: one reason per declaration naming the +// controller, none when it passes. None given fails it, since nothing read can hide a declaration. +func ManifestRefusals(manifests map[string]Manifest) []string { + if len(manifests) == 0 { + return []string{"no manifest was read: with nothing read, nothing naming the controller can be judged absent"} + } + var out []string + for _, d := range Dependencies(manifests) { + who := d.Module + if d.By != "" { + who += " (" + d.By + ")" + } + out = append(out, fmt.Sprintf("%s declares %s %s: the controller ended by its unassignment and nothing may name it "+ + "(ADR 0590 clause 5)", who, d.Kind, d.What)) + } + return out +} + +// Deprecated: a manifest says it (Manifest.CoreCredentials, CarriesCoreCredentials); kept until its users are +// delivered (the head of this file). +// +// CoreCredentialModules are the modules whose own secrets are the core's credentials: the bus, mesh-provisioner and +// its client. +var CoreCredentialModules = []string{"mesh-provisioner", "mesh-provisioner-client", "nats"} + +// Deprecated: a manifest says it (Manifest.ToolRunner, IsToolRunner); kept until its users are delivered (the head +// of this file). +// +// RuntimeModule is the module that is the node's tool runtime. +const RuntimeModule = "node-tools" + +// Deprecated: the kept streams are the indexer's judge's; kept until its users are delivered (the head of this file). +// +// StreamsKeptUntilMoved are the streams declared before the check that a module keeps its data in the bus's +// predefined forms, each allowed by name until its module moves it. +var StreamsKeptUntilMoved = map[string][]string{ + "brother": {"device-asks"}, + "mesh-build-go": {"toolchains-go"}, + "mesh-build-node": {"toolchains-node"}, + "mesh-mail": {"mail", "mail-sends"}, + "only-office": {"convert-asks"}, + "mesh-delivery-assigner": {"placement"}, + "mesh-delivery-checker": {"checks"}, + "mesh-delivery-forge-bridge": {"forge"}, +} + +// Deprecated: nothing derives it now (DerivedUpgrade reads the bus's own manifest); kept until its users are +// delivered (the head of this file). +// +// FromBus is the source an older DerivedUpgrade gave the bus's policy. +const FromBus = "the bus"