From 6645ac58c23c2c836e6f3f75f85744cf96f1ef5a Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 12:53:07 +0100 Subject: [PATCH 01/49] A manifest with a field the judge does not know waits instead of being refused; a held module keeps its bus users; tools list and redo the verdicts (ADR 0578 amended, issue 774) --- modules/mesh-delivery-assigner/go.mod | 2 +- modules/mesh-delivery-assigner/go.sum | 4 +- modules/mesh-delivery-builder/go.mod | 2 +- modules/mesh-delivery-builder/go.sum | 4 +- .../cmd/mesh-delivery-checker/engines.go | 4 +- .../cmd/mesh-delivery-checker/judge_test.go | 6 + modules/mesh-delivery-checker/go.mod | 2 +- modules/mesh-delivery-checker/go.sum | 4 +- modules/mesh-delivery-forge-bridge/go.mod | 2 +- modules/mesh-delivery-forge-bridge/go.sum | 4 +- .../cmd/mesh-delivery-indexer-tools/main.go | 53 +++++- .../cmd/mesh-delivery-indexer/main.go | 4 +- .../cmd/mesh-delivery-indexer/takein.go | 10 +- .../internal/index/candidates.go | 2 + .../internal/index/takein.go | 8 +- .../internal/index/toolchains.go | 4 +- .../internal/index/verdicts.go | 171 +++++++++++++++++- .../internal/index/verdicts_test.go | 58 ++++++ modules/mesh-delivery-indexer/judge/judge.go | 24 ++- .../mesh-delivery-indexer/judge/judge_test.go | 15 ++ modules/mesh-delivery-indexer/judge/rules.go | 38 ++++ .../mesh-delivery-indexer/manifest_test.go | 7 +- modules/mesh-delivery-indexer/module.json | 4 +- modules/mesh-delivery-teller/go.mod | 2 +- modules/mesh-delivery-teller/go.sum | 4 +- modules/mesh-delivery-view/go.mod | 2 +- modules/mesh-delivery-view/go.sum | 4 +- modules/mesh-delivery-watchman/go.mod | 2 +- modules/mesh-delivery-watchman/go.sum | 4 +- .../internal/watch/findings_test.go | 12 ++ .../internal/watch/verdicts.go | 21 +++ modules/mesh-enrolment/go.mod | 2 +- modules/mesh-enrolment/go.sum | 4 +- modules/mesh-grants/grants_test.go | 72 +++++++- modules/mesh-grants/internal/grants/held.go | 120 ++++++++++++ modules/mesh-grants/internal/grants/inputs.go | 17 +- modules/mesh-grants/internal/grants/part.go | 19 +- modules/mesh-grants/internal/grants/sealed.go | 2 +- 38 files changed, 656 insertions(+), 63 deletions(-) create mode 100644 modules/mesh-grants/internal/grants/held.go diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index f0e57eeb..39795ea8 100644 --- a/modules/mesh-delivery-assigner/go.mod +++ b/modules/mesh-delivery-assigner/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-assigner go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-assigner/go.sum b/modules/mesh-delivery-assigner/go.sum index 43cc4ec7..6de5762d 100644 --- a/modules/mesh-delivery-assigner/go.sum +++ b/modules/mesh-delivery-assigner/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/go.mod b/modules/mesh-delivery-builder/go.mod index 3fd55d9d..44661726 100644 --- a/modules/mesh-delivery-builder/go.mod +++ b/modules/mesh-delivery-builder/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-builder go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/go.sum b/modules/mesh-delivery-builder/go.sum index 340e467e..9f9f382b 100644 --- a/modules/mesh-delivery-builder/go.sum +++ b/modules/mesh-delivery-builder/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/engines.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/engines.go index 22c6de0e..e16b49e0 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/engines.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/engines.go @@ -128,7 +128,9 @@ func (m *manifestVerdicts) put(module string, v *delivery.ManifestVerdict) { delete(m.by, module) return } - m.by[module] = *v + // The sandbox's composer may be the node-engine of an older delivery, which does not know the verdict waits: it is handed a + // wait as the refusal it is held as. + m.by[module] = v.AsRefused() } func (m *manifestVerdicts) all() map[string]delivery.ManifestVerdict { diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge_test.go index 24c64d28..0b88b7f0 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge_test.go @@ -170,4 +170,10 @@ func TestAManifestFallingBackToAPredefinedKeyIsRead(t *testing.T) { if _, err := parseManifest(manifest); err != nil { t.Errorf("manifestReader: %v", err) } + // The sandbox's composer may be an older node-engine, which knows no verdict waits: it is handed a wait as a refusal. + var held manifestVerdicts + held.put("nats", &delivery.ManifestVerdict{Module: "nats", State: delivery.VerdictManifestWaits, Unknown: []string{"core-credentials"}, Judge: "j"}) + if v := held.all()["nats"]; v.State != delivery.VerdictRefused || len(v.Problems) != 1 || len(v.Unknown) != 0 { + t.Errorf("a waiting verdict is handed to the sandbox as %+v", v) + } } diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index 7024be45..3e680509 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-checker go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-checker/go.sum b/modules/mesh-delivery-checker/go.sum index 340e467e..9f9f382b 100644 --- a/modules/mesh-delivery-checker/go.sum +++ b/modules/mesh-delivery-checker/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-forge-bridge/go.mod b/modules/mesh-delivery-forge-bridge/go.mod index 94aeabd2..f9796036 100644 --- a/modules/mesh-delivery-forge-bridge/go.mod +++ b/modules/mesh-delivery-forge-bridge/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-forge-bridge go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-forge-bridge/go.sum b/modules/mesh-delivery-forge-bridge/go.sum index 43cc4ec7..6de5762d 100644 --- a/modules/mesh-delivery-forge-bridge/go.sum +++ b/modules/mesh-delivery-forge-bridge/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go index 5529d02f..a439683a 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go @@ -1,6 +1,7 @@ // Command mesh-delivery-indexer-tools serves the indexer's doing tool (novox/hq issue 1945): the runtime launches it for a // call. indexer_take_in writes one value of the indexer's takeins state and waits for the indexer's answer there. The -// indexer is the other program: it serves no request subject (ADR 0437 §4). The read tools of the indexer's states are +// indexer is the other program: it serves no request subject (ADR 0437 §4); indexer_judge_again asks the same way, and +// indexer_verdicts reads the indexer's verdicts state. The read tools of the indexer's states are // mesh-delivery-view's (indexer_module, indexer_modules, indexer_candidate, indexer_candidates). package main @@ -21,8 +22,8 @@ import ( // asker writes a takeins value and waits for the indexer's answer to it. type asker func(key string, value []byte) (index.TakeInAsk, error) -// Tools are the indexer's tools. -func Tools(ask asker, now func() time.Time, counts func() (index.OldSectionsCount, error)) []stdio.Tool { +// Tools are the indexer's tools. read is the indexer's state of verdicts, which indexer_verdicts reads; counts the old sections' count. +func Tools(ask asker, now func() time.Time, read stdio.KeptState, counts func() (index.OldSectionsCount, error)) []stdio.Tool { str := func(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) } return []stdio.Tool{ {Name: index.OldSectionsTool, @@ -62,6 +63,50 @@ func Tools(ask asker, now func() time.Time, counts func() (index.OldSectionsCoun } return a.Result, nil }}, + {Name: index.VerdictsTool, + Description: "The indexer's verdicts on manifests that wait or are refused (all: every verdict), each as its verdicts state holds it: " + + "the module, the digest, the state (waits: the manifest carries a field the indexer's judge does not know yet, named in " + + "unknown, and every reader holds the module as a refused one and keeps what runs; refused: the judge's problems), the " + + "judge that made it and when. A module's name narrows it to that module. A waiting manifest is judged again at each " + + "new indexer and refused only once the newest build of the indexer on the trunk still does not know the field; " + + "indexer_judge_again judges now.", + Input: map[string]any{"type": "object", "properties": map[string]any{ + "module": map[string]any{"type": "string", "description": "only this module's verdicts"}, + "all": map[string]any{"type": "boolean", "description": "list accepted verdicts too"}}}, + Run: func(args map[string]any) (any, error) { + keys, err := read.Keys() + if err != nil { + return nil, fmt.Errorf("the verdicts state cannot be listed: %w", err) + } + all, _ := args["all"].(bool) + rows, unread := index.ListVerdicts(keys, func(key string) ([]byte, error) { + e, err := read.Get(key) + if err != nil || e == nil { + return nil, err + } + return e.Value, nil + }, str(args, "module"), all) + return map[string]any{"verdicts": rows, "unread": unread}, nil + }}, + {Name: index.JudgeAgainTool, + Description: "Judge the manifests again now, as the indexer does at its start and after each merge, and answer how the module's " + + "verdict stands (waits, refused or accepted, and why). For a manifest that waits for a judge that knows its fields, or " + + "after a manifest was mended where the indexer has not heard it. A waiting manifest becomes refused here only when the " + + "running indexer is the newest build of itself the trunk holds. indexer_verdicts reads the result.", + Input: map[string]any{"type": "object", "required": []string{"module"}, "properties": map[string]any{ + "module": map[string]any{"type": "string", "description": "the module's name, as the catalogue registers it"}, + "why": map[string]any{"type": "string", "description": "why it is judged again now, in plain words"}}}, + Run: func(args map[string]any) (any, error) { + key, value, err := index.NewJudgeAgain(str(args, "module"), str(args, "why"), stdio.CallerOf(args), now()) + if err != nil { + return nil, err + } + a, err := ask(key, value) + if err != nil { + return nil, err + } + return a.Result, nil + }}, } } @@ -133,7 +178,7 @@ func heldCounts(s stdio.KeptState) func() (index.OldSectionsCount, error) { } func main() { - if err := stdio.Serve("", Tools(askTakeIn(stdio.State(index.TakeInsState), 25*time.Second), time.Now, heldCounts(stdio.State(index.OldSectionsState)))); err != nil { + if err := stdio.Serve("", Tools(askTakeIn(stdio.State(index.TakeInsState), 25*time.Second), time.Now, stdio.State("verdicts"), heldCounts(stdio.State(index.OldSectionsState)))); err != nil { fmt.Fprintf(os.Stderr, "[mesh-delivery-indexer-tools] %v\n", err) os.Exit(1) } diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go index ee6e8f37..baa6807b 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go @@ -424,8 +424,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro logger.Printf("judging the manifests at start: %v; judged again at the next message heard", err) return } - logger.Printf("judged %d manifests at start: %d verdicts written, %d with problems, of which outgrown and kept accepted: %v", - j.Judged, j.Written, len(j.Refused), j.Outgrown) + logger.Printf("judged %d manifests at start: %d verdicts written, %d with problems, of which outgrown and kept accepted: %v, waiting for a judge that knows their fields: %v", + j.Judged, j.Written, len(j.Refused), j.Outgrown, j.Waiting) }) // A build module says each time it wrote its toolchains key: its stream, and the indexer's durable consumer on it, are // made by the controller as the forge's are. Where the stream is not there yet the indexer says so and is started again diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/takein.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/takein.go index f0c7b024..6ec98671 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/takein.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/takein.go @@ -14,6 +14,7 @@ import ( // takerIn is the part of the indexer a takeins value asks through. type takerIn interface { TakeIn(ctx context.Context, module string) index.TakenIn + JudgeAgain(ctx context.Context, module string) index.TakenIn } // takenIn answers one value of the takeins state (the tool indexer_take_in, issue 1945): a value not yet answered takes the @@ -31,9 +32,14 @@ func takenIn(ctx context.Context, ix takerIn, kv jetstream.KeyValue, e jetstream if !a.Answered.IsZero() { return } - r := ix.TakeIn(ctx, a.Module) + asked, r := "take-in", index.TakenIn{} + if a.Judge { + asked, r = "judging again", ix.JudgeAgain(ctx, a.Module) + } else { + r = ix.TakeIn(ctx, a.Module) + } a.Result, a.Answered = &r, time.Now().UTC() - logf("%s: a take-in asked by %s (%s): %s", a.Module, a.By, a.Why, r.Answer) + logf("%s: a %s asked by %s (%s): %s", a.Module, asked, a.By, a.Why, r.Answer) data, err := json.Marshal(a) if err != nil { return diff --git a/modules/mesh-delivery-indexer/internal/index/candidates.go b/modules/mesh-delivery-indexer/internal/index/candidates.go index 09f613a6..84574816 100644 --- a/modules/mesh-delivery-indexer/internal/index/candidates.go +++ b/modules/mesh-delivery-indexer/internal/index/candidates.go @@ -113,6 +113,8 @@ type Indexer struct { filling atomic.Bool // a FillTaken runs in the background (KickFill) fillAgain atomic.Bool // a kick arrived while it ran atHeads map[string]map[string]delivery.PublishedPackages // a member head's published Go modules, by repository@head (candidatePublished) + ownMu sync.Mutex // guards ownFP + ownFP string // the indexer's own fingerprint its modules value named at the start pass (ownBuildIsNewest) judgePending atomic.Bool // the start's judge pass failed: judged again at the next message (verdicts.go) } diff --git a/modules/mesh-delivery-indexer/internal/index/takein.go b/modules/mesh-delivery-indexer/internal/index/takein.go index 9d3915fa..b59456b6 100644 --- a/modules/mesh-delivery-indexer/internal/index/takein.go +++ b/modules/mesh-delivery-indexer/internal/index/takein.go @@ -25,9 +25,11 @@ const ( // TakeInAsk is a value of the takeins state: which module a person asked to be taken in, why, who and when, and, once the // indexer read it, its answer. type TakeInAsk struct { - V int `json:"v"` - Module string `json:"module,omitempty"` - Why string `json:"why,omitempty"` + V int `json:"v"` + Module string `json:"module,omitempty"` + Why string `json:"why,omitempty"` + // Judge asks the manifests to be judged again now (indexer_judge_again) instead of the module to be taken in. + Judge bool `json:"judge,omitempty"` By string `json:"by,omitempty"` At time.Time `json:"at"` Result *TakenIn `json:"result,omitempty"` diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains.go b/modules/mesh-delivery-indexer/internal/index/toolchains.go index 1789b439..51f668e4 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains.go @@ -353,7 +353,7 @@ func (ix *Indexer) writeOverlay(ctx context.Context, overlay, finals map[string] if held && cur.Manifest == next.Manifest { // The judge's verdict stands while the digest does (ADR 0578 §3, ADR 0579 §1): a recompute never writes a // refused or outgrown module as accepted; only the judge changes it. - next.Verdict, next.Problems, next.Warnings = cur.Verdict, cur.Problems, cur.Warnings + next.Verdict, next.Problems, next.Unknown, next.Warnings = cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings } if held && sameButWhere(cur, next) { return nil, false, nil @@ -380,7 +380,7 @@ func (ix *Indexer) writeOverlay(ctx context.Context, overlay, finals map[string] func sameButWhere(cur, next delivery.Module) bool { next = withV(next) next.Commit, next.Src = cur.Commit, cur.Src - next.Verdict, next.Problems, next.Warnings = cur.Verdict, cur.Problems, cur.Warnings + next.Verdict, next.Problems, next.Unknown, next.Warnings = cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings return reflect.DeepEqual(cur, next) } diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts.go b/modules/mesh-delivery-indexer/internal/index/verdicts.go index 64d5f825..48b1af19 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts.go @@ -7,6 +7,8 @@ import ( "fmt" "maps" "slices" + "strings" + "time" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" @@ -18,7 +20,7 @@ import ( // The indexer judges every manifest it holds, once, with its own judge (novox/hq ADR 0578 §1): it writes each verdict in // its state verdicts (delivery.VerdictsBucket), one key per manifest digest, only when the verdict differs from the one // held; a module's value in the modules state carries a refusal (Verdict, Problems), so a reader of modules takes it -// without a second read; a new refusal is said as the event manifest-refused. It judges at its start, so a judge that +// without a second read; a new refusal is said as the event manifest-refused; a manifest with a field the judge does not know waits instead (ownBuildIsNewest). It judges at its start, so a judge that // changed (a new indexer) judges every manifest again, and after every merge it takes in, since a rule between manifests // can turn on any of them. Nothing polls. @@ -31,6 +33,8 @@ type Judged struct { Refused map[string][]string // Outgrown are the modules accepted before whose problems are now warnings (ADR 0579 §1). Outgrown []string + // Waiting are the modules whose manifest carries a field this judge does not know and waits. + Waiting []string } // JudgeAll judges every manifest the modules state names, as one catalogue with the predefined list the indexer keeps, @@ -80,11 +84,15 @@ func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged, if ix.OldSections != nil { counts = ix.heldOldSections(ctx) } - refused, unnamed := judge.ShelfCounting(raws, predefined, 0, counts) + refused, waits, unnamed := judge.ShelfWaiting(raws, predefined, 0, counts) for _, p := range unnamed { ix.logf("judge: a problem between the manifests that no one module's removal ends, so it refuses none: %s", p) } out.Refused = refused + // A manifest with a field this judge does not know waits for a judge that may know it (clause 3 of the record on a + // release's Approve; ADR 0578 §1): it is refused only when this program is the newest build of the indexer the trunk + // holds, so no judge that could know the field is still to come. + newest := ix.ownBuildIsNewest(held) id := ix.JudgeID if id == "" { id = judge.Identity() @@ -93,7 +101,21 @@ func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged, for _, name := range slices.Sorted(maps.Keys(digests)) { out.Judged++ v := delivery.ManifestVerdict{Module: name, State: delivery.VerdictAccepted, Judge: id, At: ix.now()} - if problems := refused[name]; len(problems) > 0 { + if fields := waits[name]; len(fields) > 0 { + accepted := ix.acceptedBefore(ctx, digests[name], fresh) + if !newest && !accepted { + v.State, v.Unknown = delivery.VerdictManifestWaits, fields + out.Waiting = append(out.Waiting, name) + } else { + _, problems := judge.Manifest(raws[name], ix.predefinedList(ctx)) + if accepted { + v.Warnings = problems + out.Outgrown = append(out.Outgrown, name) + } else { + v.State, v.Problems = delivery.VerdictRefused, problems + } + } + } else if problems := refused[name]; len(problems) > 0 { if ix.acceptedBefore(ctx, digests[name], fresh) { v.Warnings = problems out.Outgrown = append(out.Outgrown, name) @@ -117,6 +139,9 @@ func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged, } if wrote { out.Written++ + if v.Waits() { + ix.logf("judge: %s", v.Why()) + } if v.Refused() { ix.logf("judge: %s", v.Why()) if ix.SayRefused != nil { @@ -142,6 +167,11 @@ func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged, // JudgeAtStart is JudgeAll of every manifest held, none fresh, as the indexer starts (a judge that changed judges every // one again); one that fails is judged again at the next message or source revision heard (kickJudge). func (ix *Indexer) JudgeAtStart(ctx context.Context) (Judged, error) { + if held, err := ix.heldModules(ctx); err == nil { + ix.ownMu.Lock() + ix.ownFP = held[delivery.IndexerModule].FP2 // this program's build, until the trunk names another + ix.ownMu.Unlock() + } j, err := ix.JudgeAll(ctx, nil) ix.judgePending.Store(err != nil) return j, err @@ -170,7 +200,7 @@ func (ix *Indexer) acceptedBefore(ctx context.Context, digest string, fresh map[ return !fresh[digest] } prev, err := delivery.DecodeManifestVerdict(value) - return err == nil && !prev.Refused() + return err == nil && prev.State == delivery.VerdictAccepted } // writeVerdict writes v under the digest's key unless the verdict held there says the same (ManifestVerdict.Same); wrote @@ -194,6 +224,21 @@ func (ix *Indexer) writeVerdict(ctx context.Context, digest string, v delivery.M return wrote, err } +// ownBuildIsNewest says whether the build of the indexer this program is, is the newest the trunk holds: nothing gives a +// module the fingerprint it was built from, so it is the one the indexer's own modules value named when this program +// judged at its start (ownFP), and the trunk holds no newer one while that value still names it. A newer build of the +// indexer on the trunk, as one that pins a changed mesh-sdk always is, ends it at the trunk's merge, before it runs; false +// when the value names none or this program judged no start pass. +func (ix *Indexer) ownBuildIsNewest(held map[string]delivery.Module) bool { + fp := held[delivery.IndexerModule].FP2 + if fp == "" { + return false + } + ix.ownMu.Lock() + defer ix.ownMu.Unlock() + return ix.ownFP != "" && ix.ownFP == fp +} + // carryVerdict puts a refusal into the module's value, and takes one out once the manifest is accepted, while the value // still names the digest judged; an accepted module's value carries nothing, so judging writes no value of a module the // judge accepts (a reader takes a value without a verdict as accepted, delivery.TakeManifestVerdict). @@ -210,14 +255,18 @@ func (ix *Indexer) carryVerdict(ctx context.Context, name, digest string, v deli if err != nil || cur.Manifest != digest { return nil, false, nil // gone, unreadable, or moved to another manifest since: the next pass judges it } - state := "" - if v.Refused() { + // A wait is written as a refusal naming the fields, so a reader pinned before the verdict waits holds it as refused. + state, problems := "", v.Problems + switch { + case v.Waits(): + state, problems = delivery.VerdictRefused, []string{v.WaitsOn()} + case v.Refused(): state = delivery.VerdictRefused } - if cur.Verdict == state && slices.Equal(cur.Problems, v.Problems) && slices.Equal(cur.Warnings, v.Warnings) { + if cur.Verdict == state && slices.Equal(cur.Problems, problems) && slices.Equal(cur.Unknown, v.Unknown) && slices.Equal(cur.Warnings, v.Warnings) { return nil, false, nil } - cur.Verdict, cur.Problems, cur.Warnings = state, v.Problems, v.Warnings + cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings = state, problems, v.Unknown, v.Warnings if state == "" { cur.Problems = nil } @@ -247,3 +296,109 @@ func (ix *Indexer) predefinedList(ctx context.Context) catalogue.PredefinedList } return list } + +// The tools that read and steer the verdicts (the instruction "tools are how a module is spoken to"): indexer_verdicts +// lists the manifests that wait or are refused, and indexer_judge_again judges one module again now. +const ( + VerdictsTool = "indexer_verdicts" + JudgeAgainTool = "indexer_judge_again" + judgeAgainPrefix = "judge." +) + +// VerdictRow is one verdict as indexer_verdicts lists it: the manifest's digest, the module, its state, the fields it waits +// on or the problems that refuse it, and the judge that made it. +type VerdictRow struct { + Module string `json:"module"` + Digest string `json:"digest"` + State string `json:"state"` + Words string `json:"words,omitempty"` + Unknown []string `json:"unknown,omitempty"` + Problems []string `json:"problems,omitempty"` + Warnings []string `json:"warnings,omitempty"` + Judge string `json:"judge"` + At time.Time `json:"at"` +} + +// ListVerdicts reads the verdicts under keys with get and lists those of module (every module when empty) that wait or are +// refused, or every one with all, sorted by module and digest. A value that does not read is said in unread by its key. +func ListVerdicts(keys []string, get func(key string) ([]byte, error), module string, all bool) (rows []VerdictRow, unread map[string]string) { + unread = map[string]string{} + for _, key := range keys { + value, err := get(key) + if err != nil || value == nil { + if err != nil { + unread[key] = err.Error() + } + continue + } + v, err := delivery.DecodeManifestVerdict(value) + if err != nil { + unread[key] = err.Error() + continue + } + if (module != "" && v.Module != module) || (!all && !v.Held()) { + continue + } + rows = append(rows, VerdictRow{Module: v.Module, Digest: key, State: v.State, Words: v.Why(), Unknown: v.Unknown, + Problems: v.Problems, Warnings: v.Warnings, Judge: v.Judge, At: v.At}) + } + slices.SortFunc(rows, func(a, b VerdictRow) int { + if c := strings.Compare(a.Module, b.Module); c != 0 { + return c + } + return strings.Compare(a.Digest, b.Digest) + }) + return rows, unread +} + +// JudgeAgain judges the manifests again now, as at the start, and answers how the module's stands: the tool +// indexer_judge_again. A module the modules state does not hold, or whose manifest is not in the manifests state, is +// answered so. +func (ix *Indexer) JudgeAgain(ctx context.Context, module string) TakenIn { + out := TakenIn{Module: module} + held, err := ix.heldModules(ctx) + if err != nil { + out.Answer = "the modules state cannot be read: " + err.Error() + return out + } + m, ok := held[module] + if !ok || m.Manifest == "" { + out.Answer = "the indexer holds no manifest of " + module + return out + } + if _, err := ix.JudgeAll(ctx, nil); err != nil { + out.Answer = "the manifests could not be judged: " + err.Error() + return out + } + value, _, found, err := ix.Verdicts.Get(ctx, m.Manifest) + if err != nil || !found { + out.Answer = "judged, but the verdict on " + module + " cannot be read" + return out + } + v, err := delivery.DecodeManifestVerdict(value) + if err != nil { + out.Answer = "judged, but the verdict on " + module + " cannot be read: " + err.Error() + return out + } + out.Stored = true + out.Answer = "judged again by " + v.Judge + ": " + v.State + if v.Held() { + out.Answer += " (" + v.Why() + ")" + } + return out +} + +// NewJudgeAgain is the value indexer_judge_again writes for a module: its key in the takeins state and the value. +func NewJudgeAgain(module, why, by string, at time.Time) (string, []byte, error) { + key, data, err := NewTakeIn(module, why, by, at) + if err != nil { + return "", nil, err + } + var a TakeInAsk + if err := json.Unmarshal(data, &a); err != nil { + return "", nil, err + } + a.Judge = true + data, err = json.Marshal(a) + return judgeAgainPrefix + key, data, err +} diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go index 2e84f954..8d08a033 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go @@ -196,6 +196,64 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { t.Fatalf("a count that could not be read: %+v, logged %q", v, logged) } ix.OldSections = held + // Clause 3 of the record on a release's Approve: a manifest with a field this judge does not know waits (no refusal, no + // event) while a newer build of the indexer is on the trunk, and is judged again at the next judge; once this program's + // build is the trunk's newest it is refused; a wait is written into the module's value as a refusal naming the fields. + ownFP := "src2:" + strings.Repeat("e", 64) + hold(delivery.IndexerModule, `{"module":"mesh-delivery-indexer","version":"1"}`) + ix.ownFP = ownFP // this program started when the trunk held another build of the indexer than the one it holds now + later := hold("later", `{"module":"later","version":"1","a-field-from-later":1,"another":{"x":1}}`) + said = nil + before := len(said) + j, err = ix.JudgeAll(ctx, map[string]bool{later: true}) + if err != nil || len(j.Waiting) != 1 || j.Waiting[0] != "later" || len(said) != before { + t.Fatalf("a manifest with fields from later: %+v %v, said %d", j, err, len(said)) + } + if v := verdict(later); !v.Waits() || v.Refused() || strings.Join(v.Unknown, ",") != "a-field-from-later,another" { + t.Fatalf("later: %+v", v) + } + if m := module("later"); m.Verdict != delivery.VerdictRefused || len(m.Unknown) != 2 || !m.VerdictOf("later").Waits() || + !strings.Contains(m.Problems[0], "does not know yet") { + t.Fatalf("modules.later carries %+v", m) + } + // A renamed field is a known mistake: refused at once. + renamed := hold("renamed", `{"module":"renamed","version":"1","needs":{"x":"y"}}`) + if j, err := ix.JudgeAll(ctx, map[string]bool{renamed: true}); err != nil || len(j.Waiting) != 1 || !verdict(renamed).Refused() { + t.Fatalf("a renamed field: %+v %v", j, err) + } + // The trunk's newest build of the indexer is this program's: the field is one no judge to come knows. + ix.ownFP = "src2:" + strings.Repeat("a", 64) + if j, err := ix.JudgeAll(ctx, nil); err != nil || len(j.Waiting) != 0 || !verdict(later).Refused() || !strings.Contains(verdict(later).Problems[0], "a-field-from-later") { + t.Fatalf("the newest build refuses: %+v %v %+v", j, err, verdict(later)) + } + if m := module("later"); m.Verdict != delivery.VerdictRefused || m.VerdictOf("later").Waits() || len(m.Unknown) != 0 { + t.Fatalf("modules.later after the refusal carries %+v", m) + } + // The tools: indexer_verdicts lists the verdicts that wait or are refused (and every one with all), indexer_judge_again + // judges now and says how the module's verdict stands. + keys, _ := ix.Verdicts.Keys(ctx) + get := func(key string) ([]byte, error) { v, _, _, err := ix.Verdicts.Get(ctx, key); return v, err } + if rows, unread := ListVerdicts(keys, get, "later", false); len(rows) != 1 || rows[0].State != delivery.VerdictRefused || len(unread) != 0 { + t.Fatalf("indexer_verdicts of later: %+v %v", rows, unread) + } + if rows, _ := ListVerdicts(keys, get, "", true); len(rows) <= len(keys)-3 || rows[0].Module > rows[len(rows)-1].Module { + t.Fatalf("indexer_verdicts, all: %d rows of %d verdicts", len(rows), len(keys)) + } + ix.ownFP = ownFP + if got := ix.JudgeAgain(ctx, "later"); !got.Stored || !strings.Contains(got.Answer, "waits") || !strings.Contains(got.Answer, "a-field-from-later") { + t.Fatalf("indexer_judge_again: %+v", got) + } + if rows, _ := ListVerdicts(keys, get, "later", false); len(rows) != 1 { + t.Fatalf("indexer_verdicts of later after: %+v", rows) + } + if got := ix.JudgeAgain(ctx, "nobody"); got.Stored || !strings.Contains(got.Answer, "holds no manifest") { + t.Fatalf("indexer_judge_again of nobody: %+v", got) + } + if key, value, err := NewJudgeAgain("later", "why", "me", time.Now()); err != nil || key != "judge.later" { + t.Fatalf("the ask: %q %v", key, err) + } else if a, err := DecodeTakeIn(value); err != nil || !a.Judge || a.Module != "later" { + t.Fatalf("the ask's value: %+v %v", a, err) + } // Without the verdicts state the indexer judges nothing. ix.Verdicts = nil if j, err := ix.JudgeAll(ctx, nil); err != nil || j.Judged != 0 { diff --git a/modules/mesh-delivery-indexer/judge/judge.go b/modules/mesh-delivery-indexer/judge/judge.go index 32ae12ca..b41c6ba0 100644 --- a/modules/mesh-delivery-indexer/judge/judge.go +++ b/modules/mesh-delivery-indexer/judge/judge.go @@ -73,11 +73,29 @@ func Shelf(raws map[string][]byte, predefined catalogue.PredefinedList, longestM } // ShelfCounting is Shelf also refusing a manifest of a module outside the foundation that uses an old section whose count -// is 0 (OldSectionProblems; issue 1966 D4); nil counts refuse none. +// is 0 (OldSectionProblems; issue 1966 D4); nil counts refuse none. A field this judge does not know is refused, strictly, as +// the checker judges a change: the judge that runs there is the one built at the change's head, which knows the fields the +// change brings, so a field it does not know is a mistake. func ShelfCounting(raws map[string][]byte, predefined catalogue.PredefinedList, longestMachine int, counts OldSectionCounts) (refused map[string][]string, unnamed []string) { - refused = map[string][]string{} + refused, waits, unnamed := ShelfWaiting(raws, predefined, longestMachine, counts) + for name := range waits { + _, refused[name] = Manifest(raws[name], predefined) + } + return refused, unnamed +} + +// ShelfWaiting is ShelfCounting where a manifest that carries a field this judge does not know (UnknownFields) is not +// refused: it is in waits, by module, naming each field, and judged no further, because a newer judge may know the field +// (novox/hq ADR 0578 §1, amended). The indexer judges so; the checker, which runs the judge built at the head of the change, +// judges with Shelf. +func ShelfWaiting(raws map[string][]byte, predefined catalogue.PredefinedList, longestMachine int, counts OldSectionCounts) (refused, waits map[string][]string, unnamed []string) { + refused, waits = map[string][]string{}, map[string][]string{} shelf := catalogue.Shelf{} for name, raw := range raws { + if unknown := UnknownFields(raw); len(unknown) > 0 { + waits[name] = unknown + continue + } m, problems := Manifest(raw, predefined) if len(problems) > 0 { refused[name] = problems @@ -132,7 +150,7 @@ func ShelfCounting(raws map[string][]byte, predefined catalogue.PredefinedList, for name := range refused { sort.Strings(refused[name]) } - return refused, unnamed + return refused, waits, unnamed } // Between is every problem between the manifests of shelf, sorted: catalogue.CatalogueProblems, IdentityProblems over diff --git a/modules/mesh-delivery-indexer/judge/judge_test.go b/modules/mesh-delivery-indexer/judge/judge_test.go index 406be32e..f4fe062a 100644 --- a/modules/mesh-delivery-indexer/judge/judge_test.go +++ b/modules/mesh-delivery-indexer/judge/judge_test.go @@ -54,6 +54,21 @@ func TestTheJudge(t *testing.T) { } } } + // A field the judge does not know waits in the indexer's judge and stays a refusal in the checker's (clause 3 of the + // record on a release's Approve); a renamed field and any other problem are refused by both. + waitRaws := map[string][]byte{ + "later": []byte(`{"module":"later","version":"1","core-credentials":true,"other":1}`), + "renamed": []byte(`{"module":"renamed","version":"1","needs":{"x":"y"}}`), + "bad": []byte(`{"module":"bad","version":"1","provides":[{"name":"amqp"}]}`), + "ok": []byte(`{"module":"ok","version":"1"}`), + } + refused, waits, _ := ShelfWaiting(waitRaws, nil, 0) + if !reflect.DeepEqual(waits, map[string][]string{"later": {"core-credentials", "other"}}) || len(refused) != 2 || refused["renamed"] == nil || refused["bad"] == nil { + t.Errorf("ShelfWaiting: waits %v, refused %v", waits, refused) + } + if strict, _ := Shelf(waitRaws, nil, 0); len(strict) != 3 || !strings.Contains(strings.Join(strict["later"], ""), "core-credentials") { + t.Errorf("Shelf refuses as before: %v", strict) + } // The private network's manifest, which the mesh provides itself, is accepted (moved from mesh-sdk overlay's test). if raw, err := json.Marshal(overlay.Manifest()); err != nil { t.Fatal(err) diff --git a/modules/mesh-delivery-indexer/judge/rules.go b/modules/mesh-delivery-indexer/judge/rules.go index 13334227..4ca46f7d 100644 --- a/modules/mesh-delivery-indexer/judge/rules.go +++ b/modules/mesh-delivery-indexer/judge/rules.go @@ -2388,6 +2388,44 @@ func ownSecretPromptProblems(m catalogue.Manifest, name, prompt string, family b return problems } +// unknownFieldText is how the lenient decoder words the first field its shape does not have, and readWithout the list of +// every field it read the manifest without (catalogue.Manifest.SetAside). +var ( + unknownFieldText = regexp.MustCompile(`json: unknown field "([^"]+)"`) + readWithout = regexp.MustCompile(`\(read without ([^)]*)\)`) +) + +// UnknownFields are the fields of a manifest the judge does not know, as the lenient decoder sets them aside (catalogue.Decode, +// each once, the path it names them with), or none: a manifest this judge reads whole has none, and a field that used to +// mean something (renamed) is a known mistake, not a field a later judge may know, so a manifest with one has none +// either. +func UnknownFields(raw []byte) []string { + m, err := catalogue.Decode(raw) + aside := m.SetAside() + if err != nil || aside == "" { + return nil + } + first := unknownFieldText.FindStringSubmatch(aside) + if first == nil { + return nil // set aside for another reason: the judge's refusal as it is + } + fields := []string{first[1]} + if list := readWithout.FindStringSubmatch(aside); list != nil { + fields = fields[:0] + for _, f := range strings.Split(list[1], ", ") { + if f = strings.TrimSpace(f); f != "" && !slices.Contains(fields, f) { + fields = append(fields, f) + } + } + } + for _, f := range fields { + if _, was := renamed[f[strings.LastIndex(f, ".")+1:]]; was { + return nil + } + } + return fields +} + func parseManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.Manifest, error) { var m catalogue.Manifest // Strictly. **An unknown key is refused**, which is the discipline the host's declaration diff --git a/modules/mesh-delivery-indexer/manifest_test.go b/modules/mesh-delivery-indexer/manifest_test.go index c6bbbf90..f359f14f 100644 --- a/modules/mesh-delivery-indexer/manifest_test.go +++ b/modules/mesh-delivery-indexer/manifest_test.go @@ -113,9 +113,10 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build if want := []string{"gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", "gitea.gitea_open_pulls"}; !reflect.DeepEqual(m.Invokes, want) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } - // 1945: indexer_take_in, served by its tools bundle through the takeins state; the process serves none. 1966/AC11: - // indexer_old_sections reads the count of the old sections (state oldsections). - if want := []string{"indexer_take_in", "indexer_old_sections"}; !reflect.DeepEqual(m.Tools, want) { + // 1945: indexer_take_in, served by its tools bundle through the takeins state; the process serves none. The same bundle + // reads the verdicts state (indexer_verdicts) and asks the indexer to judge again through the takeins state + // (indexer_judge_again; issue 774, clause 3). 1966/AC11: indexer_old_sections reads the count of the old sections (state oldsections). + if want := []string{"indexer_take_in", "indexer_old_sections", "indexer_verdicts", "indexer_judge_again"}; !reflect.DeepEqual(m.Tools, want) { t.Errorf("tools = %v, want %v", m.Tools, want) } // No streams, no bus, no seat (ADR 0437 §4); it says no event. diff --git a/modules/mesh-delivery-indexer/module.json b/modules/mesh-delivery-indexer/module.json index 313afc81..a876e3c3 100644 --- a/modules/mesh-delivery-indexer/module.json +++ b/modules/mesh-delivery-indexer/module.json @@ -4,7 +4,9 @@ "slug": "indexer", "tools": [ "indexer_take_in", - "indexer_old_sections" + "indexer_old_sections", + "indexer_verdicts", + "indexer_judge_again" ], "consumes": [ "mesh-build-go.toolchain.reported", diff --git a/modules/mesh-delivery-teller/go.mod b/modules/mesh-delivery-teller/go.mod index b27cca6b..721c8ed5 100644 --- a/modules/mesh-delivery-teller/go.mod +++ b/modules/mesh-delivery-teller/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-teller go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-teller/go.sum b/modules/mesh-delivery-teller/go.sum index 43cc4ec7..6de5762d 100644 --- a/modules/mesh-delivery-teller/go.sum +++ b/modules/mesh-delivery-teller/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index faf8b6ba..499bb922 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-view go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index 43cc4ec7..6de5762d 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-watchman/go.mod b/modules/mesh-delivery-watchman/go.mod index 9eafb632..bd9b3bf2 100644 --- a/modules/mesh-delivery-watchman/go.mod +++ b/modules/mesh-delivery-watchman/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-watchman go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-watchman/go.sum b/modules/mesh-delivery-watchman/go.sum index 1582a407..6e5f2993 100644 --- a/modules/mesh-delivery-watchman/go.sum +++ b/modules/mesh-delivery-watchman/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-watchman/internal/watch/findings_test.go b/modules/mesh-delivery-watchman/internal/watch/findings_test.go index 573abe74..3e94a361 100644 --- a/modules/mesh-delivery-watchman/internal/watch/findings_test.go +++ b/modules/mesh-delivery-watchman/internal/watch/findings_test.go @@ -59,6 +59,18 @@ func TestTheWatchmansFindings(t *testing.T) { t.Fatalf("findings = %v", o.findings) } w.ModuleVerdict("node-tools", value(""), Put) + // Clause 3 of the record on a release's Approve: a manifest that waits for a judge that knows its fields is its own + // warning (a refusal in the value, naming the fields), not manifest-refused, and goes when it is accepted. + waiting, _ := delivery.EncodeModule(delivery.Module{FP2: "src2:" + strings.Repeat("a", 64), Manifest: strings.Repeat("b", 64), + Repository: "novox/mesh-catalog", Commit: "c", Verdict: delivery.VerdictRefused, Problems: []string{"waits"}, Unknown: []string{"core-credentials"}}) + w.ModuleVerdict("nats", waiting, Put) + if f := o.findings[FindingKey(ScopeModule, "nats", KindManifestWaits)]; f.Severity != Warning || !strings.Contains(f.Summary, "core-credentials") { + t.Fatalf("findings = %v", o.findings) + } + if _, refused := o.findings[FindingKey(ScopeModule, "nats", KindManifestRefused)]; refused { + t.Fatalf("a waiting manifest was found refused: %v", o.findings) + } + w.ModuleVerdict("nats", value(""), Put) }, map[string]string{}}, {"WM4: a stuck key not moved is its finding, and its move deletes it", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { w.Step("go", "n1.ask1", runningStep("ask1", t0), Put) diff --git a/modules/mesh-delivery-watchman/internal/watch/verdicts.go b/modules/mesh-delivery-watchman/internal/watch/verdicts.go index 1a80e65c..b31b648e 100644 --- a/modules/mesh-delivery-watchman/internal/watch/verdicts.go +++ b/modules/mesh-delivery-watchman/internal/watch/verdicts.go @@ -14,6 +14,11 @@ const KindManifestRefused = "manifest-refused" // (novox/hq ADR 0579 §1): it stays accepted and runs; the warning stands until the manifest is mended. const KindManifestOutgrown = delivery.ManifestOutgrown +// KindManifestWaits is a module whose manifest carries a field the indexer's judge does not know yet (novox/hq ADR 0578 +// §1, amended): every reader holds it as a refused one and keeps what runs, and the warning stands until a judge knows the +// field or the manifest is mended. +const KindManifestWaits = delivery.ManifestWaits + // ModuleVerdict is a change of the indexer's value of a module (delivery.ModulesBucket): a value carrying a refusal is a // finding, one that does not (accepted, or not judged yet) or a deleted key ends it. func (w *Watchman) ModuleVerdict(module string, value []byte, op Op) { @@ -21,9 +26,11 @@ func (w *Watchman) ModuleVerdict(module string, value []byte, op Op) { defer w.mu.Unlock() key := FindingKey(ScopeModule, module, KindManifestRefused) outgrown := FindingKey(ScopeModule, module, KindManifestOutgrown) + waits := FindingKey(ScopeModule, module, KindManifestWaits) if op != Put { w.unfind(key) w.unfind(outgrown) + w.unfind(waits) return } m, err := delivery.DecodeModule(value) @@ -32,6 +39,20 @@ func (w *Watchman) ModuleVerdict(module string, value []byte, op Op) { return } v := m.VerdictOf(module) + if v != nil && v.Waits() { + w.unfind(key) + w.unfind(outgrown) + w.find(Finding{Scope: ScopeModule, ID: module, Kind: KindManifestWaits, Severity: Warning, Source: delivery.ModulesBucket, + Summary: v.Why(), + Said: strings.Join(v.Unknown, "; "), + Headline: "The manifest of " + module + " waits for a judge that knows its fields", + Explanation: "The manifest carries " + strings.Join(v.Unknown, ", ") + ", which the indexer's judge does not know yet, so every " + + "reader holds " + module + " as a refused one and keeps what runs; an indexer that knows the fields judges it again " + + "at its start, and the newest build of the indexer refuses a field it does not know.", + Needs: "nothing while a newer indexer is on its way; else a pull request that mends the manifest of " + module}) + return + } + w.unfind(waits) if refused, _ := delivery.TakeManifestVerdict(v); !refused { w.unfind(key) if v == nil || len(v.Warnings) == 0 { diff --git a/modules/mesh-enrolment/go.mod b/modules/mesh-enrolment/go.mod index d6d91c02..3ea0d929 100644 --- a/modules/mesh-enrolment/go.mod +++ b/modules/mesh-enrolment/go.mod @@ -3,7 +3,7 @@ module mesh-enrolment go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-enrolment/go.sum b/modules/mesh-enrolment/go.sum index 43cc4ec7..6de5762d 100644 --- a/modules/mesh-enrolment/go.sum +++ b/modules/mesh-enrolment/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 6488719b..816052a4 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -1487,7 +1487,7 @@ func TestGrants(t *testing.T) { fp := partOn(b) fp.Objects = p.Objects fp.Say = func(c asks.Condition) error { - if c.Key != grants.GrantMissingCondition && c.Key != grants.RefusedCondition { // their own rows + if c.Key != grants.GrantMissingCondition && c.Key != grants.RefusedCondition && c.Key != grants.HeldModuleCondition { // their own rows fresh = append(fresh, c) } return nil @@ -2391,7 +2391,7 @@ func TestGrants(t *testing.T) { var said, behind []asks.Condition p.Say = func(c asks.Condition) error { switch c.Key { - case grants.GrantMissingCondition, grants.RefusedCondition, grants.FullCondition: // their own rows + case grants.GrantMissingCondition, grants.RefusedCondition, grants.HeldModuleCondition, grants.FullCondition: // their own rows case grants.ConsumerBehindCondition: behind = append(behind, c) default: @@ -2690,6 +2690,74 @@ func TestGrants(t *testing.T) { t.Errorf("the refusal was not cleared once it composes: %v, said %+v", res.PrincipalsRefused, said) } }) + + // Clause 3 of the record on a release's Approve (issue 774): a module whose manifest waits for a judge that knows a field + // of it, or is refused, and whose want names no build, keeps its users of the list in force and is said in a condition; + // the rest of the list is written as ever, and the users go the day the manifest is accepted and the module unassigned. + t.Run("a module whose manifest waits or is refused keeps its users of the list in force", func(t *testing.T) { + b := fixture() + const ctl = `{"module":"mod-r","version":"1","own-secrets":{"broker":"/var/lib/r/broker","broker-mod-r-tools":"${dir:tools-secret}/broker-mod-r-tools"},` + + `"resources":[{"id":"tools-secret","type":"directory","path":"/var/lib/mesh/mod-r-tools","mode":"0711"}],"build":{"artifacts":[{"name":"tools",` + + `"kind":"bundle","language":"go","system":"arch","from":"cmd/mod-r-tools","binary":"mod-r-tools","loads":["mod-r-tools"],"bus":{}}]}}` + b.manifestOf("mod-r", ctl) + b.assign("b", "mod-r") + for _, u := range shadowOf(t, b) { + b.put(grants.ReadAccounts, u.User, map[string]any{"user": u.User, "hash": "$2a$10$first", "generation": 1, "fingerprint": "sha256:1"}) + } + b.put(grants.ReadAccounts, "b.mod-r.mod-r-tools", map[string]any{"user": "b.mod-r.mod-r-tools", "hash": "$2a$10$first", "generation": 1, "fingerprint": "sha256:1"}) + busKey, err := seal.GenerateKeyPair() + if err != nil { + t.Fatal(err) + } + b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, + UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 5, Key: busKey.PublicKey}}) + p := partOn(b) + p.Objects = &fakeObjects{} + var said []asks.Condition + p.Say = func(c asks.Condition) error { + if c.Key == grants.HeldModuleCondition { + said = append(said, c) + } + return nil + } + p.Compose() + inForce, _, _ := p.Sealed() + before, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(inForce.Digest, "b.mod-r.mod-r-tools")) + if !ok || len(said) != 1 || said[0].State != asks.StateCleared { + t.Fatalf("the list in force keeps no grants of b.mod-r.mod-r-tools (%v), or a held module was said before: %+v", ok, said) + } + for gen, held := range []struct{ name, unknown string }{{"waits", "core-credentials"}, {"refused", ""}} { + // Another user's hash changes in the same pass, so a list that is written shows the held module did not hold it. + b.put(grants.ReadAccounts, "a.mod-x", map[string]any{"user": "a.mod-x", "hash": fmt.Sprintf("$2a$10$gen%d", gen), "generation": gen + 2, "fingerprint": fmt.Sprintf("sha256:%d", gen+2)}) + was, _, _ := p.Sealed() + var unknown []string + if held.unknown != "" { + unknown = []string{held.unknown} + } + b.put(grants.ReadModules, "mod-r", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32), + Manifest: delivery.ManifestDigest([]byte(ctl)), Repository: "novox/x", Commit: "c", Verdict: delivery.VerdictRefused, + Problems: []string{"it is held"}, Unknown: unknown}) + b.put(grants.ReadModules, "mod-q", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32), + Manifest: delivery.ManifestDigest([]byte(ctl)), Repository: "novox/x", Commit: "c"}) + res := p.Compose() + now, _, _ := p.Sealed() + after, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(now.Digest, "b.mod-r.mod-r-tools")) + if now.Digest == was.Digest || slices.ContainsFunc(res.Errors, func(e string) bool { return strings.Contains(e, "composed whole") }) { + t.Errorf("%s: a held module held the whole list: sealed %+v", held.name, res.Sealed) + } + if !ok || string(after) != string(before) || len(res.PrincipalsRefused) != 0 { + t.Errorf("%s: b.mod-r.mod-r-tools did not keep its grants of the list in force: %s, was %s", held.name, after, before) + } + if last := said[len(said)-1]; last.State != asks.StateRaised || !strings.Contains(last.Summary, "b.mod-r.mod-r-tools (stays in the list") { + t.Errorf("%s: the kept user was not said: %+v", held.name, last) + } + } + // Accepted again: the users compose from the manifest and the condition clears. + b.manifestOf("mod-r", ctl) + if p.Compose(); said[len(said)-1].State != asks.StateCleared { + t.Errorf("the condition was not cleared once the manifest is accepted: %+v", said[len(said)-1]) + } + }) } // fakeObjects is the part's object bucket of sealed lists. diff --git a/modules/mesh-grants/internal/grants/held.go b/modules/mesh-grants/internal/grants/held.go new file mode 100644 index 00000000..1b970744 --- /dev/null +++ b/modules/mesh-grants/internal/grants/held.go @@ -0,0 +1,120 @@ +package grants + +import ( + "fmt" + "sort" + "strings" + + "git.novox.be/novox/mesh-sdk/go/asks" +) + +// **A module whose manifest waits or is refused keeps its bus users** (clause 3 of the record on a release's Approve; issue +// 774). The indexer holds a manifest it refuses, or that waits for a judge that knows a field of it, out of the manifests +// the part composes from (inputs.go, Refused), so a module with no want naming a build had no manifest to derive its users +// from and its users left the list: the bus module's accounts among them, which locked its tools out. Now such a module's +// users, the ones the list in force carries, stay in the list as it holds them, the same way a refused principal's do +// (refused.go), and the part says so as the warning mesh-grants.module-held until the manifest is accepted. One module's +// manifest never holds the list: every other principal is composed as ever. A module whose want names a build is composed +// from the manifest of that build (Inputs.Running), not kept. + +// HeldModuleCondition is the key of the warning raised while a module's manifest is held and its users are kept. +const HeldModuleCondition = "mesh-grants.module-held" + +// heldModuleUsers are, by user, why it is kept: the users of candidates (the ones the list in force and the bus carry) that +// belong to a module placed on a node whose manifest the indexer holds (in.Refused) and whose want names no build. A user of +// a module on a node is `.` or `..` (buscompose.Principal.Username). Pure. +func heldModuleUsers(in Inputs, candidates []string) map[string]string { + held := map[string]string{} + for _, g := range in.Refused { + if module, ok := strings.CutPrefix(g.Input, "manifest of "); ok { + held[module] = g.Why + } + } + out := map[string]string{} + if len(held) == 0 { + return out + } + for node, modules := range in.Assigned { + for _, module := range modules { + why, ok := held[module] + if !ok { + continue + } + if _, runs := in.Running[node+"."+module]; runs { + continue + } + prefix := node + "." + module + for _, user := range candidates { + if user == prefix || strings.HasPrefix(user, prefix+".") { + out[user] = fmt.Sprintf("the manifest of %s is held: %s", module, why) + } + } + } + } + return out +} + +// inForceGrants are the grants of each user of the list in force as the part sealed them, or nil where it kept none. +func (p *Part) inForceGrants() map[string]User { + if g, _, had := p.Sealed(); had && g.Digest != "" && p.Store != nil { + inForce, _ := p.sealedGrantsOf(g.Digest) + return inForce + } + return nil +} + +// mergedRefusals is refused and held in one map, for the parts that keep a user's grants of the list in force. +func mergedRefusals(refused, held map[string]string) map[string]string { + if len(held) == 0 { + return refused + } + out := make(map[string]string, len(refused)+len(held)) + for u, why := range held { + out[u] = why + } + for u, why := range refused { + out[u] = why + } + return out +} + +// sayHeldModules raises HeldModuleCondition while a held module's users are kept, naming each user with what became of it, +// and clears it once none is; said once per set, again when it changes, and at the first pass after a start. +func (p *Part) sayHeldModules(held map[string]string, kept map[string]User) { + if p.Say == nil { + return + } + var lines []string + users := make([]string, 0, len(held)) + for u := range held { + users = append(users, u) + } + sort.Strings(users) + for _, user := range users { + became := "not in the list in force and left out" + if _, ok := kept[user]; ok { + became = "stays in the list as the list in force holds it" + } + lines = append(lines, user+" ("+became+"): "+held[user]) + } + why := strings.Join(lines, "; ") + p.holdMu.Lock() + defer p.holdMu.Unlock() + if p.heldModulesKnown && why == p.heldModulesSaid { + return + } + c := asks.Condition{Key: HeldModuleCondition, Subject: asks.Scope{Scope: "module", ID: "mesh-grants"}} + if why == "" { + c.State = asks.StateCleared + } else { + c.Severity, c.State = asks.SeverityWarning, asks.StateRaised + c.Summary = fmt.Sprintf("%d bus user(s) of a module whose manifest is held are kept in the list: %s", len(users), why) + c.Headline, c.Needs = "A module's manifest is held, its bus users are kept", "a manifest the indexer accepts, or a judge that knows its fields" + c.Explanation = asks.NeedsYou + "a manifest the indexer accepts (a refused one mended, or a waiting one judged by an indexer that knows its fields). " + + "The rest of the user list is written as ever; a held module's users stay as the list in force holds them: " + why + c.Resolved = "every module's manifest is accepted" + } + if p.Say(c) == nil { + p.heldModulesSaid, p.heldModulesKnown = why, true + } +} diff --git a/modules/mesh-grants/internal/grants/inputs.go b/modules/mesh-grants/internal/grants/inputs.go index 035ffb21..64f36d08 100644 --- a/modules/mesh-grants/internal/grants/inputs.go +++ b/modules/mesh-grants/internal/grants/inputs.go @@ -353,8 +353,8 @@ func ReadInputs(r Reader, cache *Manifests) Inputs { } var m catalogue.Manifest if err == nil { - // The indexer's refusal, carried in its value (ADR 0578 §3): held and named as a refused manifest. - if verdict := v.VerdictOf(module); verdict != nil && verdict.Refused() { + // The indexer's refusal, or its wait for a judge that knows a field, carried in its value (ADR 0578 §3): held and named as a refused manifest. + if verdict := v.VerdictOf(module); verdict != nil && verdict.Held() { err = errors.New(verdict.Why()) } } @@ -551,14 +551,23 @@ func sortGaps(g []Gap) { sort.SliceStable(g, func(i, j int) bool { return strings.Compare(g[i].Input, g[j].Input) < 0 }) } -// ReadFailures names every read this pass could not make and every manifest refused: what a list composed now may lack. -func (in Inputs) ReadFailures() []string { +// ReadFailed names every read this pass could not make: what a list composed now may lack. A manifest the indexer holds +// (refused, or waiting for a judge that knows a field of it) is not one: it holds its module's users in the list as the +// list in force has them (held.go), and never holds the list, so one manifest never freezes the bus's users (issue 774). +func (in Inputs) ReadFailed() []string { var out []string for _, g := range in.Gaps { if g.Failed { out = append(out, g.Input+": "+g.Why) } } + return out +} + +// ReadFailures is ReadFailed and every manifest refused or waiting: what the part retires nothing on (the accounts it +// keeps, the hearings it withholds). +func (in Inputs) ReadFailures() []string { + out := in.ReadFailed() for _, g := range in.Refused { out = append(out, g.Input+": "+g.Why) } diff --git a/modules/mesh-grants/internal/grants/part.go b/modules/mesh-grants/internal/grants/part.go index 29b8ffe4..bb0a8b30 100644 --- a/modules/mesh-grants/internal/grants/part.go +++ b/modules/mesh-grants/internal/grants/part.go @@ -189,6 +189,9 @@ type Part struct { // refusedSaid is the refused principals last said as a condition, once refusedKnown (refused.go). refusedSaid string refusedKnown bool + // heldModulesSaid is the held modules' kept users last said as a condition, once heldModulesKnown (held.go). + heldModulesSaid string + heldModulesKnown bool // missingSaid is the modules without a grant last said as a condition, once missingKnown (grants.go). missingSaid string missingKnown bool @@ -259,14 +262,24 @@ func (p *Part) Compose() Result { res.Errors = append(res.Errors, "the shadow list could not be composed: "+err.Error()) } // A refused principal keeps its grants of the list in force, said once per change (refused.go). - kept, _ := p.keptOf(refused) + // And a module whose manifest is held (refused, or waiting for a judge that knows a field of it) and whose want names no + // build keeps its users of the list in force (held.go). + loaded, loadErr := LoadedUsers(p.Call) + inForce := p.inForceGrants() + candidates := names(loaded) + for user := range inForce { + candidates = append(candidates, user) + } + heldUsers := heldModuleUsers(in, candidates) + allRefused := mergedRefusals(refused, heldUsers) + kept, _ := p.keptOf(allRefused) for _, user := range sortedKeys(refused) { res.PrincipalsRefused = append(res.PrincipalsRefused, user+": "+refused[user]) } p.sayPrincipalsRefused(refused, kept) + p.sayHeldModules(heldUsers, kept) users = WithHashes(withKept(users, kept), in.Hashes) st.Digest, st.Users = Digest(users), len(users) - loaded, loadErr := LoadedUsers(p.Call) st.Compare = Compare(users, loaded, Placed(in)).Excusing(ExcusedBy(MultiHeld(listRecs))) if loadErr != nil { st.Compare.Unread = loadErr.Error() @@ -298,7 +311,7 @@ func (p *Part) Compose() Result { if p.Store != nil { p.writeWithheld(&res, in, recs) } - list, n, leftOut, dropped, err := ComposeList(listRecs, in.Hashes, refused, kept) + list, n, leftOut, dropped, err := ComposeList(listRecs, in.Hashes, allRefused, kept) // The grants the assigner reads (ADR 0547 §4, §5): one that adds a permission only once the bus loaded a list that // carries it (issue 774). if p.Store != nil { diff --git a/modules/mesh-grants/internal/grants/sealed.go b/modules/mesh-grants/internal/grants/sealed.go index 9b83190f..376900e1 100644 --- a/modules/mesh-grants/internal/grants/sealed.go +++ b/modules/mesh-grants/internal/grants/sealed.go @@ -299,7 +299,7 @@ func listUsers(list []byte) []string { // they could not be read). Empty when it may. func (p *Part) holdsBack(in Inputs, leftOut, dropped, bus, lost []string, digest string) string { p.hashHold = nil - if f := in.ReadFailures(); len(f) > 0 { + if f := in.ReadFailed(); len(f) > 0 { return "held: the list in force is kept while a read of this pass failed (" + strings.Join(f, "; ") + ")" } if bus == nil { From 78fe1ce960b3b2782cfb8e48690b3c32bdbd9196 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 13:03:11 +0100 Subject: [PATCH 02/49] Review round 1: the indexer learns its build from the node-engine's seat; a held module's seats and hearings come from its running manifest; indexer_judge; one judge pass at a time (issue 774) --- modules/mesh-delivery-assigner/go.mod | 2 +- modules/mesh-delivery-assigner/go.sum | 4 +- modules/mesh-delivery-builder/go.mod | 2 +- modules/mesh-delivery-builder/go.sum | 4 +- modules/mesh-delivery-checker/go.mod | 2 +- modules/mesh-delivery-checker/go.sum | 4 +- modules/mesh-delivery-forge-bridge/go.mod | 2 +- modules/mesh-delivery-forge-bridge/go.sum | 4 +- .../cmd/mesh-delivery-indexer-tools/main.go | 6 +- .../cmd/mesh-delivery-indexer/main.go | 3 +- .../cmd/mesh-delivery-indexer/named_test.go | 13 ++++ .../cmd/mesh-delivery-indexer/ownbuild.go | 37 +++++++++++ .../internal/index/candidates.go | 39 ++++++------ .../internal/index/verdicts.go | 47 +++++++++----- .../internal/index/verdicts_test.go | 19 +++++- .../mesh-delivery-indexer/manifest_test.go | 6 +- modules/mesh-delivery-indexer/module.json | 5 +- modules/mesh-delivery-teller/go.mod | 2 +- modules/mesh-delivery-teller/go.sum | 4 +- modules/mesh-delivery-view/go.mod | 2 +- modules/mesh-delivery-view/go.sum | 4 +- modules/mesh-delivery-watchman/go.mod | 2 +- modules/mesh-delivery-watchman/go.sum | 4 +- modules/mesh-enrolment/go.mod | 2 +- modules/mesh-enrolment/go.sum | 4 +- modules/mesh-grants/grants_test.go | 47 ++++++++++++++ modules/mesh-grants/internal/grants/inputs.go | 63 ++++++++++++++++++- .../mesh-grants/internal/grants/records.go | 7 ++- 28 files changed, 267 insertions(+), 73 deletions(-) create mode 100644 modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/ownbuild.go diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index 39795ea8..c9e96c42 100644 --- a/modules/mesh-delivery-assigner/go.mod +++ b/modules/mesh-delivery-assigner/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-assigner go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-assigner/go.sum b/modules/mesh-delivery-assigner/go.sum index 6de5762d..21738611 100644 --- a/modules/mesh-delivery-assigner/go.sum +++ b/modules/mesh-delivery-assigner/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/go.mod b/modules/mesh-delivery-builder/go.mod index 44661726..0f3ceef0 100644 --- a/modules/mesh-delivery-builder/go.mod +++ b/modules/mesh-delivery-builder/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-builder go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/go.sum b/modules/mesh-delivery-builder/go.sum index 9f9f382b..08770b61 100644 --- a/modules/mesh-delivery-builder/go.sum +++ b/modules/mesh-delivery-builder/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index 3e680509..d74c83c7 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-checker go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-checker/go.sum b/modules/mesh-delivery-checker/go.sum index 9f9f382b..08770b61 100644 --- a/modules/mesh-delivery-checker/go.sum +++ b/modules/mesh-delivery-checker/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-forge-bridge/go.mod b/modules/mesh-delivery-forge-bridge/go.mod index f9796036..60ad48c5 100644 --- a/modules/mesh-delivery-forge-bridge/go.mod +++ b/modules/mesh-delivery-forge-bridge/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-forge-bridge go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-forge-bridge/go.sum b/modules/mesh-delivery-forge-bridge/go.sum index 6de5762d..21738611 100644 --- a/modules/mesh-delivery-forge-bridge/go.sum +++ b/modules/mesh-delivery-forge-bridge/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go index a439683a..41bc0f1d 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go @@ -1,6 +1,6 @@ // Command mesh-delivery-indexer-tools serves the indexer's doing tool (novox/hq issue 1945): the runtime launches it for a // call. indexer_take_in writes one value of the indexer's takeins state and waits for the indexer's answer there. The -// indexer is the other program: it serves no request subject (ADR 0437 §4); indexer_judge_again asks the same way, and +// indexer is the other program: it serves no request subject (ADR 0437 §4); indexer_judge asks the same way, and // indexer_verdicts reads the indexer's verdicts state. The read tools of the indexer's states are // mesh-delivery-view's (indexer_module, indexer_modules, indexer_candidate, indexer_candidates). package main @@ -69,7 +69,7 @@ func Tools(ask asker, now func() time.Time, read stdio.KeptState, counts func() "unknown, and every reader holds the module as a refused one and keeps what runs; refused: the judge's problems), the " + "judge that made it and when. A module's name narrows it to that module. A waiting manifest is judged again at each " + "new indexer and refused only once the newest build of the indexer on the trunk still does not know the field; " + - "indexer_judge_again judges now.", + "indexer_judge judges now.", Input: map[string]any{"type": "object", "properties": map[string]any{ "module": map[string]any{"type": "string", "description": "only this module's verdicts"}, "all": map[string]any{"type": "boolean", "description": "list accepted verdicts too"}}}, @@ -178,7 +178,7 @@ func heldCounts(s stdio.KeptState) func() (index.OldSectionsCount, error) { } func main() { - if err := stdio.Serve("", Tools(askTakeIn(stdio.State(index.TakeInsState), 25*time.Second), time.Now, stdio.State("verdicts"), heldCounts(stdio.State(index.OldSectionsState)))); err != nil { + if err := stdio.Serve("", Tools(askTakeIn(stdio.State(index.TakeInsState), 25*time.Second), time.Now, stdio.State(index.VerdictsState), heldCounts(stdio.State(index.OldSectionsState)))); err != nil { fmt.Fprintf(os.Stderr, "[mesh-delivery-indexer-tools] %v\n", err) os.Exit(1) } diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go index baa6807b..bbb31c68 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go @@ -303,7 +303,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro Verdicts: awaitedStore{verdicts}, SayRefused: sayRefused(js, cred, logger.Printf), SayAccepted: sayAccepted(js, cred, logger.Printf), Predefined: awaitedStore{predefined}, ConfigCandidates: awaitedStore{configCandidates}, Logf: logger.Printf, Named: namedVia(calls.Call), Trunk: trunkVia(calls.Call), Contains: containsVia(calls.Call), Read: readVia(calls.Call), - ReadOpen: openVia(calls.Call), Packages: awaitedStore{packages}, Files: filesVia(calls.Call), GoSum: goSumVia(calls.Call), GoMod: goModVia(calls.Call)} + ReadOpen: openVia(calls.Call), Packages: awaitedStore{packages}, Files: filesVia(calls.Call), GoSum: goSumVia(calls.Call), GoMod: goModVia(calls.Call), + OwnBuild: ownBuildVia(calls.Call, cred.Node)} // The log's entries at the start are read before any fingerprint is computed or registered module indexed (below); each // new one indexes the module it names when that module is registered and the bucket holds none (registered.go). var started atomic.Bool diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named_test.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named_test.go index b3040ed6..8db30456 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named_test.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named_test.go @@ -50,6 +50,19 @@ func TestTheTrunkIsTheDirectoriesTheForgeListsTrees(t *testing.T) { // 1900/AC3 if _, has := gotArgs.(map[string]any)["paths"]; has { t.Error("the trunk asked for paths") } + // The build the indexer runs is the engine's word (Runs.FP of its composition verb), asked of its own machine. + var asked string + own := func(key string, body any) (json.RawMessage, error) { + asked = key + return json.RawMessage(`{"runs":{"fp":"src2:abc"}}`), nil + } + if fp, err := ownBuildVia(own, "n1")(context.Background()); err != nil || fp != "src2:abc" || asked != "seat:node-engine.composition@n1" { + t.Errorf("own build = %q %v asked %q", fp, err, asked) + } + none := func(string, any) (json.RawMessage, error) { return json.RawMessage(`{}`), nil } + if _, err := ownBuildVia(none, "n1")(context.Background()); err == nil { + t.Error("an engine that says nothing runs gave a build") + } } func TestReadViaReadsManifestsAndLocks(t *testing.T) { diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/ownbuild.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/ownbuild.go new file mode 100644 index 00000000..79a31448 --- /dev/null +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/ownbuild.go @@ -0,0 +1,37 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// compositionVerb is the node-engine seat's read verb that says what runs of a module on a machine (its Runs.FP), asked of +// that machine's own engine as `seat:node-engine.composition@`. The indexer learns the build it runs from it, never +// from the trunk's value of itself or the haves bucket. +const compositionVerb = "seat:node-engine.composition" + +// ownBuildVia asks the engine of node which fingerprint of the indexer runs there. +func ownBuildVia(call toolCaller, node string) func(ctx context.Context) (string, error) { + return func(context.Context) (string, error) { + raw, err := call(compositionVerb+"@"+node, map[string]any{"module": delivery.IndexerModule}) + if err != nil { + return "", fmt.Errorf("%s: %w", compositionVerb, err) + } + var a struct { + Runs *struct { + FP string `json:"fp"` + } `json:"runs"` + } + if err := json.Unmarshal(raw, &a); err != nil { + return "", fmt.Errorf("the answer of %s is not a composition: %w", compositionVerb, err) + } + if a.Runs == nil || a.Runs.FP == "" { + return "", errors.New("the engine says no fingerprint of the indexer runs") + } + return a.Runs.FP, nil + } +} diff --git a/modules/mesh-delivery-indexer/internal/index/candidates.go b/modules/mesh-delivery-indexer/internal/index/candidates.go index 84574816..2e207599 100644 --- a/modules/mesh-delivery-indexer/internal/index/candidates.go +++ b/modules/mesh-delivery-indexer/internal/index/candidates.go @@ -98,24 +98,27 @@ type Indexer struct { // closed message the indexer missed heals at the next one (issue 1126); nil asks none. ReadOpen func(ctx context.Context) ([]delivery.OpenPull, error) - mu sync.Mutex // guards source, toolchains and previews, which the watches and the forge messages all reach - source sourceNow - previews map[string]delivery.Preview // by previews key (previews.go) - toolchains map[string]map[string]delivery.Toolchain // language, then node (toolchains.go) - tried map[string]bool // repository, commit and directory a registered module's manifest was tried at (registered.go) - unread map[string]bool // registered modules whose last read the forge could not answer, read again by RetryUnread - moving map[string]delivery.Forge // by modules key: a merge naming a module another repository holds, waiting for that trunk to drop it (moves.go) - retrying atomic.Bool // a RetryUnread runs in the background (kickRetry) - retryAgain atomic.Bool // an event asked for one more RetryUnread after the running one - retryWG sync.WaitGroup // the background RetryUnread, for tests to wait on - recompute sync.Mutex // one Recompute at a time - untaken map[string]string // repository to the latest merge applied while the taken state was missing (taken.go) - filling atomic.Bool // a FillTaken runs in the background (KickFill) - fillAgain atomic.Bool // a kick arrived while it ran - atHeads map[string]map[string]delivery.PublishedPackages // a member head's published Go modules, by repository@head (candidatePublished) - ownMu sync.Mutex // guards ownFP - ownFP string // the indexer's own fingerprint its modules value named at the start pass (ownBuildIsNewest) - judgePending atomic.Bool // the start's judge pass failed: judged again at the next message (verdicts.go) + mu sync.Mutex // guards source, toolchains and previews, which the watches and the forge messages all reach + source sourceNow + previews map[string]delivery.Preview // by previews key (previews.go) + toolchains map[string]map[string]delivery.Toolchain // language, then node (toolchains.go) + tried map[string]bool // repository, commit and directory a registered module's manifest was tried at (registered.go) + unread map[string]bool // registered modules whose last read the forge could not answer, read again by RetryUnread + moving map[string]delivery.Forge // by modules key: a merge naming a module another repository holds, waiting for that trunk to drop it (moves.go) + retrying atomic.Bool // a RetryUnread runs in the background (kickRetry) + retryAgain atomic.Bool // an event asked for one more RetryUnread after the running one + retryWG sync.WaitGroup // the background RetryUnread, for tests to wait on + recompute sync.Mutex // one Recompute at a time + untaken map[string]string // repository to the latest merge applied while the taken state was missing (taken.go) + filling atomic.Bool // a FillTaken runs in the background (KickFill) + fillAgain atomic.Bool // a kick arrived while it ran + atHeads map[string]map[string]delivery.PublishedPackages // a member head's published Go modules, by repository@head (candidatePublished) + // OwnBuild answers the fingerprint of the build this program runs, from the node-engine's seat; nil knows none. + OwnBuild func(ctx context.Context) (string, error) + judgeMu sync.Mutex // one judge pass at a time (verdicts.go) + ownMu sync.Mutex // guards ownFP + ownFP string // the fingerprint of the build this program runs, asked at the start pass (learnOwnBuild) + judgePending atomic.Bool // the start's judge pass failed: judged again at the next message (verdicts.go) } func (ix *Indexer) logf(format string, args ...any) { diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts.go b/modules/mesh-delivery-indexer/internal/index/verdicts.go index 48b1af19..6b812027 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts.go @@ -24,9 +24,11 @@ import ( // changed (a new indexer) judges every manifest again, and after every merge it takes in, since a rule between manifests // can turn on any of them. Nothing polls. -// VerdictsBucket is the indexer's state of verdicts. +// VerdictsBucket is the indexer's state of verdicts; VerdictsState is its local name, as the tools bundle reads it. var VerdictsBucket = delivery.VerdictsBucket +const VerdictsState = "verdicts" + // Judged is what one pass of the judge did: the manifests judged, the verdicts written, and the modules refused. type Judged struct { Judged, Written int @@ -47,6 +49,10 @@ type Judged struct { // watchman raises as manifest-outgrown. A manifest in fresh (the digests a merge just took in) with no verdict yet, and // one already refused, is judged in full. func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged, error) { + // A merge, the start pass and indexer_judge may all judge at once: one pass at a time, so none writes a verdict from + // modules another is rewriting. + ix.judgeMu.Lock() + defer ix.judgeMu.Unlock() var out Judged if ix.Verdicts == nil || ix.Modules == nil || ix.Manifests == nil { return out, nil @@ -167,11 +173,7 @@ func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged, // JudgeAtStart is JudgeAll of every manifest held, none fresh, as the indexer starts (a judge that changed judges every // one again); one that fails is judged again at the next message or source revision heard (kickJudge). func (ix *Indexer) JudgeAtStart(ctx context.Context) (Judged, error) { - if held, err := ix.heldModules(ctx); err == nil { - ix.ownMu.Lock() - ix.ownFP = held[delivery.IndexerModule].FP2 // this program's build, until the trunk names another - ix.ownMu.Unlock() - } + ix.learnOwnBuild(ctx) j, err := ix.JudgeAll(ctx, nil) ix.judgePending.Store(err != nil) return j, err @@ -224,11 +226,26 @@ func (ix *Indexer) writeVerdict(ctx context.Context, digest string, v delivery.M return wrote, err } -// ownBuildIsNewest says whether the build of the indexer this program is, is the newest the trunk holds: nothing gives a -// module the fingerprint it was built from, so it is the one the indexer's own modules value named when this program -// judged at its start (ownFP), and the trunk holds no newer one while that value still names it. A newer build of the -// indexer on the trunk, as one that pins a changed mesh-sdk always is, ends it at the trunk's merge, before it runs; false -// when the value names none or this program judged no start pass. +// learnOwnBuild asks where the indexer's program is told which build it runs (OwnBuild: the node-engine's seat, never the +// trunk's value of the indexer, which names a build that may be merged and not delivered). Asked at the start pass; where +// it is not answered the build stays unknown and every unknown field waits, which is the safe side. +func (ix *Indexer) learnOwnBuild(ctx context.Context) { + if ix.OwnBuild == nil { + return + } + fp, err := ix.OwnBuild(ctx) + if err != nil || fp == "" { + ix.logf("judge: the build this program runs is not known (%v), so a manifest with a field it does not know waits and is not refused", err) + return + } + ix.ownMu.Lock() + ix.ownFP = fp + ix.ownMu.Unlock() +} + +// ownBuildIsNewest says whether the build of the indexer this program runs (ownFP, from the node-engine) is the newest the +// trunk holds: the indexer's own modules value names it. A newer build merged and not yet delivered ends it at the trunk's +// merge, as one that pins a changed mesh-sdk always is; false when either is unknown. func (ix *Indexer) ownBuildIsNewest(held map[string]delivery.Module) bool { fp := held[delivery.IndexerModule].FP2 if fp == "" { @@ -298,10 +315,10 @@ func (ix *Indexer) predefinedList(ctx context.Context) catalogue.PredefinedList } // The tools that read and steer the verdicts (the instruction "tools are how a module is spoken to"): indexer_verdicts -// lists the manifests that wait or are refused, and indexer_judge_again judges one module again now. +// lists the manifests that wait or are refused, and indexer_judge judges one module again now. const ( VerdictsTool = "indexer_verdicts" - JudgeAgainTool = "indexer_judge_again" + JudgeAgainTool = "indexer_judge" judgeAgainPrefix = "judge." ) @@ -352,7 +369,7 @@ func ListVerdicts(keys []string, get func(key string) ([]byte, error), module st } // JudgeAgain judges the manifests again now, as at the start, and answers how the module's stands: the tool -// indexer_judge_again. A module the modules state does not hold, or whose manifest is not in the manifests state, is +// indexer_judge. A module the modules state does not hold, or whose manifest is not in the manifests state, is // answered so. func (ix *Indexer) JudgeAgain(ctx context.Context, module string) TakenIn { out := TakenIn{Module: module} @@ -388,7 +405,7 @@ func (ix *Indexer) JudgeAgain(ctx context.Context, module string) TakenIn { return out } -// NewJudgeAgain is the value indexer_judge_again writes for a module: its key in the takeins state and the value. +// NewJudgeAgain is the value indexer_judge writes for a module: its key in the takeins state and the value. func NewJudgeAgain(module, why, by string, at time.Time) (string, []byte, error) { key, data, err := NewTakeIn(module, why, by, at) if err != nil { diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go index 8d08a033..24dab79a 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go @@ -216,6 +216,19 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { !strings.Contains(m.Problems[0], "does not know yet") { t.Fatalf("modules.later carries %+v", m) } + // The build this program runs is asked of the engine, and the trunk moving before the start pass changes nothing: the + // trunk's value of the indexer names a newer build than the one the engine says runs, so the field still waits. + ix.ownFP = "" + ix.OwnBuild = func(context.Context) (string, error) { return ownFP, nil } + if _, err := ix.JudgeAtStart(ctx); err != nil || !verdict(later).Waits() { + t.Fatalf("the trunk moved before the start pass: %v %+v", err, verdict(later)) + } + ix.OwnBuild = func(context.Context) (string, error) { return "", errors.New("the engine does not answer") } + ix.ownFP = "" + if _, err := ix.JudgeAtStart(ctx); err != nil || !verdict(later).Waits() { + t.Fatalf("an unknown build refused: %v %+v", err, verdict(later)) + } + ix.ownFP = ownFP // A renamed field is a known mistake: refused at once. renamed := hold("renamed", `{"module":"renamed","version":"1","needs":{"x":"y"}}`) if j, err := ix.JudgeAll(ctx, map[string]bool{renamed: true}); err != nil || len(j.Waiting) != 1 || !verdict(renamed).Refused() { @@ -229,7 +242,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { if m := module("later"); m.Verdict != delivery.VerdictRefused || m.VerdictOf("later").Waits() || len(m.Unknown) != 0 { t.Fatalf("modules.later after the refusal carries %+v", m) } - // The tools: indexer_verdicts lists the verdicts that wait or are refused (and every one with all), indexer_judge_again + // The tools: indexer_verdicts lists the verdicts that wait or are refused (and every one with all), indexer_judge // judges now and says how the module's verdict stands. keys, _ := ix.Verdicts.Keys(ctx) get := func(key string) ([]byte, error) { v, _, _, err := ix.Verdicts.Get(ctx, key); return v, err } @@ -241,13 +254,13 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { } ix.ownFP = ownFP if got := ix.JudgeAgain(ctx, "later"); !got.Stored || !strings.Contains(got.Answer, "waits") || !strings.Contains(got.Answer, "a-field-from-later") { - t.Fatalf("indexer_judge_again: %+v", got) + t.Fatalf("indexer_judge: %+v", got) } if rows, _ := ListVerdicts(keys, get, "later", false); len(rows) != 1 { t.Fatalf("indexer_verdicts of later after: %+v", rows) } if got := ix.JudgeAgain(ctx, "nobody"); got.Stored || !strings.Contains(got.Answer, "holds no manifest") { - t.Fatalf("indexer_judge_again of nobody: %+v", got) + t.Fatalf("indexer_judge of nobody: %+v", got) } if key, value, err := NewJudgeAgain("later", "why", "me", time.Now()); err != nil || key != "judge.later" { t.Fatalf("the ask: %q %v", key, err) diff --git a/modules/mesh-delivery-indexer/manifest_test.go b/modules/mesh-delivery-indexer/manifest_test.go index f359f14f..0c6c61f5 100644 --- a/modules/mesh-delivery-indexer/manifest_test.go +++ b/modules/mesh-delivery-indexer/manifest_test.go @@ -110,13 +110,13 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build // Three invokes: the file of a registered module's manifest and lock files (registered.go), the forge's trees (the named paths' objects, since the indexer is the one reader of a manifest's paths: // ADR 0499 clauses 2 and 3, ADR 0495 clause 1; and the module directories of the trunk, issue 1900) and whether a // commit is in another (issue 1900). - if want := []string{"gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", "gitea.gitea_open_pulls"}; !reflect.DeepEqual(m.Invokes, want) { + if want := []string{"gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", "gitea.gitea_open_pulls", "seat:node-engine.composition"}; !reflect.DeepEqual(m.Invokes, want) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } // 1945: indexer_take_in, served by its tools bundle through the takeins state; the process serves none. The same bundle // reads the verdicts state (indexer_verdicts) and asks the indexer to judge again through the takeins state - // (indexer_judge_again; issue 774, clause 3). 1966/AC11: indexer_old_sections reads the count of the old sections (state oldsections). - if want := []string{"indexer_take_in", "indexer_old_sections", "indexer_verdicts", "indexer_judge_again"}; !reflect.DeepEqual(m.Tools, want) { + // (indexer_judge; issue 774, clause 3). 1966/AC11: indexer_old_sections reads the count of the old sections (state oldsections). + if want := []string{"indexer_take_in", "indexer_old_sections", "indexer_verdicts", "indexer_judge"}; !reflect.DeepEqual(m.Tools, want) { t.Errorf("tools = %v, want %v", m.Tools, want) } // No streams, no bus, no seat (ADR 0437 §4); it says no event. diff --git a/modules/mesh-delivery-indexer/module.json b/modules/mesh-delivery-indexer/module.json index a876e3c3..45067500 100644 --- a/modules/mesh-delivery-indexer/module.json +++ b/modules/mesh-delivery-indexer/module.json @@ -6,7 +6,7 @@ "indexer_take_in", "indexer_old_sections", "indexer_verdicts", - "indexer_judge_again" + "indexer_judge" ], "consumes": [ "mesh-build-go.toolchain.reported", @@ -43,7 +43,8 @@ "gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", - "gitea.gitea_open_pulls" + "gitea.gitea_open_pulls", + "seat:node-engine.composition" ], "own-secrets": { "broker": "${dir:state}/broker" diff --git a/modules/mesh-delivery-teller/go.mod b/modules/mesh-delivery-teller/go.mod index 721c8ed5..821d3f97 100644 --- a/modules/mesh-delivery-teller/go.mod +++ b/modules/mesh-delivery-teller/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-teller go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-teller/go.sum b/modules/mesh-delivery-teller/go.sum index 6de5762d..21738611 100644 --- a/modules/mesh-delivery-teller/go.sum +++ b/modules/mesh-delivery-teller/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index 499bb922..cf0fb7c4 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-view go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index 6de5762d..21738611 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-watchman/go.mod b/modules/mesh-delivery-watchman/go.mod index bd9b3bf2..66433c0d 100644 --- a/modules/mesh-delivery-watchman/go.mod +++ b/modules/mesh-delivery-watchman/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-watchman go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-watchman/go.sum b/modules/mesh-delivery-watchman/go.sum index 6e5f2993..1c98fd23 100644 --- a/modules/mesh-delivery-watchman/go.sum +++ b/modules/mesh-delivery-watchman/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-enrolment/go.mod b/modules/mesh-enrolment/go.mod index 3ea0d929..678fdaa3 100644 --- a/modules/mesh-enrolment/go.mod +++ b/modules/mesh-enrolment/go.mod @@ -3,7 +3,7 @@ module mesh-enrolment go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-enrolment/go.sum b/modules/mesh-enrolment/go.sum index 6de5762d..21738611 100644 --- a/modules/mesh-enrolment/go.sum +++ b/modules/mesh-enrolment/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 816052a4..c00d43ac 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -739,6 +739,53 @@ func TestGrants(t *testing.T) { } }) + // ADR 0601 clause 3: a held module's seats, hearings and reads are composed from the manifest its running build was judged + // on, so a principal that claims a seat it defines keeps its grants while its trunk manifest is refused or waits. + t.Run("a held module's seat is composed from the manifest its running build was judged on", func(t *testing.T) { + const router = `{"module":"messenger","version":"1","runs-as":"messenger","resources":[{"id":"account","type":"user","name":"messenger","shell":"/usr/bin/nologin","home":"/var/lib/messenger"}],"own-secrets":{"broker":"/var/lib/m/broker"},"secrets-owner":"messenger",` + + `"seats":[{"name":"operator-channel","scope":"mesh","serves":["open"]},{"name":"channel","scope":"mesh","kinded":true,"accepts":["show"]}],` + + `"claims":[{"name":"operator-channel","scope":"mesh","serves":["open"]}]}` + const telegram = `{"module":"telegram","version":"1","runs-as":"telegram","resources":[{"id":"account","type":"user","name":"telegram","shell":"/usr/bin/nologin","home":"/var/lib/telegram"}],"own-secrets":{"broker":"/var/lib/t/broker"},"secrets-owner":"telegram",` + + `"claims":[{"name":"channel","scope":"mesh","kind":"telegram","capabilities":["deliver","verified-sender"]}]}` + setup := func(held, runs bool) *fakeBus { + b := fixture() + b.manifestOf("messenger", router) + b.manifestOf("telegram", telegram) + b.assign("a", "messenger") + b.assign("b", "telegram") + if held { + // The trunk's manifest waits for a field; the build that runs was judged on the manifest before. + b.put(grants.ReadModules, "messenger", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32), + Manifest: delivery.ManifestDigest([]byte(`{"module":"messenger","version":"2"}`)), Repository: "novox/x", Commit: "c", + Verdict: delivery.VerdictRefused, Problems: []string{"it waits"}, Unknown: []string{"core-credentials"}}) + if runs { + fp := "src2:" + strings.Repeat("cd", 32) + digest := delivery.ManifestDigest([]byte(router)) + b.put(grants.ReadManifests, digest, delivery.Manifest{V: delivery.Version, Module: "messenger", JSON: router}) + b.put(grants.ReadHave, "a.messenger", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, FP: fp}) + key, _ := delivery.BuilderBuildKey("messenger", fp) + b.put(grants.ReadBuilds, key, map[string]any{"v": delivery.Version, "state": "done", "manifest": digest}) + } + } + return b + } + grantsOf := func(b *fakeBus) string { + for _, u := range shadowOf(t, b) { + if u.User == "b.telegram" { + return strings.Join(u.Publish, ",") + "|" + strings.Join(u.Subscribe, ",") + } + } + return "" + } + accepted, held, unseen := grantsOf(setup(false, false)), grantsOf(setup(true, true)), grantsOf(setup(true, false)) + if accepted == "" || held != accepted { + t.Errorf("a caller of a held module's seat lost grants:\n%s\nwas\n%s", held, accepted) + } + if unseen == accepted { + t.Errorf("the row proves nothing: the grants are the same without the held module's running manifest") + } + }) + t.Run("root-free: each engine's word fills the composition, failing closed (issue 1945)", func(t *testing.T) { const router = `{"module":"messenger","version":"1","runs-as":"messenger","resources":[{"id":"account","type":"user","name":"messenger","shell":"/usr/bin/nologin","home":"/var/lib/messenger"}],"own-secrets":{"broker":"/var/lib/m/broker"},"secrets-owner":"messenger",` + `"seats":[{"name":"operator-channel","scope":"mesh","serves":["open"]},{"name":"channel","scope":"mesh","kinded":true,"accepts":["show"]}],` + diff --git a/modules/mesh-grants/internal/grants/inputs.go b/modules/mesh-grants/internal/grants/inputs.go index 64f36d08..029a37e5 100644 --- a/modules/mesh-grants/internal/grants/inputs.go +++ b/modules/mesh-grants/internal/grants/inputs.go @@ -131,7 +131,11 @@ type Inputs struct { // Refused is every manifest the indexer holds that the part cannot read or mesh-sdk's reader refuses, assigned or // not, each with why: its module counts as placed nowhere, so it is said loudly, never passed over (issue 1945). Refused []Gap - Gaps []Gap + // Held is, by module, the manifest a held module's running build was judged on (its have's fingerprint, the builder's + // build of it): what the seats it defines, the streams others hear from it and its reads are composed from while its + // trunk manifest is refused or waits, so no other principal loses a grant to it (ADR 0601 clause 3). + Held map[string]catalogue.Manifest + Gaps []Gap } // HeldHash is one key of the vault's bus-accounts: the hash, and what it is of. The hash is never said by the part, and @@ -440,6 +444,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs { sort.Strings(fromTrunk) in.Gaps = append(in.Gaps, Gap{Input: "wanted manifests", Why: "no build record names the manifest at the wanted fingerprint, so the trunk's is used: " + strings.Join(fromTrunk, ", ")}) } + in.Held = heldManifests(r, in, cache) in.Hashes = readHashes(r, &in.Gaps) // The seat mesh-enrolment's keys are every token since the controller's end (issue 1126). in.Enrolling, _ = readEnrolling(r, &in.Gaps) @@ -573,3 +578,59 @@ func (in Inputs) ReadFailures() []string { } return out } + +// heldManifests are the manifests the running builds of the held modules (in.Refused) were judged on, by module: the have +// of the module on a node names the fingerprint that runs, and the builder's build of it the manifest. A module that runs +// nowhere, or whose build or manifest cannot be read, has none, and that is said as a gap. +func heldManifests(r Reader, in Inputs, cache *Manifests) map[string]catalogue.Manifest { + out := map[string]catalogue.Manifest{} + for _, g := range in.Refused { + module, ok := strings.CutPrefix(g.Input, "manifest of ") + if !ok { + continue + } + for _, node := range in.Nodes { + raw, found, err := r.Get(ReadHave, node+"."+module) + if err != nil || !found { + continue + } + h, err := delivery.DecodeHave(raw) + if err != nil || h.FP == "" { + continue + } + bk, err := delivery.BuilderBuildKey(module, h.FP) + if err != nil { + continue + } + braw, found, err := r.Get(ReadBuilds, bk) + if err != nil || !found { + continue + } + b, err := delivery.DecodeBuilderBuild(braw) + if err != nil || b.Manifest == "" { + continue + } + if m, err := cache.get(r, b.Manifest); err == nil { + out[module] = m + break + } + } + } + return out +} + +// declared are the manifests the seats, hearings and reads are composed from: the trunk's, and for a held module the +// manifest its running build was judged on (Held). +func (in Inputs) declared() map[string]catalogue.Manifest { + if len(in.Held) == 0 { + return in.Manifests + } + out := make(map[string]catalogue.Manifest, len(in.Manifests)+len(in.Held)) + for m, man := range in.Held { + out[m] = man + } + for m, man := range in.Manifests { + out[m] = man + } + return out +} diff --git a/modules/mesh-grants/internal/grants/records.go b/modules/mesh-grants/internal/grants/records.go index bb74d475..28014fbd 100644 --- a/modules/mesh-grants/internal/grants/records.go +++ b/modules/mesh-grants/internal/grants/records.go @@ -27,7 +27,8 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) { var gaps []Gap seats := map[string]catalogue.SeatDeclaration{} declarers := map[string]string{} - for _, m := range in.Manifests { + declared := in.declared() + for _, m := range declared { for _, s := range m.DefinesSeats { seats[s.Name] = s declarers[s.Name] = m.Module @@ -79,7 +80,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) { for _, sr := range catalogue.ReadsGranted(m, here, node) { d.KeyedReads = append(d.KeyedReads, buscompose.KeyedRead{Read: sr.Read, Key: sr.Key}) } - d.Hears, d.HearsAStream, d.Consumes = hearsOf(m, in.Manifests, node, values, d.Consumes) + d.Hears, d.HearsAStream, d.Consumes = hearsOf(m, declared, node, values, d.Consumes) d.Reads, d.ReadsObjects, d.ReadsRefused = readsOfObjects(m.Module, node, d.Reads, d.ReadsRefused, in.Manifests, values) return d } @@ -101,7 +102,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) { } // The node-engine's own manifest, placed on every node (ADR 0563): its state, logs and streams alone, created by this // part; it adds no principal, since each node's engine speaks as the node's account. - if m, ok := in.Manifests[buscompose.EngineModule]; ok { + if m, ok := declared[buscompose.EngineModule]; ok { out.Engine = &buscompose.Declared{Module: m.Module, State: bucketsFor(m, in.Nodes), Logs: logsOf(m), Streams: streamsOf(m)} } return out, gaps From 6301e49c7326ab9482a61d683ccbcfa9fd818dfe Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 13:07:11 +0100 Subject: [PATCH 03/49] Review round 2: a held module's object reads come from its running manifest; one with none keeps the list in force, said; indexer_judge asks the engine for the running build again (issue 774) --- .../internal/index/verdicts.go | 2 ++ .../internal/index/verdicts_test.go | 8 +++++ modules/mesh-grants/grants_test.go | 33 +++++++++++++++++++ modules/mesh-grants/internal/grants/inputs.go | 16 +++++++++ .../mesh-grants/internal/grants/records.go | 2 +- 5 files changed, 60 insertions(+), 1 deletion(-) diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts.go b/modules/mesh-delivery-indexer/internal/index/verdicts.go index 6b812027..6d79ec5a 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts.go @@ -383,6 +383,8 @@ func (ix *Indexer) JudgeAgain(ctx context.Context, module string) TakenIn { out.Answer = "the indexer holds no manifest of " + module return out } + // The build this program runs is asked again, so a start pass that missed the engine's answer is recovered without a restart. + ix.learnOwnBuild(ctx) if _, err := ix.JudgeAll(ctx, nil); err != nil { out.Answer = "the manifests could not be judged: " + err.Error() return out diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go index 24dab79a..28acdca8 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go @@ -252,7 +252,15 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { if rows, _ := ListVerdicts(keys, get, "", true); len(rows) <= len(keys)-3 || rows[0].Module > rows[len(rows)-1].Module { t.Fatalf("indexer_verdicts, all: %d rows of %d verdicts", len(rows), len(keys)) } + // indexer_judge asks the engine for the running build again: a start pass that missed it is recovered (the trunk's value of + // the indexer is the newer one, so the field waits; asked and answered with that same build it is refused). + ix.ownFP = "" + ix.OwnBuild = func(context.Context) (string, error) { return "src2:" + strings.Repeat("a", 64), nil } + if got := ix.JudgeAgain(ctx, "later"); !got.Stored || !strings.Contains(got.Answer, "refused") || ix.ownFP == "" { + t.Fatalf("indexer_judge did not learn the running build: %+v", got) + } ix.ownFP = ownFP + ix.OwnBuild = nil if got := ix.JudgeAgain(ctx, "later"); !got.Stored || !strings.Contains(got.Answer, "waits") || !strings.Contains(got.Answer, "a-field-from-later") { t.Fatalf("indexer_judge: %+v", got) } diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index c00d43ac..17e8db89 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -680,6 +680,22 @@ func TestGrants(t *testing.T) { if slices.Contains(web.Publish, "$JS.API.DIRECT.GET.KV_mesh-knowledge_sections.>") { t.Error("the object bucket's read is granted as a state's") } + // ADR 0601 clause 3: the owner's trunk manifest now waits for a field; its running build's manifest still declares the + // bucket, so web keeps its object grant. + fp := "src2:" + strings.Repeat("cd", 32) + b.put(grants.ReadModules, "mesh-knowledge", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32), + Manifest: delivery.ManifestDigest([]byte(`{"module":"mesh-knowledge","version":"2"}`)), Repository: "novox/x", Commit: "c", + Verdict: delivery.VerdictRefused, Problems: []string{"it waits"}, Unknown: []string{"core-credentials"}}) + b.put(grants.ReadHave, "a.mesh-knowledge", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, FP: fp}) + heldKey, _ := delivery.BuilderBuildKey("mesh-knowledge", fp) + b.put(grants.ReadBuilds, heldKey, map[string]any{"v": delivery.Version, "state": "done", "manifest": delivery.ManifestDigest([]byte(knowledge))}) + if heldWeb := (func() grants.User { + us := shadowOf(t, b) + return us[slices.IndexFunc(us, func(u grants.User) bool { return u.User == "a.web" })] + })(); !slices.Contains(heldWeb.Publish, read) { + t.Errorf("a reader of a held module's object store lost its grant: %v", heldWeb.Publish) + } + b.manifestOf("mesh-knowledge", knowledge) // accepted again for the rest of the row // ADR 0592 §6, §25: the bucket past 75 percent of its cap is said, in one warning, and cleared once below; the history // stream at 10 percent is not. var said []asks.Condition @@ -2758,6 +2774,11 @@ func TestGrants(t *testing.T) { } b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 5, Key: busKey.PublicKey}}) + // mod-r runs a build whose manifest is the accepted one: the held module's seats and streams are composed from it. + runFP := "src2:" + strings.Repeat("cd", 32) + b.put(grants.ReadHave, "b.mod-r", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, FP: runFP}) + runKey, _ := delivery.BuilderBuildKey("mod-r", runFP) + b.put(grants.ReadBuilds, runKey, map[string]any{"v": delivery.Version, "state": "done", "manifest": delivery.ManifestDigest([]byte(ctl))}) p := partOn(b) p.Objects = &fakeObjects{} var said []asks.Condition @@ -2799,6 +2820,18 @@ func TestGrants(t *testing.T) { t.Errorf("%s: the kept user was not said: %+v", held.name, last) } } + // A held module that runs no build the part can read declares nothing to anyone: the list in force is kept, said. + delete(b.states[grants.ReadHave], "b.mod-r") + b.put(grants.ReadModules, "mod-r", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32), + Manifest: delivery.ManifestDigest([]byte(ctl)), Repository: "novox/x", Commit: "c", Verdict: delivery.VerdictRefused, + Problems: []string{"it is held"}, Unknown: []string{"core-credentials"}}) + b.put(grants.ReadAccounts, "a.mod-x", map[string]any{"user": "a.mod-x", "hash": "$2a$10$gen9", "generation": 9, "fingerprint": "sha256:9"}) + wasList, _, _ := p.Sealed() + res := p.Compose() + nowList, _, _ := p.Sealed() + if nowList.Digest != wasList.Digest || res.Sealed == nil || !strings.Contains(res.Sealed.Why, "running manifest of mod-r") { + t.Errorf("a held module with no running manifest did not keep the list in force: %+v", res.Sealed) + } // Accepted again: the users compose from the manifest and the condition clears. b.manifestOf("mod-r", ctl) if p.Compose(); said[len(said)-1].State != asks.StateCleared { diff --git a/modules/mesh-grants/internal/grants/inputs.go b/modules/mesh-grants/internal/grants/inputs.go index 029a37e5..82b6a39c 100644 --- a/modules/mesh-grants/internal/grants/inputs.go +++ b/modules/mesh-grants/internal/grants/inputs.go @@ -445,6 +445,22 @@ func ReadInputs(r Reader, cache *Manifests) Inputs { in.Gaps = append(in.Gaps, Gap{Input: "wanted manifests", Why: "no build record names the manifest at the wanted fingerprint, so the trunk's is used: " + strings.Join(fromTrunk, ", ")}) } in.Held = heldManifests(r, in, cache) + // A held module placed on a node whose running manifest is not found declares no seat, stream or object bucket to + // anyone: composing past it would drop the grants of those who use them, so the list in force is kept (ADR 0601 + // clause 3), and the gap says which module and why. + for _, g := range in.Refused { + module, ok := strings.CutPrefix(g.Input, "manifest of ") + if _, found := in.Held[module]; !ok || found { + continue + } + for _, assigned := range in.Assigned { + if slices.Contains(assigned, module) { + in.Gaps = append(in.Gaps, Gap{Input: "running manifest of " + module, Failed: true, + Why: "its manifest is held and no running build's manifest is found, so the seats and streams it declares cannot be composed: the list in force is kept"}) + break + } + } + } in.Hashes = readHashes(r, &in.Gaps) // The seat mesh-enrolment's keys are every token since the controller's end (issue 1126). in.Enrolling, _ = readEnrolling(r, &in.Gaps) diff --git a/modules/mesh-grants/internal/grants/records.go b/modules/mesh-grants/internal/grants/records.go index 28014fbd..62d42424 100644 --- a/modules/mesh-grants/internal/grants/records.go +++ b/modules/mesh-grants/internal/grants/records.go @@ -81,7 +81,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) { d.KeyedReads = append(d.KeyedReads, buscompose.KeyedRead{Read: sr.Read, Key: sr.Key}) } d.Hears, d.HearsAStream, d.Consumes = hearsOf(m, declared, node, values, d.Consumes) - d.Reads, d.ReadsObjects, d.ReadsRefused = readsOfObjects(m.Module, node, d.Reads, d.ReadsRefused, in.Manifests, values) + d.Reads, d.ReadsObjects, d.ReadsRefused = readsOfObjects(m.Module, node, d.Reads, d.ReadsRefused, declared, values) return d } d := declare(m) From 054f08961440d7ce10b0e2d18db89aac50334f7e Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 11:39:39 +0100 Subject: [PATCH 04/49] =?UTF-8?q?The=20bus=20has=20the=20upgrade=20policy?= =?UTF-8?q?=20its=20manifest=20says,=20as=20any=20module:=20the=20judge=20?= =?UTF-8?q?and=20the=20assigner=20rows=20read=20it=20from=20the=20manifest?= =?UTF-8?q?=20(ADR=200601=20=C2=A71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmd/mesh-delivery-assigner/seat_tools_test.go | 4 ++-- modules/mesh-delivery-indexer/judge/rules_test.go | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go index 0e202c95..046dceb8 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go @@ -385,7 +385,7 @@ func wantsVerbs(t *testing.T) { } manifest("web", "") manifest("photos", `,"upgrade":{"policy":"record","why":"a person checks the albums"}`) - manifest("nats", `,"provides":[{"name":"mesh-bus","scope":"mesh"}]`) + manifest("nats", `,"provides":[{"name":"mesh-bus","scope":"mesh"}],"upgrade":{"policy":"record","why":"its streams"}`) for node, fp := range map[string]string{"anchor": fpOld, "laptop": fpNew} { st[haveState].values[node+".nats"], _ = delivery.EncodeHave(delivery.Have{FP: fp, Phase: delivery.PhaseRunning, Health: delivery.HealthOK}) } @@ -406,7 +406,7 @@ func wantsVerbs(t *testing.T) { if p := policyOf("photos"); p.Policy != catalogue.PolicyRecord || p.From != catalogue.FromModule { t.Errorf("photos = %+v", p) } - if p := policyOf("nats"); p.Policy != catalogue.PolicyRecord || p.From != catalogue.FromBus { + if p := policyOf("nats"); p.Policy != catalogue.PolicyRecord || p.From != catalogue.FromModule { // the policy its manifest says (issue 774) t.Errorf("nats = %+v", p) } // compare: each node's own composition is the digest its have names, and the assigner's findings; nothing splits, the diff --git a/modules/mesh-delivery-indexer/judge/rules_test.go b/modules/mesh-delivery-indexer/judge/rules_test.go index a036e926..d0727909 100644 --- a/modules/mesh-delivery-indexer/judge/rules_test.go +++ b/modules/mesh-delivery-indexer/judge/rules_test.go @@ -5171,22 +5171,20 @@ func TestWhereAModulesUpgradePolicyComesFrom(t *testing.T) { catalogue.PolicyRoll, catalogue.FromDefault}, {"irreplaceable but says roll", catalogue.Manifest{Module: "photos", Upgrade: &catalogue.UpgradePolicy{Policy: catalogue.PolicyRoll}, Data: &catalogue.Data{Own: []catalogue.DataItem{{ID: "originals", Class: catalogue.ClassIrreplaceable}}}}, catalogue.PolicyRoll, catalogue.FromModule}, - {"the bus, whatever it says", catalogue.Manifest{Module: "nats", Provides: []catalogue.Offer{{Name: "mesh-bus"}}, - Upgrade: &catalogue.UpgradePolicy{Policy: catalogue.PolicyRoll}}, catalogue.PolicyRecord, catalogue.FromBus}, + // Issue 774 (the operator, 2026-11-03): the bus has the policy its manifest says, as any module. + {"the bus says record", catalogue.Manifest{Module: "nats", Provides: []catalogue.Offer{{Name: "mesh-bus"}}, + Upgrade: &catalogue.UpgradePolicy{Policy: catalogue.PolicyRecord, Why: "its streams"}}, catalogue.PolicyRecord, catalogue.FromModule}, } for _, c := range cases { policy, from, _ := catalogue.DerivedUpgrade(c.m) if policy != c.policy || from != c.from { t.Errorf("%s: %s from %s, want %s from %s", c.name, policy, from, c.policy, c.from) } - // ADR 0565 clause 7 (issue 1953 L5-S3): a person's choice wins over each source but the bus; none leaves the - // derivation as it was. + // ADR 0565 clause 7 (issue 1953 L5-S3): a person's choice wins over each source; none leaves the derivation as it + // was. The assigner's upgrade verb refuses a choice for the bus (wants_tools.go setPolicy). person := &catalogue.PersonPolicy{V: catalogue.PersonPolicyVersion, Module: c.m.Module, Policy: catalogue.PolicyTogether, Why: "one zone", By: "person.jochen"} policy, from, _, by := catalogue.Upgrade(c.m, person) wantPolicy, wantFrom, wantBy := catalogue.PolicyTogether, catalogue.FromPerson, "person.jochen" - if c.from == catalogue.FromBus { - wantPolicy, wantFrom, wantBy = c.policy, catalogue.FromBus, "" - } if policy != wantPolicy || from != wantFrom || by != wantBy { t.Errorf("%s with a person's choice: %s from %s by %q", c.name, policy, from, by) } From ab3cb1ca9e792356c76f8cc9d8003b9f0721984f Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 11:58:25 +0100 Subject: [PATCH 05/49] =?UTF-8?q?The=20judge=20knows=20the=20tool=20runner?= =?UTF-8?q?=20by=20what=20its=20manifest=20says=20and=20keeps=20the=20kept?= =?UTF-8?q?=20streams=20itself,=20and=20this=20repository's=20check=20that?= =?UTF-8?q?=20no=20manifest=20names=20the=20ended=20controller=20reads=20t?= =?UTF-8?q?hem=20as=20text:=20mesh-sdk=20names=20no=20module=20(ADR=200601?= =?UTF-8?q?=20=C2=A74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/mesh-delivery-indexer/judge/rules.go | 29 +++++++++++++++---- .../mesh-delivery-indexer/judge/rules_test.go | 27 ++++++++++------- modules/mesh-grants/grants_test.go | 24 ++++++++------- .../internal/buscompose/health_grants.go | 5 +++- 4 files changed, 57 insertions(+), 28 deletions(-) diff --git a/modules/mesh-delivery-indexer/judge/rules.go b/modules/mesh-delivery-indexer/judge/rules.go index 4ca46f7d..49fa9f40 100644 --- a/modules/mesh-delivery-indexer/judge/rules.go +++ b/modules/mesh-delivery-indexer/judge/rules.go @@ -136,7 +136,7 @@ func spokenSystems() string { // and was simply absent — seven modules' tools went missing that way on 2026-10-03. Refused here, // naming the field that would deliver it. func undeliveredBundles(m catalogue.Manifest) []string { - if m.Build == nil || m.Module == catalogue.RuntimeModule { + if m.Build == nil || catalogue.IsToolRunner(m) { return nil } named := runByAResource(m) @@ -3306,9 +3306,9 @@ func waitProblems(m catalogue.Manifest) []string { if len(m.Waits) == 0 { return nil } - if m.Module != catalogue.RuntimeModule { - return []string{fmt.Sprintf("%s says what a session waits on; only %s, the tool runner's module, "+ - "declares waits (novox/hq ADR 0367 §2)", m.Module, catalogue.RuntimeModule)} + if !catalogue.IsToolRunner(m) { + return []string{fmt.Sprintf("%s says what a session waits on; only the tool runner's module (it says "+ + "\"tool-runner\": true) declares waits (novox/hq ADR 0367 §2)", m.Module)} } var problems []string seen := map[string]bool{} @@ -5420,12 +5420,29 @@ func StateProblems(m catalogue.Manifest) []string { return problems } -// PredefinedFormProblems refuses each stream a module declares that StreamsKeptUntilMoved does not name for it. +// **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. The judge's own exception, kept here and no longer in mesh-sdk, +// which names no module (issue 774, the operator 2026-11-03). +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"}, +} + +// PredefinedFormProblems refuses each stream a module declares that streamsKeptUntilMoved does not name for it. func PredefinedFormProblems(m catalogue.Manifest) []string { var out []string for _, s := range m.Streams { // A stream that keeps its history is one of the bus's predefined forms (novox/hq ADR 0592 §6). - if !s.KeepsHistory() && !slices.Contains(catalogue.StreamsKeptUntilMoved[m.Module], s.Name) { + if !s.KeepsHistory() && !slices.Contains(streamsKeptUntilMoved[m.Module], s.Name) { out = append(out, fmt.Sprintf("%s keeps the stream %q, outside the bus's predefined forms: keep it as a state, "+ "a log or an object store, which the runtime reads for the module's tools and others read by `reads`", m.Module, s.Name)) } diff --git a/modules/mesh-delivery-indexer/judge/rules_test.go b/modules/mesh-delivery-indexer/judge/rules_test.go index d0727909..1c14098a 100644 --- a/modules/mesh-delivery-indexer/judge/rules_test.go +++ b/modules/mesh-delivery-indexer/judge/rules_test.go @@ -157,12 +157,18 @@ func TestABundleNothingDeliversIsRefused(t *testing.T) { run := base() run.Resources = []map[string]any{{"id": "daemon", "type": "process", "artifact": "tools", "run": []any{"node", "tools/index.js"}}} runtime := base() - runtime.Module = catalogue.RuntimeModule + runtime.Module, runtime.ToolRunner = "node-tools", true for name, m := range map[string]catalogue.Manifest{"loads": loads, "tools": tools, "a process": run, "the runtime": runtime} { if p := undeliveredBundles(m); len(p) != 0 { t.Errorf("a bundle delivered by %s was refused: %v", name, p) } } + // The tool runner is what says it is one (issue 774): its name alone exempts nothing. + named := base() + named.Module = "node-tools" + if p := undeliveredBundles(named); len(p) != 1 { + t.Errorf("a bundle of a module named as the tool runner, which does not say it is one: %v", p) + } } func TestABundlesWordsAreRefusedWhenTheyAreNotPathsOrConstants(t *testing.T) { @@ -3108,15 +3114,16 @@ func TestAVerbWithAnUnknownKeyIsRefused(t *testing.T) { // each refusal is said in the manifest's words, at parse, rather than at the composition of the bus's users. func TestAWaitIsRefusedUnlessItNamesOneFamilyOfTheToolRunner(t *testing.T) { for manifest, want := range map[string]string{ - `{"module":"shop","version":"1","waits":["mesh-knowledge.reviewed"]}`: "only node-tools", - `{"module":"node-tools","version":"1","waits":["mesh-knowledge.*"]}`: "never a wildcard", - `{"module":"node-tools","version":"1","waits":["**"]}`: "never a wildcard", - `{"module":"node-tools","version":"1","waits":["mesh-knowledge.>"]}`: "never a wildcard", - `{"module":"node-tools","version":"1","waits":["*.reviewed"]}`: "never a wildcard", - `{"module":"node-tools","version":"1","waits":["reviewed"]}`: "does not name an emitter and an event", - `{"module":"node-tools","version":"1","waits":["mesh-knowledge..reviewed"]}`: "does not name an emitter and an event", - `{"module":"node-tools","version":"1","waits":["Mesh-Knowledge.reviewed x"]}`: "does not name an emitter and an event", - `{"module":"node-tools","version":"1","waits":["a.b","a.b"]}`: "twice", + `{"module":"shop","version":"1","waits":["mesh-knowledge.reviewed"]}`: "only the tool runner's module", + `{"module":"node-tools","version":"1","waits":["mesh-knowledge.reviewed"]}`: "only the tool runner's module", // its name says nothing (issue 774) + `{"module":"node-tools","version":"1","tool-runner":true,"waits":["mesh-knowledge.*"]}`: "never a wildcard", + `{"module":"node-tools","version":"1","tool-runner":true,"waits":["**"]}`: "never a wildcard", + `{"module":"node-tools","version":"1","tool-runner":true,"waits":["mesh-knowledge.>"]}`: "never a wildcard", + `{"module":"node-tools","version":"1","tool-runner":true,"waits":["*.reviewed"]}`: "never a wildcard", + `{"module":"node-tools","version":"1","tool-runner":true,"waits":["reviewed"]}`: "does not name an emitter and an event", + `{"module":"node-tools","version":"1","tool-runner":true,"waits":["mesh-knowledge..reviewed"]}`: "does not name an emitter and an event", + `{"module":"node-tools","version":"1","tool-runner":true,"waits":["Mesh-Knowledge.reviewed x"]}`: "does not name an emitter and an event", + `{"module":"node-tools","version":"1","tool-runner":true,"waits":["a.b","a.b"]}`: "twice", } { _, err := ParseManifest([]byte(manifest)) if err == nil { diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 17e8db89..90ffeaed 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -11,6 +11,7 @@ import ( "maps" "os" "path/filepath" + "regexp" "slices" "strings" "sync" @@ -2140,26 +2141,27 @@ func TestGrants(t *testing.T) { }) // ADR 0590 clause 5's test for this repository (issue 1126): no module.json here names mesh-controller, the lease - // read and the builder's consume of artifacts-gone included (mesh-sdk catalogue.ManifestRefusals). + // read and the builder's consume of artifacts-gone included. Read as text, as mesh-catalog's controllerdeps check reads + // its own: mesh-sdk names no module, so it holds no such check (issue 774, the operator 2026-11-03). t.Run("no module of this repository names the controller", func(t *testing.T) { paths, _ := filepath.Glob("../*/module.json") - manifests := map[string]catalogue.Manifest{} + if len(paths) < 8 { + t.Fatalf("read %d manifests, want every module of the repository", len(paths)) + } + names := regexp.MustCompile(`(^|[^A-Za-z0-9_-])(seat:)?mesh-controller([._"]|$)`) for _, path := range paths { raw, err := os.ReadFile(path) if err != nil { t.Fatal(err) } - m, err := catalogue.Decode(raw) - if err != nil { + if _, err := catalogue.Decode(raw); err != nil { t.Fatalf("%s: %v", path, err) } - manifests[m.Module] = m - } - if len(manifests) < 8 { - t.Fatalf("read %d manifests, want every module of the repository", len(manifests)) - } - for _, why := range catalogue.ManifestRefusals(manifests) { - t.Error(why) + for i, line := range strings.Split(string(raw), "\n") { + if names.MatchString(line) { + t.Errorf("%s:%d names the ended controller: %s", path, i+1, strings.TrimSpace(line)) + } + } } }) diff --git a/modules/mesh-grants/internal/buscompose/health_grants.go b/modules/mesh-grants/internal/buscompose/health_grants.go index 29036e94..7b06c49a 100644 --- a/modules/mesh-grants/internal/buscompose/health_grants.go +++ b/modules/mesh-grants/internal/buscompose/health_grants.go @@ -45,6 +45,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", @@ -55,7 +58,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) { From ecf96be8ce697c8985a9ad57c0fe3b4342bd3ed4 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 13:19:41 +0100 Subject: [PATCH 06/49] =?UTF-8?q?The=20judge's=20row=20for=20a=20field=20i?= =?UTF-8?q?t=20does=20not=20know=20names=20a=20field=20mesh-sdk=20does=20n?= =?UTF-8?q?ot=20know=20now:=20core-credentials=20is=20one=20of=20its=20man?= =?UTF-8?q?ifest=20words=20(ADR=200601=20=C2=A74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/mesh-delivery-indexer/judge/judge_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/mesh-delivery-indexer/judge/judge_test.go b/modules/mesh-delivery-indexer/judge/judge_test.go index f4fe062a..4b6cdc7c 100644 --- a/modules/mesh-delivery-indexer/judge/judge_test.go +++ b/modules/mesh-delivery-indexer/judge/judge_test.go @@ -57,16 +57,16 @@ func TestTheJudge(t *testing.T) { // A field the judge does not know waits in the indexer's judge and stays a refusal in the checker's (clause 3 of the // record on a release's Approve); a renamed field and any other problem are refused by both. waitRaws := map[string][]byte{ - "later": []byte(`{"module":"later","version":"1","core-credentials":true,"other":1}`), + "later": []byte(`{"module":"later","version":"1","later-field":true,"other":1}`), "renamed": []byte(`{"module":"renamed","version":"1","needs":{"x":"y"}}`), "bad": []byte(`{"module":"bad","version":"1","provides":[{"name":"amqp"}]}`), "ok": []byte(`{"module":"ok","version":"1"}`), } refused, waits, _ := ShelfWaiting(waitRaws, nil, 0) - if !reflect.DeepEqual(waits, map[string][]string{"later": {"core-credentials", "other"}}) || len(refused) != 2 || refused["renamed"] == nil || refused["bad"] == nil { + if !reflect.DeepEqual(waits, map[string][]string{"later": {"later-field", "other"}}) || len(refused) != 2 || refused["renamed"] == nil || refused["bad"] == nil { t.Errorf("ShelfWaiting: waits %v, refused %v", waits, refused) } - if strict, _ := Shelf(waitRaws, nil, 0); len(strict) != 3 || !strings.Contains(strings.Join(strict["later"], ""), "core-credentials") { + if strict, _ := Shelf(waitRaws, nil, 0); len(strict) != 3 || !strings.Contains(strings.Join(strict["later"], ""), "later-field") { t.Errorf("Shelf refuses as before: %v", strict) } // The private network's manifest, which the mesh provides itself, is accepted (moved from mesh-sdk overlay's test). From a9aa6d29ead10e61bba7901bc233c318b9a8e9a7 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 13:21:18 +0100 Subject: [PATCH 07/49] =?UTF-8?q?The=20assigner=20moves=20the=20bus=20modu?= =?UTF-8?q?le=20by=20its=20rules=20as=20any=20module,=20with=20no=20bar=20?= =?UTF-8?q?and=20no=20snapshot=20of=20its=20own,=20and=20a=20person=20may?= =?UTF-8?q?=20choose=20its=20policy=20as=20any=20module's:=20its=20node=20?= =?UTF-8?q?backs=20its=20streams=20up=20before=20it=20applies=20(ADR=20060?= =?UTF-8?q?1=20=C2=A71,=20=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmd/mesh-delivery-assigner/pass_test.go | 8 ++-- .../mesh-delivery-assigner/seat_tools_test.go | 8 +++- .../cmd/mesh-delivery-assigner/wants_tools.go | 8 +--- .../internal/assign/asks.go | 8 ---- .../internal/assign/policy.go | 2 +- .../internal/assign/wants.go | 48 ++++--------------- .../internal/assign/wants_test.go | 33 ++++--------- .../mesh-delivery-indexer/judge/rules_test.go | 2 +- 8 files changed, 31 insertions(+), 86 deletions(-) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go index df5b9dc9..a527a890 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go @@ -414,10 +414,11 @@ func TestAModuleOnNeitherListIsRaisedOnceAndClearedOnce(t *testing.T) { if last := logged[len(logged)-1]; last != "screen-lock on g14 has a want again" { t.Errorf("the clear said %q", last) } - // 1945 (the bus bar): a module that leaves neither list with no want is cleared in words saying so, never "has a want again". - bus := map[string]neitherOne{"novox.nats": {node: "novox", module: "nats", holding: assign.Holding{Bus: true, Words: "the bus moves only by the operator's put"}}} + // 1945: a module that leaves neither list with no want is cleared in words saying so, never "has a want again". The bus is + // on neither list by a rule as any module (ADR 0601 §1), nothing for the operator to do. + bus := map[string]neitherOne{"novox.nats": {node: "novox", module: "nats", holding: assign.Holding{Rule: delivery.RuleGrant, Words: "no credential"}}} p.sayNeither(bus, nil, nil) - if c := said[len(said)-1]; c.State != asks.StateRaised || !strings.HasPrefix(c.Explanation, asks.NeedsYou) || !strings.Contains(c.Explanation, "the bus moves only by the operator's put") { + if c := said[len(said)-1]; c.State != asks.StateRaised || !strings.HasPrefix(c.Explanation, asks.NothingToDo) || !strings.Contains(c.Explanation, "rule 6 holds its first want back: no credential") { t.Errorf("the bus's neither condition = %+v", c) } p.sayNeither(map[string]neitherOne{}, nil, map[string]string{"novox.nats": "it is held, and its held condition says so"}) @@ -474,7 +475,6 @@ func testThePassGathersTheModulesOnNeitherList(t *testing.T) { {nil, assign.Outcome{Rules: map[string]assign.Holding{"shanks": {Rule: delivery.RuleGrant, Words: "no credential"}}}, "rofi on shanks gets no want: rule 6: no credential"}, {nil, wanted, "rofi on shanks has a want"}, {map[string]delivery.Want{"shanks": wanted.Wants["shanks"]}, assign.Outcome{}, ""}, - {nil, assign.Outcome{Rules: map[string]assign.Holding{"shanks": {Bus: true, Words: "the bus moves only by the operator's put"}}}, "rofi on shanks gets no want: the bus moves only by the operator's put"}, } { logged = nil p.sayNoWant("rofi", []string{"shanks"}, c.before, c.out) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go index 046dceb8..fd93f700 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go @@ -471,8 +471,12 @@ func wantsVerbs(t *testing.T) { len(said) != 1 || !strings.HasPrefix(said[0], "policy-changed policy-changed.web.") { t.Errorf("chosen %+v, asked %v, said %v", c, asked, said) } - if _, err := w.doUpgrade(map[string]any{"module": "nats", "policy": "roll", "why": "x"}, "op"); err == nil { - t.Error("the bus was given a policy") + // A person may choose the bus's policy as any module's (ADR 0601 §1: no part treats the module that provides the bus apart). + if _, err := w.doUpgrade(map[string]any{"module": "nats", "policy": "roll", "why": "x"}, "op"); err != nil { + t.Errorf("a person's policy for the bus was refused: %v", err) + } + if _, err := w.doUpgrade(map[string]any{"module": "nats", "policy": "default", "why": "back to the module"}, "op"); err != nil { + t.Fatal(err) } if _, err := w.doUpgrade(map[string]any{"module": "web", "policy": "roll"}, "op"); err == nil { t.Error("a policy without why was set") diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/wants_tools.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/wants_tools.go index 4749b289..d0c4092c 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/wants_tools.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/wants_tools.go @@ -197,18 +197,14 @@ func (s *wantsTools) doUpgrade(args map[string]any, by string) (any, error) { } // setPolicy writes a person's choice of a module's policy, or deletes it for default, records the act in the hand-act log -// and says policy-changed. The bus is never rolled: a choice for it is refused. +// and says policy-changed. The bus's choice is a person's as any module's (ADR 0601 §1). func (s *wantsTools) setPolicy(module, policy, why, by string, manifests map[string]catalogue.Manifest) (any, error) { if module == "" { return nil, errors.New("a policy is set for one module: name it") } - m, ok := manifests[module] - if !ok { + if _, ok := manifests[module]; !ok { return nil, fmt.Errorf("the indexer holds no manifest of %s", module) } - if catalogue.ProvidesBus(m) { - return nil, fmt.Errorf("%s is the bus, which is never rolled whoever says so (novox/hq ADR 0236): its step is the put on the operator's warrant", module) - } if why == "" { return nil, errors.New("a person's policy says why") } diff --git a/modules/mesh-delivery-assigner/internal/assign/asks.go b/modules/mesh-delivery-assigner/internal/assign/asks.go index b8125bee..70651e77 100644 --- a/modules/mesh-delivery-assigner/internal/assign/asks.go +++ b/modules/mesh-delivery-assigner/internal/assign/asks.go @@ -119,14 +119,6 @@ func NeitherCondition(node, module string, h Holding, raised bool) asks.Conditio if n := len([]rune(headline)); n > asks.HeadlineLength { headline = string([]rune(headline)[:asks.HeadlineLength]) } - if h.Bus { - needs := fmt.Sprintf("put %s on %s with assigner_put", module, node) - return asks.Condition{Key: key, Severity: asks.SeverityUrgent, Subject: subject, State: asks.StateRaised, - Summary: headline, Headline: headline, Needs: needs, - Explanation: fmt.Sprintf("%s%s. %s is assigned to %s, but the node holds no want for it and the node's own composition does not "+ - "hold it either, so nothing applies it there. The assigner never moves the bus: %s. This clears once the node has a want for it.", - asks.NeedsYou, needs, module, node, h.Words)} - } return asks.Condition{Key: key, Severity: asks.SeverityUrgent, Subject: subject, State: asks.StateRaised, Summary: headline, Headline: headline, Explanation: fmt.Sprintf("%s %s is assigned to %s, but the node holds no want for it and the node's own composition does not "+ diff --git a/modules/mesh-delivery-assigner/internal/assign/policy.go b/modules/mesh-delivery-assigner/internal/assign/policy.go index c3d94566..fab4630b 100644 --- a/modules/mesh-delivery-assigner/internal/assign/policy.go +++ b/modules/mesh-delivery-assigner/internal/assign/policy.go @@ -16,7 +16,7 @@ import ( // A person's upgrade policy is the assigner's (novox/hq ADR 0565 §7, §8, issue 1953 D4, D5): kept in its state `policies`, one // key per module, set and taken back through `mesh-wants.upgrade`. Every reader takes one derivation, mesh-sdk's -// catalogue.Upgrade: a person's choice over the module's word, its irreplaceable data and the default, the bus never rolled. +// catalogue.Upgrade: a person's choice over the module's word, its irreplaceable data and the default, the bus as any module (ADR 0601 §1). // PoliciesBucket is the assigner's state `policies`. var PoliciesBucket = delivery.AssignerModule + "_" + delivery.AssignerPoliciesState diff --git a/modules/mesh-delivery-assigner/internal/assign/wants.go b/modules/mesh-delivery-assigner/internal/assign/wants.go index 3859e68d..629014e9 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants.go @@ -268,29 +268,18 @@ type RecordNeed struct { Words string } -// Holding is a rule holding a node back, and why. Bus marks the bar on the bus instead of one of the seven rules (Rule is then -// 0): the bus moves only by the operator's put (ADR 0539 §15, ADR 0546 §8), never by the assigner (issue 1945: nats on novox, -// running an old-form build, got no want and the pass said nothing). +// Holding is a rule holding a node back, and why. The bus module is held back by the rules alone, as every module (ADR 0601 §1, +// §2): no bar of its own. type Holding struct { Rule int Words string - Bus bool } -// Reason is the holding in words: the rule and its words, or for the bus bar its words alone. +// Reason is the holding in words: the rule and its words. func (h Holding) Reason() string { - if h.Bus { - return h.Words - } return fmt.Sprintf("rule %d: %s", h.Rule, h.Words) } -// BusBarWords say why the assigner writes no want moving the bus on node: it runs prev, the trunk builds fp, and only the -// operator's put moves it (ADR 0539 §15, ADR 0546 §8). The words reach the operator, so they name no record. -func BusBarWords(node, prev, fp string) string { - return fmt.Sprintf("only your put moves the bus: %s runs %s, the trunk builds %s; put it with assigner_put", node, short(prev), short(fp)) -} - // Wants is the pass of one module m: for each of its nodes, the want by the six rules, starting from the want written // before (wants, by node); lastHealthy is, by node, the newest fingerprint other than m.FP the node ran healthy. A failed // health check of m.FP on a node holds it: every node wanting m.FP is set back to its last healthy fingerprint, but a @@ -425,14 +414,6 @@ func Wants(m Module, n Nodes, wants map[string]delivery.Want, lastHealthy map[st if need, wait := at.mustAsk(node, asked, hadLive, ran, running.FP, running.Composed == "", d); wait { if need != nil { out.Record = append(out.Record, *need) - } else if prev, barred := at.busBar(asked, hadLive, running.FP); barred { - // The bus is barred: no want moves it but the operator's put. Said, never silent (issue 1945): its holding is named, - // and a node that runs it with no live want has it on neither list, so its condition stays raised. - h := Holding{Bus: true, Words: BusBarWords(node, prev, at.FP)} - out.Rules[node] = h - if !hadLive && running.Phase != delivery.PhaseRemoved && !held { - out.Neither[node] = h - } } continue } @@ -659,10 +640,10 @@ func short(fp string) string { return part[:12] } -// mustAsk says whether the move to the composition d on node waits: for the operator's yes (need names the ask) or, for the bus, -// for the operator's hand (ADR 0539 §14, §15). A module the node never ran (it has no want and no have), a move judged neither, a module that is not of -// policy record and a want that stays are written as any want; so is a revision of the bus module's composition at the -// same fingerprint judged neither (a user list only, §7). +// mustAsk says whether the move to the composition d on node waits for the operator's yes (need names the ask, ADR 0539 §14). A +// module the node never ran (it has no want and no have), a move judged neither, a module that is not of policy record and a want +// that stays are written as any want. The bus module is no exception: it moves by the rules (ADR 0601 §1), its node backing up its +// streams before it applies (§2); the assigner asks no snapshot for it. func (m Module) mustAsk(node string, before delivery.Want, had, ran bool, running string, haveNamesNoComposition bool, d delivery.ComposedDigest) (*RecordNeed, bool) { if (!had && !ran) || before.Removed || (before.FP == m.FP && before.Composed == d.Digest) { return nil, false @@ -674,9 +655,6 @@ func (m Module) mustAsk(node string, before delivery.Want, had, ran bool, runnin if !had && ran && running == m.FP && haveNamesNoComposition && d.Judgement == delivery.JudgedUnchanged { return nil, false } - if _, barred := m.busBar(before, had, running); barred { - return nil, true // the bus is never moved by the assigner; the operator writes its want (§15) - } if !m.recorded(d) || d.Judgement == delivery.JudgedNeither || d.Judgement == delivery.JudgedUnchanged { return nil, false } @@ -686,17 +664,7 @@ func (m Module) mustAsk(node string, before delivery.Want, had, ran bool, runnin return &RecordNeed{Node: node, Digest: d.Digest, Judgement: d.Judgement, Words: d.Words}, true } -// busBar says whether m is the bus and the move would take it off the fingerprint it ran before, prev: its want's, or with no -// readable want the one its have names (ADR 0539 §15 bars a new one). -func (m Module) busBar(before delivery.Want, had bool, running string) (prev string, barred bool) { - prev = before.FP - if !had { - prev = running - } - return prev, catalogue.ProvidesBus(m.Manifest) && prev != m.FP -} - -// recorded says whether the module is of policy record: a person's choice at the assigner over everything but the bus (ADR +// recorded says whether the module is of policy record: a person's choice at the assigner over the manifest's word (ADR // 0565 §7, catalogue.Upgrade); for a module with none, as the controller holds it beside the composition's digest until the // lease write (ADR 0539 §14); on a composition made by hand, which carries none, as the manifest derives it. func (m Module) recorded(d delivery.ComposedDigest) bool { diff --git a/modules/mesh-delivery-assigner/internal/assign/wants_test.go b/modules/mesh-delivery-assigner/internal/assign/wants_test.go index 35af5f8a..95d4eee3 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants_test.go @@ -639,11 +639,13 @@ func TestAMoveJudgedNeitherOrNeverRunOrNotRecordedIsWrittenAtOnce(t *testing.T) // 1906/D4 (ADR 0539 §7, §15): the bus module's new fingerprint is never written by the assigner, but a revision of its // composition at the same fingerprint that only changes the user list (judged neither) is written at once. -func TestTheBusIsNotMovedButItsUserListRevisionIsWrittenAtOnce(t *testing.T) { +func TestTheBusMovesByTheRulesAndItsUserListRevisionIsWrittenAtOnce(t *testing.T) { m, n, _ := movingTo(delivery.JudgedNeither) m.Manifest.Provides = []catalogue.Offer{{Name: "mesh-bus"}} - if out := Wants(m, n, wantedOld(), nil); out.Wants["ace"].FP == fNew { - t.Errorf("the bus moved to a new fingerprint: %+v", out.Wants["ace"]) + // ADR 0601 §1, §2: the bus module's want is written by the rules, as any module's, and no snapshot is asked for it: its node + // backs its streams up before it applies. + if out := Wants(m, n, wantedOld(), nil); out.Wants["ace"].FP != fNew || len(out.Record) != 0 || len(out.Neither) != 0 { + t.Errorf("the bus did not move by the rules: %+v", out) } running := map[string]delivery.Want{"ace": {FP: fNew, Composed: dig(fOld)}, "g14": {FP: fNew, Composed: dig(fOld)}} out := Wants(m, n, running, nil) @@ -696,23 +698,6 @@ func TestTheBusIsNotMovedButItsUserListRevisionIsWrittenAtOnce(t *testing.T) { t.Errorf("a held bus did not take its user-list revision as a put: %+v", w) } m.Puts = nil - // 1945 (the bus bar made loud): a node that runs the bus at an old-form build and holds no want for it gets none, since only - // the operator's put moves the bus, and the pass says so: a holding naming the bar, and the node on neither list so its - // condition stays raised. A node with a live want keeps it and is not on neither list. - m.Composed = composedEverywhere - n.Haves["ace"]["mail"] = delivery.Have{FP: "b-1700000000-oldform", Phase: delivery.PhaseRunning, Health: delivery.HealthOK} - out = Wants(m, n, map[string]delivery.Want{"g14": {FP: fOld, Composed: dig(fOld)}}, nil) - if w, wrote := out.Wants["ace"]; wrote { - t.Errorf("the bus bar wrote ace's want %+v", w) - } - h, on := out.Neither["ace"] - if !on || !h.Bus || out.Rules["ace"] != h || !strings.Contains(h.Words, "only your put moves the bus") || !strings.Contains(h.Words, "ace runs b-1700000000-oldform") || - !strings.Contains(h.Words, "assigner_put") { - t.Errorf("the barred bus on ace: neither %+v (%v), rules %+v", h, on, out.Rules["ace"]) - } - if _, on := out.Neither["g14"]; on || out.Wants["g14"].FP == fNew { - t.Errorf("g14 has a live want: neither %+v, its want %+v", out.Neither, out.Wants["g14"]) - } stepsOfAPut(t) } @@ -889,8 +874,8 @@ func TestTheRecordPolicyIsTheOneTheControllerHolds(t *testing.T) { } } -// Review round 1, item 2 (ADR 0539 §15): a node that runs the bus with no readable want compares the new fingerprint with the one -// it runs, from its have: the same fingerprint is not a new one (written by the rules), another is barred. +// A node that runs the bus with no readable want gets its want by the rules, at the fingerprint it runs or at a new one alike: the +// bus has no bar of its own (ADR 0601 §1). func TestTheBusWithNoWantIsComparedWithWhatItRuns(t *testing.T) { m, n, _ := movingTo(delivery.JudgedNeither) m.Manifest.Provides = []catalogue.Offer{{Name: "mesh-bus"}} @@ -899,8 +884,8 @@ func TestTheBusWithNoWantIsComparedWithWhatItRuns(t *testing.T) { t.Errorf("the bus runs F with no want and was not given one: %+v %v", w, ok) } n.Haves["ace"]["mail"] = delivery.Have{FP: fOld, Phase: delivery.PhaseRunning, Health: delivery.HealthOK} - if w, ok := Wants(m, n, nil, nil).Wants["ace"]; ok { - t.Errorf("the bus was moved to a new fingerprint with no want: %+v", w) + if w, ok := Wants(m, n, nil, nil).Wants["ace"]; !ok || w.FP != fNew { + t.Errorf("the bus running an older fingerprint with no want was not given its want by the rules: %+v %v", w, ok) } } diff --git a/modules/mesh-delivery-indexer/judge/rules_test.go b/modules/mesh-delivery-indexer/judge/rules_test.go index 1c14098a..6e873e02 100644 --- a/modules/mesh-delivery-indexer/judge/rules_test.go +++ b/modules/mesh-delivery-indexer/judge/rules_test.go @@ -5188,7 +5188,7 @@ func TestWhereAModulesUpgradePolicyComesFrom(t *testing.T) { t.Errorf("%s: %s from %s, want %s from %s", c.name, policy, from, c.policy, c.from) } // ADR 0565 clause 7 (issue 1953 L5-S3): a person's choice wins over each source; none leaves the derivation as it - // was. The assigner's upgrade verb refuses a choice for the bus (wants_tools.go setPolicy). + // was. The bus is no exception (ADR 0601 §1). person := &catalogue.PersonPolicy{V: catalogue.PersonPolicyVersion, Module: c.m.Module, Policy: catalogue.PolicyTogether, Why: "one zone", By: "person.jochen"} policy, from, _, by := catalogue.Upgrade(c.m, person) wantPolicy, wantFrom, wantBy := catalogue.PolicyTogether, catalogue.FromPerson, "person.jochen" From daf178c4ca178a3586cf1a05c64cec95b1b8f063 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 13:23:53 +0100 Subject: [PATCH 08/49] =?UTF-8?q?A=20release's=20Approve=20answers=20the?= =?UTF-8?q?=20record=20ask=20of=20a=20move=20of=20the=20build=20alone=20to?= =?UTF-8?q?=20the=20build=20it=20carries:=20the=20assigner=20reads=20the?= =?UTF-8?q?=20release=20by=20the=20build's=20source=20revision=20from=20th?= =?UTF-8?q?e=20gate's=20source=20and=20merges,=20and=20names=20it=20in=20t?= =?UTF-8?q?he=20want;=20a=20move=20that=20changes=20a=20setting=20or=20a?= =?UTF-8?q?=20secret=20too=20is=20asked=20as=20before=20(ADR=200601=20?= =?UTF-8?q?=C2=A71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmd/mesh-delivery-assigner/main.go | 17 +++++- .../cmd/mesh-delivery-assigner/pass.go | 33 ++++++++--- .../cmd/mesh-delivery-assigner/pass_test.go | 56 +++++++++++++++++++ .../cmd/mesh-delivery-assigner/world.go | 43 +++++++++++++- .../internal/assign/wants.go | 4 ++ .../mesh-delivery-assigner/manifest_test.go | 4 +- modules/mesh-delivery-assigner/module.json | 2 + 7 files changed, 144 insertions(+), 15 deletions(-) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go index 07368f8a..b87acaac 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go @@ -60,6 +60,8 @@ var readBuckets = []string{ delivery.PresenceBucket, delivery.HaveBucket, provisions.ProvisionsBucket, + delivery.SourceBucket, + delivery.MergesBucket, } // readFamilies are the states kept one bucket per node the assigner reads, each named in its manifest's reads: every @@ -140,6 +142,12 @@ func open(ctx context.Context, js process.BucketOpener, logf func(string, ...any logf("the bucket %s cannot be opened (%v): the assigner's conditions are said straight to the router until it starts again", name, err) continue } + if (name == delivery.SourceBucket || name == delivery.MergesBucket) && err != nil { + // The gate's source and merges (ADR 0601 §1): without them no release is known to carry a build, so every move of + // a record module is asked of a person, as before. + logf("the bucket %s cannot be opened (%v): no release's Approve answers a record ask until it is bound", name, err) + continue + } if name == delivery.PartGrantsBucket && err != nil { // The grants part's grants (ADR 0571 clause 9): without them no module counts as granted until it is bound. logf("the bucket %s cannot be opened (%v): no grant is read until it is bound once the bus says it was made", name, err) @@ -158,7 +166,7 @@ func open(ctx context.Context, js process.BucketOpener, logf func(string, ...any func startsWithout(name string) bool { switch name { case delivery.SeatHoldersBucket, provisions.ProvisionsBucket, assign.PoliciesBucket, assign.FindingsBucket, - delivery.AssignerPutsBucket: + delivery.AssignerPutsBucket, delivery.SourceBucket, delivery.MergesBucket: return true } return slices.Contains(seatBuckets, name) || name == delivery.PartGrantsBucket @@ -241,6 +249,8 @@ var watched = []string{ assign.PoliciesBucket, provisions.ProvisionsBucket, delivery.SeatHoldersBucket, + delivery.SourceBucket, + delivery.MergesBucket, } // follow watches every bucket a pass reads and makes a pass at start, once every watch handed what stands, and after @@ -336,8 +346,9 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber, } started++ var opts []jetstream.WatchOpt - if name == delivery.HaveBucket { - opts = append(opts, jetstream.IncludeHistory()) // what each node ran healthy before, for a set-back + if name == delivery.HaveBucket || name == delivery.SourceBucket { + // what each node ran healthy before, for a set-back; and the release behind each source revision (ADR 0601 §1) + opts = append(opts, jetstream.IncludeHistory()) } if err := watch(name, opts...); err != nil { return err diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go index e3726890..1992c412 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go @@ -174,7 +174,8 @@ func (p *passer) wants(ctx context.Context) { Allowed: func(node, digest string) bool { key, err := delivery.RecordAskKey(node, name, digest) return err == nil && p.answeredWith(key, assign.OptionYes) - }} + }, + Release: func(fp string) (string, bool) { return p.w.release(name, fp) }} if fp != "" { held, _ := delivery.HeldKey(name, fp) grant, _ := delivery.PartGrantKey(name, fp) @@ -819,7 +820,9 @@ func (p *passer) backupAsk(ctx context.Context, key, node, module string, want d // yeses writes a placed node's wants naming the build alone, and sees to the moves a node that composes its own judged a // person must allow (ADR 0547 §3): for a have of phase // waiting-yes it raises the record ask once, naming the composition waiting and the node's judgement, and on a yes writes -// the want again naming it; a yes once written is kept while the want stays at its build, so the node is not asked again. +// the want again naming it; a yes once written is kept while the want stays at its build, so the node is not asked again. A +// move of the build alone (the have says so) to a build an approved release carries is given its yes by that Approve, the +// want naming the release, with no ask (ADR 0601 §1). func (p *passer) yeses(ctx context.Context, module string, m assign.Module, before map[string]delivery.Want, out *assign.Outcome) { for _, node := range m.Nodes { if m.Self == nil || !m.Self(node) { @@ -836,25 +839,37 @@ func (p *passer) yeses(ctx context.Context, module string, m assign.Module, befo if w.Removed { continue } - yes := "" + yes, release := "", "" if had && was.FP == w.FP { - yes = was.Yes + yes, release = was.Yes, was.Release } key, err := delivery.HaveKey(node, module) if err != nil { continue } if h, err := delivery.DecodeHave(p.w.value(delivery.HaveBucket, key)); err == nil && h.Phase == delivery.PhaseWaitingYes && h.Waits != yes { - if m.Allowed != nil && m.Allowed(node, h.Waits) { - yes = h.Waits - } else { + switch { + case m.Allowed != nil && m.Allowed(node, h.Waits): + yes, release = h.Waits, "" + case h.BuildAlone && h.FP != w.FP && m.Release != nil: + // A move of the build alone, to the build an approved release carries: that Approve is the yes (ADR 0601 §1), + // named in the want. A move that changes a setting or a secret too, or one the node could not tell, is asked. + if r, ok := m.Release(w.FP); ok { + yes, release = h.Waits, r + break + } + p.recordAsk(ctx, module, w.FP, assign.RecordNeed{Node: node, Digest: h.Waits, Judgement: h.Judgement, Words: h.Detail}) + default: p.recordAsk(ctx, module, w.FP, assign.RecordNeed{Node: node, Digest: h.Waits, Judgement: h.Judgement, Words: h.Detail}) } } // A want carried over from before the node composed its own may still name the controller's composition; a placed // node's want names the build alone (ADR 0547 §2). - if yes != w.Yes || w.Composed != "" || !w.Self { - w.Yes, w.Composed, w.Self = yes, "", true + if yes == "" { + release = "" + } + if yes != w.Yes || release != w.Release || w.Composed != "" || !w.Self { + w.Yes, w.Release, w.Composed, w.Self = yes, release, "", true out.Wants[node] = w } } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go index a527a890..dd4c767f 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go @@ -788,6 +788,62 @@ func mustKey(key string, err error) string { func TestARecordAskOfAHeldBackNodeSurvivesPassesAndItsYesMovesTheWant(t *testing.T) { // A placed node's yes comes from its have (issue 1126): folded here to hold the count. aPlacedNodesWantsNameTheBuildAloneAndItsYesComesFromItsHave(t) + aReleasesApproveAnswersTheMoveOfItsBuildAlone(t) +} + +// ADR 0601 §1: a placed node's have waiting for a yes to a move of the build alone, to a build an approved release carries, is +// given its yes by that Approve, the want naming the release, and no record ask is raised; a move that changes a setting or a +// secret too, one at the build it runs, and one no approved release carries are asked of a person as before. +func aReleasesApproveAnswersTheMoveOfItsBuildAlone(t *testing.T) { + fpOld := delivery.SourcePrefix2 + strings.Repeat("a", 64) + waiting, was := "sha256:"+strings.Repeat("e", 64), "sha256:"+strings.Repeat("d", 64) + commit := strings.Repeat("1", 40) + for _, c := range []struct { + name string + alone bool + at string // the fingerprint the have runs + mergedAt string // the commit the release merged web at; "" for no merge of the release + yes, asked bool + }{ + {"the build alone, its release approved", true, fpOld, commit, true, false}, + {"a setting changed too", false, fpOld, commit, false, true}, + {"a setting changed at the build it runs", true, fpTest, commit, false, true}, + {"no approved release carries it", true, fpOld, "", false, true}, + {"the release merged another commit", true, fpOld, strings.Repeat("2", 40), false, true}, + } { + asksKV := &memKV{values: map[string][]byte{}} + p := newPasser(buckets{delivery.AssignerAsksBucket: asksKV}, time.Now()) + p.logf = t.Logf + mod, _ := delivery.EncodeModule(delivery.Module{FP2: fpTest, Repository: "novox/web", Commit: commit, Src: 7, Path: delivery.RootPath}) + p.w.put(delivery.ModulesBucket, "web", mod) + src, _ := delivery.EncodeSource(delivery.Source{Group: "feat/web", Heads: map[string]string{"novox/web": commit}}) + p.w.putEntry(delivery.SourceBucket, delivery.SourceMainKey, src, 7, time.Now()) + later, _ := delivery.EncodeSource(delivery.Source{Group: "feat/other", Heads: map[string]string{"novox/web": commit, "novox/x": commit}}) + p.w.putEntry(delivery.SourceBucket, delivery.SourceMainKey, later, 8, time.Now()) // a later merge leaves revision 7's release + if c.mergedAt != "" { + merge, err := delivery.EncodeMerge(delivery.Merge{Group: "feat/web", Ask: "merge.feat_web.0123456789ab", Pulls: []delivery.MergePull{ + {Head: delivery.Head{Repository: "novox/web", Number: 3, Head: strings.Repeat("3", 40)}, State: delivery.MergeMerged, Commit: c.mergedAt}}}) + if err != nil { + t.Fatal(err) + } + p.w.put(delivery.MergesBucket, "feat/web", merge) + } + have, _ := delivery.EncodeHave(delivery.Have{FP: c.at, Composed: was, Phase: delivery.PhaseWaitingYes, Health: delivery.HealthOK, + Judgement: delivery.JudgedRecreates, Waits: waiting, Detail: "it recreates web.server", BuildAlone: c.alone}) + p.w.put(delivery.HaveBucket, "g14.web", have) + m := assign.Module{Name: "web", FP: fpTest, Nodes: []string{"g14"}, Self: func(string) bool { return true }, + Release: func(fp string) (string, bool) { return p.w.release("web", fp) }} + out := assign.Outcome{Wants: map[string]delivery.Want{"g14": {FP: fpTest, Self: true}}} + p.yeses(context.Background(), "web", m, map[string]delivery.Want{"g14": {FP: fpOld, Self: true}}, &out) + w := out.Wants["g14"] + if got := w.Yes == waiting && w.Release == "feat/web"; got != c.yes || (!c.yes && (w.Yes != "" || w.Release != "")) { + t.Errorf("%s: the want is %+v", c.name, w) + } + akey, _ := delivery.RecordAskKey("g14", "web", waiting) + if _, there := asksKV.values[akey]; there != c.asked { + t.Errorf("%s: a record ask raised %v, want %v", c.name, there, c.asked) + } + } } // 774/S26/AC1 (ADR 0549 §5, ADR 0567 §8): the seat's placement alone names where modules run: no controller state is read, so a diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go index 2277786d..751b6c2c 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go @@ -37,6 +37,9 @@ type world struct { // placement is the seat's state placement assembled node by node from its keys in either form (mesh-sdk // delivery.PlacementKeys; research 072 row 37): the one reader of its keys in the assigner. placement *delivery.PlacementKeys + // sourceGroups is, by revision of the gate's source.main, the release (group) that moved it, from every revision its + // history keeps (watched with history): a build's source revision names the release that carries it (ADR 0601 §1). + sourceGroups map[uint64]string } // entryMeta is a value's revision in its bucket and when it was written. @@ -54,7 +57,8 @@ type parsed struct { func newWorld() *world { return &world{values: map[string]map[string][]byte{}, manifests: map[string]parsed{}, healthy: map[string][]string{}, changes: map[string]uint64{}, - meta: map[string]entryMeta{}, trunkBuilt: map[string]delivery.BuildLogEntry{}, placement: delivery.NewPlacementKeys()} + meta: map[string]entryMeta{}, trunkBuilt: map[string]delivery.BuildLogEntry{}, placement: delivery.NewPlacementKeys(), + sourceGroups: map[uint64]string{}} } // putEntry is put, keeping the value's revision and the time it was written. @@ -70,6 +74,43 @@ func (w *world) putEntry(bucket, key string, value []byte, revision uint64, at t if bucket == delivery.PlacementBucket { _, _ = w.placement.Put(key, value, revision, false) // put took it already; this is its revision } + if bucket == delivery.SourceBucket && key == delivery.SourceMainKey { + if s, err := delivery.DecodeSource(value); err == nil && s.Group != "" { + w.sourceGroups[revision] = s.Group + } + } +} + +// release is the release whose Approve carries module's build fp (ADR 0601 §1), and whether one does: the trunk's value of +// the module names fp, the source revision it was first merged at names the release that moved it, and the gate's merge of +// that release, which it writes on the operator's Approve, holds the module's own trunk merge among its merged pull +// requests. Any of them unknown: none, and the move is asked of a person. +func (w *world) release(module, fp string) (string, bool) { + t, ok := w.trunk()[module] + if !ok || fp == "" || t.FP2 != fp || t.Src == 0 || t.Commit == "" { + return "", false + } + w.mu.Lock() + group := w.sourceGroups[t.Src] + w.mu.Unlock() + key, err := delivery.MergesKey(group) + if group == "" || err != nil { + return "", false + } + raw := w.value(delivery.MergesBucket, key) + if raw == nil { + return "", false + } + merge, err := delivery.DecodeMerge(raw) + if err != nil { + return "", false + } + for _, pull := range merge.Pulls { + if pull.State == delivery.MergeMerged && pull.Commit == t.Commit && delivery.SameRepository(pull.Repository, t.Repository) { + return group, true + } + } + return "", false } // metaOf is the revision and time of the value a bucket holds at key, as the watch said them. diff --git a/modules/mesh-delivery-assigner/internal/assign/wants.go b/modules/mesh-delivery-assigner/internal/assign/wants.go index 629014e9..ab020fca 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants.go @@ -45,6 +45,10 @@ type Module struct { // Allowed says whether the operator answered yes to the record ask about the move of this module on node to the composition // with this digest (ADR 0539 §14). Nil: none did. Allowed func(node, digest string) bool + // Release names the release whose Approve carries the build fp, as mesh-delivery's gate holds it for the release's heads, + // read by the build's source revision (ADR 0601 §1): that Approve is the yes to a move of the build alone. False when no + // approved release is known to carry it. Nil: none is read. + Release func(fp string) (string, bool) // Released is a hold on FP the operator released (ADR 0539 §19): FP is not held again by a health check that failed. Released bool // Restored says whether the operator answered "restore" to the restore ask of this data module on node: it is then set back diff --git a/modules/mesh-delivery-assigner/manifest_test.go b/modules/mesh-delivery-assigner/manifest_test.go index d5e14e06..c594947e 100644 --- a/modules/mesh-delivery-assigner/manifest_test.go +++ b/modules/mesh-delivery-assigner/manifest_test.go @@ -34,8 +34,8 @@ func TestManifest(t *testing.T) { reads := slices.Clone(m.Reads) slices.Sort(reads) // ADR 0593 §8: it composes nothing, so it reads no candidate, held or resolved value, compose step or build log; it reads - // every node's published compositions to compare them. - want := []string{"mesh-delivery-builder.builds", "mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules", + // every node's published compositions to compare them, and the gate's source and merges, for the release that carries a build (ADR 0601 §1). + want := []string{"mesh-delivery-builder.builds", "mesh-delivery-gate.merges", "mesh-delivery-gate.source", "mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-grants.grants", "mesh-host.composed", "mesh-provisioner.provisions", "node-engine.have"} if !reflect.DeepEqual(reads, want) { diff --git a/modules/mesh-delivery-assigner/module.json b/modules/mesh-delivery-assigner/module.json index 3c7deb4b..86ee65ec 100644 --- a/modules/mesh-delivery-assigner/module.json +++ b/modules/mesh-delivery-assigner/module.json @@ -25,6 +25,8 @@ ], "reads": [ "mesh-delivery-builder.builds", + "mesh-delivery-gate.merges", + "mesh-delivery-gate.source", "mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules", "mesh-delivery-teller.answers", From b342c67b197880469490c882be9a287aa7ec90bb Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 13:27:39 +0100 Subject: [PATCH 09/49] =?UTF-8?q?The=20grants=20part=20knows=20the=20tool?= =?UTF-8?q?=20runner=20by=20what=20its=20manifest=20says,=20never=20by=20i?= =?UTF-8?q?ts=20name,=20and=20keeps=20a=20node's=20tool=20runner=20user=20?= =?UTF-8?q?as=20the=20list=20in=20force=20holds=20it=20while=20no=20manife?= =?UTF-8?q?st=20there=20says=20it=20yet,=20said=20in=20the=20held=20condit?= =?UTF-8?q?ion=20(ADR=200601=20=C2=A74;=20the=20prepare's=20seams=20findin?= =?UTF-8?q?g=20on=20mesh-grants's=20own=20name)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/mesh-grants/grants_test.go | 28 +++++++++++++++++-- .../internal/buscompose/buscompose_test.go | 8 +++--- .../buscompose/creates_streams_test.go | 2 +- .../mesh-grants/internal/buscompose/nats.go | 17 +++++------ .../mesh-grants/internal/buscompose/users.go | 14 ++++++---- .../mesh-grants/internal/buscompose/waits.go | 8 +++--- modules/mesh-grants/internal/grants/held.go | 25 +++++++++++++++++ modules/mesh-grants/internal/grants/part.go | 3 ++ .../mesh-grants/internal/grants/records.go | 2 +- 9 files changed, 81 insertions(+), 26 deletions(-) diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 90ffeaed..88deec86 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -599,7 +599,7 @@ func TestGrants(t *testing.T) { b := fixture() b.manifestOf("mod-z", `{"module":"mod-z","version":"1","own-secrets":{"broker":"/var/lib/z/broker"}`+row.build+`}`) b.assign("a", "mod-z") - b.manifestOf("node-tools", `{"module":"node-tools","version":"1","own-secrets":{"broker":"/var/lib/nt/broker"}}`) + b.manifestOf("node-tools", `{"module":"node-tools","version":"1","tool-runner":true,"own-secrets":{"broker":"/var/lib/nt/broker"}}`) b.assign("a", "node-tools") users := shadowOf(t, b) tools := users[slices.IndexFunc(users, func(u grants.User) bool { return u.User == "a.node-tools" })] @@ -1255,7 +1255,7 @@ func TestGrants(t *testing.T) { n.manifestOf("claude-licence-manager", string(licence)) n.assign("novox", "nats") n.assign("novox", "claude-licence-manager") - n.manifestOf("node-tools", `{"module":"node-tools","version":"1"}`) // the machine's tool runner, which carries the seat + n.manifestOf("node-tools", `{"module":"node-tools","version":"1","tool-runner":true}`) // the machine's tool runner, which carries the seat n.assign("novox", "node-tools") // The live placement names mesh-host; with its manifest's state the runner once carried it and the whole list was // refused as a second writer of the engine's health (issue 774, 2026-10-30): the shadow below now composes. @@ -2839,6 +2839,30 @@ func TestGrants(t *testing.T) { if p.Compose(); said[len(said)-1].State != asks.StateCleared { t.Errorf("the condition was not cleared once the manifest is accepted: %+v", said[len(said)-1]) } + // ADR 0601 §4: a tool runner whose manifest does not say so yet (an older build's) keeps its user as the list in force + // holds it, known by what it runs (it says the runner is alive), and the held condition says so; never composed as a + // plain module. + const runner = `{"module":"node-tools","version":"1","tool-runner":true,"own-secrets":{"broker":"/var/lib/nt/broker"}}` + b.manifestOf("node-tools", runner) + b.assign("a", "node-tools") + b.put(grants.ReadAccounts, "a.node-tools", map[string]any{"user": "a.node-tools", "hash": "$2a$10$first", "generation": 1, "fingerprint": "sha256:1"}) + p.Compose() + inForce, _, _ = p.Sealed() + ran, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(inForce.Digest, "a.node-tools")) + if !ok || !strings.Contains(string(ran), "mesh.control.a.tools-alive") { + t.Fatalf("the list in force holds no tool runner on a: %s", ran) + } + b.manifestOf("node-tools", strings.Replace(runner, `"tool-runner":true,`, "", 1)) + b.put(grants.ReadAccounts, "a.mod-x", map[string]any{"user": "a.mod-x", "hash": "$2a$10$gen10", "generation": 10, "fingerprint": "sha256:10"}) + p.Compose() + now, _, _ := p.Sealed() + kept, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(now.Digest, "a.node-tools")) + if !ok || string(kept) != string(ran) { + t.Errorf("the tool runner of a was composed without what it runs: %s, was %s", kept, ran) + } + if last := said[len(said)-1]; last.State != asks.StateRaised || !strings.Contains(last.Summary, "a.node-tools (stays in the list") { + t.Errorf("the kept tool runner was not said: %+v", last) + } }) } diff --git a/modules/mesh-grants/internal/buscompose/buscompose_test.go b/modules/mesh-grants/internal/buscompose/buscompose_test.go index 32953ed9..2b00cff9 100644 --- a/modules/mesh-grants/internal/buscompose/buscompose_test.go +++ b/modules/mesh-grants/internal/buscompose/buscompose_test.go @@ -129,7 +129,7 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { ByCaller: []string{"ask", "cancel", "decided"}, Records: []string{"messenger_asks"}} users, err := Users(Records{Nodes: []string{"one", "two"}, Assigned: map[string][]Declared{ "one": {{Module: "messenger", Holds: []Seat{router}, Invokes: []string{"seat:" + EngineSeat + ".root-free"}}}, - "two": {{Module: RuntimeModule}, {Module: "shop", Invokes: []string{"*"}}}}}) + "two": {{Module: "node-tools", ToolRunner: true}, {Module: "shop", Invokes: []string{"*"}}}}}) if err != nil { t.Fatal(err) } @@ -361,7 +361,7 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { {Module: "mesh-enrolment", RunsAs: "mesh-enrolment", Holds: []Seat{holder}, Invokes: calls, State: []Bucket{{Module: "mesh-enrolment", Name: "enrolments", KeyTTL: true}}}, {Module: "shop", Invokes: calls[:2]}, - {Module: RuntimeModule}, {Module: "agent-tools", Invokes: []string{"*"}}}}}) + {Module: "node-tools", ToolRunner: true}, {Module: "agent-tools", Invokes: []string{"*"}}}}}) if err != nil { t.Fatal(err) } @@ -782,7 +782,7 @@ func healthChecks(t *testing.T) { } } // Nor the runtime carrying the node's modules, which reads each one's membership from the state. - if perms, err := PermissionsFor(Principal{Kind: KindNodeTools, Node: "a", Module: RuntimeModule, + if perms, err := PermissionsFor(Principal{Kind: KindNodeTools, Node: "a", Module: "node-tools", Carries: []Declared{{Module: "restic"}}}); err != nil || MaySubscribe(perms, "mesh.assignment.a.restic") || !MayPublish(perms, "$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.a.restic") { t.Errorf("the runtime hears mesh.assignment or reads no membership: %v", err) @@ -926,7 +926,7 @@ func everyNodesCompositions(t *testing.T) { t.Error("the read is granted as one bucket mesh-host_composed") } // A bundle reading them (the provisioner, carried by the machine's runtime) reads them on the runtime's connection. - carried := Principal{Kind: KindNodeTools, Node: "one", Module: RuntimeModule, Nodes: []string{"one", "two"}, + carried := Principal{Kind: KindNodeTools, Node: "one", Module: "node-tools", Nodes: []string{"one", "two"}, Carries: []Declared{{Module: "mesh-provisioner", Reads: []string{delivery.ComposedRead}}}} runner, err := PermissionsFor(carried) if err != nil { diff --git a/modules/mesh-grants/internal/buscompose/creates_streams_test.go b/modules/mesh-grants/internal/buscompose/creates_streams_test.go index 35937977..7c99a208 100644 --- a/modules/mesh-grants/internal/buscompose/creates_streams_test.go +++ b/modules/mesh-grants/internal/buscompose/creates_streams_test.go @@ -21,7 +21,7 @@ func theGrantsPartCreatesOnlyDeclaredStreams(t *testing.T) { Logs: []Log{{Module: "mesh-delivery-builder", Name: "builds"}}, Streams: []ModuleStream{{Module: "mesh-delivery-builder", Name: "BUILDASKS", Events: []string{"ask"}}}}, {Module: GrantsModule, State: []Bucket{{Module: GrantsModule, Name: "assertions"}}, Reads: []string{VaultBusAccounts}}, - {Module: RuntimeModule}, + {Module: "node-tools", ToolRunner: true}, // The node-engine's manifest as a placement names it (issue 774, met twice on 2026-10-30): no account of its // own, so the runner would carry it, and its state is the node's to write alone. {Module: EngineModule, NoAccount: true, Consumes: []string{"restic.backed-up"}, diff --git a/modules/mesh-grants/internal/buscompose/nats.go b/modules/mesh-grants/internal/buscompose/nats.go index 93482882..d8314b75 100644 --- a/modules/mesh-grants/internal/buscompose/nats.go +++ b/modules/mesh-grants/internal/buscompose/nats.go @@ -58,12 +58,13 @@ const ( KindLedger Kind = "ledger" ) -// RuntimeModule is the module that IS the node's tool runtime (novox/hq ADR 0175). Where it is -// assigned, the mesh composes one runtime principal for the machine in place of that module's own, -// and the per-module containers that served tools until then stop being the way tools reach a node. -// Mirrored in the catalogue package, which the agreement test holds to the same string; one -// constant, so a rename is one edit and the two packages cannot drift. -const RuntimeModule = "node-tools" +// The node's tool runner (novox/hq ADR 0175) is the module whose manifest says it is (`tool-runner`, Declared.ToolRunner; +// ADR 0601 §4), never a name here. Where it is assigned, the mesh composes one runtime principal for the machine in place of +// that module's own, and the per-module containers that served tools until then stop being the way tools reach a node. + +// ToolsAliveSubject is what a node's tool runner says it is alive on: granted to the runner alone, so a user of the list in +// force that may publish it is that node's tool runner (ADR 0601 §4's keeping of what runs). +func ToolsAliveSubject(node string) string { return "mesh.control." + node + ".tools-alive" } // Seat is a role on the bus as a principal relates to it: the subjects it accepts, and those it // emits (novox/hq ADR 0118, design 29 §5). @@ -999,7 +1000,7 @@ func PermissionsFor(p Principal) (Permissions, error) { pub = append(pub, readGrants(GrantsMembershipsBucket, p.Node+".*")...) // And that it is there (novox/hq to-be 45 §3, S11): its own heartbeat, under its machine's // name and no other's, on core NATS like the host's. - pub = append(pub, "mesh.control."+p.Node+".tools-alive") + pub = append(pub, ToolsAliveSubject(p.Node)) // And every tool on the mesh (ADR 0175, decision 5): any node may call any tool on any // node, as the console already could — the runtime is the console's serving mode. invoked, err := invokedSubjects([]string{"*"}) @@ -1016,7 +1017,7 @@ func PermissionsFor(p Principal) (Permissions, error) { // discovery for each module and seat it carries, and the console it is asks the bus. // One service per runtime process, named for the runtime: the bus lets a principal answer each // request once, so the runtime announces everything it carries under its own name. - sub = append(sub, announcing(append([]string{RuntimeModule}, serves...)...)...) + sub = append(sub, announcing(append([]string{p.Module}, serves...)...)...) pub = append(pub, discovering()...) // **And it consumes for the modules it carries** (novox/hq ADR 0198, which changes ADR 0175's // "it consumes nothing"): a module's long-running code is a bundle this runtime launches, and diff --git a/modules/mesh-grants/internal/buscompose/users.go b/modules/mesh-grants/internal/buscompose/users.go index cbf0a027..5a667a96 100644 --- a/modules/mesh-grants/internal/buscompose/users.go +++ b/modules/mesh-grants/internal/buscompose/users.go @@ -26,6 +26,8 @@ import ( // Declared is one module on one node, as composing its authority needs it. type Declared struct { Module string + // ToolRunner says the module is the node's tool runner, as its manifest says (`tool-runner`, ADR 0601 §4). + ToolRunner bool // Objects are its own object buckets, and ReadsObjects the bus names of the object buckets of others its reads name // and their owners' grants let it read (novox/hq ADR 0592 §5, §9). Objects []ObjectBucket @@ -202,10 +204,10 @@ func Users(r Records) ([]Principal, error) { // node: its serving grants are the union of theirs. Every other module keeps its own // principal — a module still serving tools from its own container holds its own // credential until it moves, and the two serve side by side in the meantime. - runtimeHere := false + runtimeHere, runner := false, "" for _, d := range r.Assigned[node] { - if d.Module == RuntimeModule { - runtimeHere = true + if d.ToolRunner { + runtimeHere, runner = true, d.Module } } for _, d := range r.Assigned[node] { @@ -237,7 +239,7 @@ func Users(r Records) ([]Principal, error) { if len(d.Ledgers) > 0 { out = append(out, Principal{Kind: KindLedger, Node: node, Module: d.Module, Ledgers: sortedCopy(d.Ledgers)}) } - if runtimeHere && d.Module == RuntimeModule { + if runtimeHere && d.ToolRunner { continue } // **A module with nowhere to read an account is no user** (novox/hq issue 195). `module @@ -284,7 +286,7 @@ func Users(r Records) ([]Principal, error) { carried = append(carried, d) } } - out = append(out, Principal{Kind: KindNodeTools, Node: node, Module: RuntimeModule, Carries: carried}) + out = append(out, Principal{Kind: KindNodeTools, Node: node, Module: runner, Carries: carried}) } } // A node enrols through the seat mesh-enrolment alone (ADR 0587 §6, ADR 0590): while no holder serves redeem, an @@ -525,7 +527,7 @@ func CarriedByRunner(d Declared) bool { return Composed(d) && d.RunsAs == "" && // stays carried, and so does the runtime module itself. Whether the module has its own broker secret is not // the test: a module with tools and its own credential is still carried (ADR 0175). func runnerServes(d Declared) bool { - return d.Module == RuntimeModule || d.NoAccount || d.Loads || len(d.Serves) > 0 || len(d.Holds) > 0 || len(d.Waits) > 0 || len(d.Checks) > 0 + return d.ToolRunner || d.NoAccount || d.Loads || len(d.Serves) > 0 || len(d.Holds) > 0 || len(d.Waits) > 0 || len(d.Checks) > 0 } // declaredStreamsOf is the stream of every bucket, log and stream a module placed anywhere declares, once each, in name diff --git a/modules/mesh-grants/internal/buscompose/waits.go b/modules/mesh-grants/internal/buscompose/waits.go index c92300ed..1fb8616b 100644 --- a/modules/mesh-grants/internal/buscompose/waits.go +++ b/modules/mesh-grants/internal/buscompose/waits.go @@ -62,9 +62,9 @@ func waitGrants(carries []Declared) ([]string, error) { if len(d.Waits) == 0 { continue } - if d.Module != RuntimeModule { - return nil, fmt.Errorf("%s declares what a session waits on; only %s, the tool runner's module, "+ - "does (novox/hq ADR 0367 §2)", d.Module, RuntimeModule) + if !d.ToolRunner { + return nil, fmt.Errorf("%s declares what a session waits on; only the tool runner's module (its manifest says "+ + "tool-runner) does (novox/hq ADR 0367 §2)", d.Module) } for _, w := range d.Waits { s, err := WaitSubject(w) @@ -79,7 +79,7 @@ func waitGrants(carries []Declared) ([]string, error) { // waitsIssuedFor is the families a membership names: the tool runner's module's, and nobody else's. func waitsIssuedFor(d Declared) []WaitIssued { - if d.Module != RuntimeModule { + if !d.ToolRunner { return nil } var out []WaitIssued diff --git a/modules/mesh-grants/internal/grants/held.go b/modules/mesh-grants/internal/grants/held.go index 1b970744..1a283593 100644 --- a/modules/mesh-grants/internal/grants/held.go +++ b/modules/mesh-grants/internal/grants/held.go @@ -2,9 +2,12 @@ package grants import ( "fmt" + "slices" "sort" "strings" + "mesh-grants/internal/buscompose" + "git.novox.be/novox/mesh-sdk/go/asks" ) @@ -54,6 +57,28 @@ func heldModuleUsers(in Inputs, candidates []string) map[string]string { return out } +// runnerUsersKept are, by user, why it is kept: on a node where no placed module's manifest says it is the tool runner +// (`tool-runner`), the user of the list in force that is that node's tool runner, known by what runs (it may say the runner +// is alive, buscompose.ToolsAliveSubject, which the runner alone is granted), never by a module's name (ADR 0601 §4). A +// manifest that does not yet say what shared code no longer infers keeps the runner as it runs, said in the held module's +// condition, and is never composed without it. Pure. +func runnerUsersKept(recs buscompose.Records, inForce map[string]User) map[string]string { + out := map[string]string{} + for node, placed := range recs.Assigned { + if slices.ContainsFunc(placed, func(d buscompose.Declared) bool { return d.ToolRunner }) { + continue + } + alive := buscompose.ToolsAliveSubject(node) + for user, u := range inForce { + if strings.HasPrefix(user, node+".") && slices.Contains(u.Publish, alive) { + out[user] = fmt.Sprintf("no manifest placed on %s says it is the tool runner (tool-runner), so the tool runner keeps "+ + "what it runs until its manifest says so", node) + } + } + } + return out +} + // inForceGrants are the grants of each user of the list in force as the part sealed them, or nil where it kept none. func (p *Part) inForceGrants() map[string]User { if g, _, had := p.Sealed(); had && g.Digest != "" && p.Store != nil { diff --git a/modules/mesh-grants/internal/grants/part.go b/modules/mesh-grants/internal/grants/part.go index bb0a8b30..ed644c8d 100644 --- a/modules/mesh-grants/internal/grants/part.go +++ b/modules/mesh-grants/internal/grants/part.go @@ -271,6 +271,9 @@ func (p *Part) Compose() Result { candidates = append(candidates, user) } heldUsers := heldModuleUsers(in, candidates) + for user, why := range runnerUsersKept(listRecs, inForce) { + heldUsers[user] = why + } allRefused := mergedRefusals(refused, heldUsers) kept, _ := p.keptOf(allRefused) for _, user := range sortedKeys(refused) { diff --git a/modules/mesh-grants/internal/grants/records.go b/modules/mesh-grants/internal/grants/records.go index 62d42424..c93bf116 100644 --- a/modules/mesh-grants/internal/grants/records.go +++ b/modules/mesh-grants/internal/grants/records.go @@ -132,7 +132,7 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio fromModules = append(fromModules, c) } d := buscompose.Declared{ - Module: m.Module, Emits: m.EmitsAll(), Consumes: fromModules, Watches: watches, Serves: m.Tools, + Module: m.Module, ToolRunner: catalogue.IsToolRunner(m), Emits: m.EmitsAll(), Consumes: fromModules, Watches: watches, Serves: m.Tools, Invokes: m.Invokes, Waits: waitsOf(m, seats), State: bucketsOf(m), Reads: m.Reads, Logs: logsOf(m), Streams: streamsOf(m), Queues: queueOwnersOf(m), Processes: processesOf(m), Checks: catalogue.HealthChecks(m), RunsAs: m.RunsAs, Issued: issuedTools(m), From 6b9d211f720907e98b23bf0e3232ada88a104c7b Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 13:28:29 +0100 Subject: [PATCH 10/49] =?UTF-8?q?The=20checker=20decides=20a=20node=20the?= =?UTF-8?q?=20sandbox=20did=20not=20judge=20by=20the=20contract's=20field?= =?UTF-8?q?=20(composer.Machine.NotJudged,=20verdict=20not-judged)=20and?= =?UTF-8?q?=20names=20it=20with=20the=20composer's=20words,=20never=20by?= =?UTF-8?q?=20a=20phrase=20in=20its=20reason=20(ADR=200593=20=C2=A79;=20th?= =?UTF-8?q?e=20prepare's=20seams=20finding=20on=20"not=20judged")?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/check/engines.go | 14 +++++++++++++- .../internal/check/engines_test.go | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/modules/mesh-delivery-checker/internal/check/engines.go b/modules/mesh-delivery-checker/internal/check/engines.go index 64c8579b..2e077789 100644 --- a/modules/mesh-delivery-checker/internal/check/engines.go +++ b/modules/mesh-delivery-checker/internal/check/engines.go @@ -147,6 +147,12 @@ func JudgeEngines(group, heads, composerName string, nodes []string, answers map v.Warnings = append(v.Warnings, node+": "+delivery.ComposeNoAnswer) continue } + if a.Verdict == delivery.ComposeCandidateNotJudged { + // Composed in the sandbox and not judged (ADR 0593 §9): named as no answer, with the composer's words; decided by the + // verdict, never by the words (ADR 0601 §4's kind). + v.Warnings = append(v.Warnings, node+": "+delivery.ComposeNoAnswer+" ("+a.Refused+")") + continue + } v.Machines = append(v.Machines, node) if a.Verdict != delivery.ComposeCandidateRefused { continue @@ -221,8 +227,11 @@ func (e EngineCompositions) inSandbox(ctx context.Context, c *Case, head, node s return a, fmt.Errorf("the composer at the node-engine's head %s did not answer for %s: %w", head, node, err) } out := delivery.ComposeCandidateAnswer{V: a.V, Node: node, Candidate: a.Candidate, Verdict: delivery.ComposeCandidateComposed} + notJudged := "" for _, cv := range verdict.Cases { - if m, ok := cv.Machines[node]; ok && !m.Composes { + if m, ok := cv.Machines[node]; ok && m.NotJudged != "" { + notJudged = m.NotJudged // the composer did not judge the node: neither composed nor refused (ADR 0593 §9) + } else if ok && !m.Composes { out.Verdict = delivery.ComposeCandidateRefused out.Refused = strings.Join(append(append([]string{m.Reason}, m.Problems...), m.Refusals...), "; ") } @@ -234,5 +243,8 @@ func (e EngineCompositions) inSandbox(ctx context.Context, c *Case, head, node s } } sort.Slice(out.Modules, func(i, j int) bool { return out.Modules[i].Module < out.Modules[j].Module }) + if notJudged != "" && out.Verdict == delivery.ComposeCandidateComposed { + out.Verdict, out.Refused = delivery.ComposeCandidateNotJudged, notJudged + } return out, nil } diff --git a/modules/mesh-delivery-checker/internal/check/engines_test.go b/modules/mesh-delivery-checker/internal/check/engines_test.go index 52e98c68..fed2774f 100644 --- a/modules/mesh-delivery-checker/internal/check/engines_test.go +++ b/modules/mesh-delivery-checker/internal/check/engines_test.go @@ -168,6 +168,25 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { if v.State != delivery.CompositionFail || v.SDK != head2 || !strings.Contains(v.Words, "the new composer refuses it") { t.Errorf("verdict = %+v", v) } + // ADR 0593 §9 (ADR 0601 §4's kind): a node the composer did not judge is named as no answer with its words, by the + // contract's field, and the check passes; words in Reason alone are no such mark. + sb.err = nil + for _, row := range []struct { + m composer.Machine + pass bool + }{ + {composer.Machine{NotJudged: "its node-engine sends no running state for web"}, true}, + {composer.Machine{Reason: "not judged: its node-engine sends no running state for web"}, false}, + } { + sb.v = func(node string) composer.Verdict { + return composer.Verdict{V: composer.Version, Cases: []composer.CaseVerdict{{Name: "change", Machines: map[string]composer.Machine{node: row.m}}}} + } + v, err = ec.Compose(context.Background(), host) + named := len(v.Warnings) == 1 && strings.Contains(v.Warnings[0], "anchor: "+delivery.ComposeNoAnswer+" (its node-engine sends no running state for web)") + if err != nil || (v.State == delivery.CompositionPass) != row.pass || named != row.pass { + t.Errorf("%+v: verdict %+v, %v", row.m, v, err) + } + } // Review finding 3 (ADR 0593 §9 lets only a node that does not answer pass): a composer at the head that cannot // run fails the composition, naming the head, never passing as a silent node. sb.err = errors.New("the build of mesh-compose failed") From ea5be10532579fda5ae5df156b6a85cf90d1aead Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 13:46:47 +0100 Subject: [PATCH 11/49] Pin mesh-sdk 09ec82cb3551, the group's head of feat/l3b-bus (ADR 0601) --- modules/mesh-delivery-assigner/go.mod | 2 +- modules/mesh-delivery-assigner/go.sum | 4 ++-- modules/mesh-delivery-builder/go.mod | 2 +- modules/mesh-delivery-builder/go.sum | 4 ++-- modules/mesh-delivery-checker/go.mod | 2 +- modules/mesh-delivery-checker/go.sum | 4 ++-- modules/mesh-delivery-forge-bridge/go.mod | 2 +- modules/mesh-delivery-forge-bridge/go.sum | 4 ++-- modules/mesh-delivery-teller/go.mod | 2 +- modules/mesh-delivery-teller/go.sum | 4 ++-- modules/mesh-delivery-view/go.mod | 2 +- modules/mesh-delivery-view/go.sum | 4 ++-- modules/mesh-delivery-watchman/go.mod | 2 +- modules/mesh-delivery-watchman/go.sum | 4 ++-- modules/mesh-enrolment/go.mod | 2 +- modules/mesh-enrolment/go.sum | 4 ++-- 16 files changed, 24 insertions(+), 24 deletions(-) diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index c9e96c42..2c5e40b6 100644 --- a/modules/mesh-delivery-assigner/go.mod +++ b/modules/mesh-delivery-assigner/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-assigner go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-assigner/go.sum b/modules/mesh-delivery-assigner/go.sum index 21738611..271c7949 100644 --- a/modules/mesh-delivery-assigner/go.sum +++ b/modules/mesh-delivery-assigner/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 h1:DAxLU3IjZ9Kof8F3kVnINcauLqzCdQ1smwMpzX8yi7I= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/go.mod b/modules/mesh-delivery-builder/go.mod index 0f3ceef0..f22b87b5 100644 --- a/modules/mesh-delivery-builder/go.mod +++ b/modules/mesh-delivery-builder/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-builder go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/go.sum b/modules/mesh-delivery-builder/go.sum index 08770b61..ad942d9c 100644 --- a/modules/mesh-delivery-builder/go.sum +++ b/modules/mesh-delivery-builder/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 h1:DAxLU3IjZ9Kof8F3kVnINcauLqzCdQ1smwMpzX8yi7I= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index d74c83c7..00c23872 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-checker go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-checker/go.sum b/modules/mesh-delivery-checker/go.sum index 08770b61..ad942d9c 100644 --- a/modules/mesh-delivery-checker/go.sum +++ b/modules/mesh-delivery-checker/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 h1:DAxLU3IjZ9Kof8F3kVnINcauLqzCdQ1smwMpzX8yi7I= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-forge-bridge/go.mod b/modules/mesh-delivery-forge-bridge/go.mod index 60ad48c5..8513b089 100644 --- a/modules/mesh-delivery-forge-bridge/go.mod +++ b/modules/mesh-delivery-forge-bridge/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-forge-bridge go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-forge-bridge/go.sum b/modules/mesh-delivery-forge-bridge/go.sum index 21738611..271c7949 100644 --- a/modules/mesh-delivery-forge-bridge/go.sum +++ b/modules/mesh-delivery-forge-bridge/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 h1:DAxLU3IjZ9Kof8F3kVnINcauLqzCdQ1smwMpzX8yi7I= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-teller/go.mod b/modules/mesh-delivery-teller/go.mod index 821d3f97..2143e487 100644 --- a/modules/mesh-delivery-teller/go.mod +++ b/modules/mesh-delivery-teller/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-teller go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-teller/go.sum b/modules/mesh-delivery-teller/go.sum index 21738611..271c7949 100644 --- a/modules/mesh-delivery-teller/go.sum +++ b/modules/mesh-delivery-teller/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 h1:DAxLU3IjZ9Kof8F3kVnINcauLqzCdQ1smwMpzX8yi7I= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index cf0fb7c4..5138e158 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-view go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index 21738611..271c7949 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 h1:DAxLU3IjZ9Kof8F3kVnINcauLqzCdQ1smwMpzX8yi7I= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-watchman/go.mod b/modules/mesh-delivery-watchman/go.mod index 66433c0d..1eb115ff 100644 --- a/modules/mesh-delivery-watchman/go.mod +++ b/modules/mesh-delivery-watchman/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-watchman go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-watchman/go.sum b/modules/mesh-delivery-watchman/go.sum index 1c98fd23..bd3f7f22 100644 --- a/modules/mesh-delivery-watchman/go.sum +++ b/modules/mesh-delivery-watchman/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 h1:DAxLU3IjZ9Kof8F3kVnINcauLqzCdQ1smwMpzX8yi7I= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-enrolment/go.mod b/modules/mesh-enrolment/go.mod index 678fdaa3..04f71c8a 100644 --- a/modules/mesh-enrolment/go.mod +++ b/modules/mesh-enrolment/go.mod @@ -3,7 +3,7 @@ module mesh-enrolment go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-enrolment/go.sum b/modules/mesh-enrolment/go.sum index 21738611..271c7949 100644 --- a/modules/mesh-enrolment/go.sum +++ b/modules/mesh-enrolment/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 h1:DAxLU3IjZ9Kof8F3kVnINcauLqzCdQ1smwMpzX8yi7I= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= From a41d2851c7a3a95aa11de464737080d591ffc6e3 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 14:02:01 +0100 Subject: [PATCH 12/49] =?UTF-8?q?A=20release=20answers=20only=20the=20buil?= =?UTF-8?q?d=20whose=20fingerprint=20it=20moved:=20the=20indexer=20stamps?= =?UTF-8?q?=20the=20source=20revision=20at=20which=20a=20fingerprint=20las?= =?UTF-8?q?t=20changed,=20a=20recompute=20with=20no=20merge=20the=20revisi?= =?UTF-8?q?on=20of=20its=20moment,=20and=20the=20assigner=20reads=20that?= =?UTF-8?q?=20stamp,=20never=20the=20Src=20a=20recompute=20keeps=20(ADR=20?= =?UTF-8?q?0601=20=C2=A71;=20review=20round=201,=20finding=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmd/mesh-delivery-assigner/pass_test.go | 23 ++++++++++++++----- .../cmd/mesh-delivery-assigner/world.go | 11 +++++---- .../internal/index/source.go | 10 ++++++++ .../internal/index/source_test.go | 2 +- .../internal/index/toolchains.go | 14 ++++++++++- .../internal/index/toolchains_test.go | 8 +++++++ 6 files changed, 55 insertions(+), 13 deletions(-) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go index dd4c767f..799ef530 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go @@ -803,18 +803,23 @@ func aReleasesApproveAnswersTheMoveOfItsBuildAlone(t *testing.T) { alone bool at string // the fingerprint the have runs mergedAt string // the commit the release merged web at; "" for no merge of the release + fpSrc uint64 // the source revision at which web's fingerprint last changed yes, asked bool }{ - {"the build alone, its release approved", true, fpOld, commit, true, false}, - {"a setting changed too", false, fpOld, commit, false, true}, - {"a setting changed at the build it runs", true, fpTest, commit, false, true}, - {"no approved release carries it", true, fpOld, "", false, true}, - {"the release merged another commit", true, fpOld, strings.Repeat("2", 40), false, true}, + {"the build alone, its release approved", true, fpOld, commit, 7, true, false}, + {"a setting changed too", false, fpOld, commit, 7, false, true}, + {"a setting changed at the build it runs", true, fpTest, commit, 7, false, true}, + {"no approved release carries it", true, fpOld, "", 7, false, true}, + {"the release merged another commit", true, fpOld, strings.Repeat("2", 40), 7, false, true}, + // Review round 1: its fingerprint moved by a recompute after the release (a toolchain, a package), with no merge of + // web: stamped with that moment's revision, whose release carries nothing of web; the earlier Approve answers nothing. + {"a fingerprint moved by a recompute after the release", true, fpOld, commit, 8, false, true}, + {"a fingerprint whose revision is not known", true, fpOld, commit, 0, false, true}, } { asksKV := &memKV{values: map[string][]byte{}} p := newPasser(buckets{delivery.AssignerAsksBucket: asksKV}, time.Now()) p.logf = t.Logf - mod, _ := delivery.EncodeModule(delivery.Module{FP2: fpTest, Repository: "novox/web", Commit: commit, Src: 7, Path: delivery.RootPath}) + mod, _ := delivery.EncodeModule(delivery.Module{FP2: fpTest, Repository: "novox/web", Commit: commit, Src: 7, FPSrc: c.fpSrc, Path: delivery.RootPath}) p.w.put(delivery.ModulesBucket, "web", mod) src, _ := delivery.EncodeSource(delivery.Source{Group: "feat/web", Heads: map[string]string{"novox/web": commit}}) p.w.putEntry(delivery.SourceBucket, delivery.SourceMainKey, src, 7, time.Now()) @@ -827,6 +832,12 @@ func aReleasesApproveAnswersTheMoveOfItsBuildAlone(t *testing.T) { t.Fatal(err) } p.w.put(delivery.MergesBucket, "feat/web", merge) + other, err := delivery.EncodeMerge(delivery.Merge{Group: "feat/other", Ask: "merge.feat_other.0123456789ab", Pulls: []delivery.MergePull{ + {Head: delivery.Head{Repository: "novox/x", Number: 4, Head: strings.Repeat("4", 40)}, State: delivery.MergeMerged, Commit: commit}}}) + if err != nil { + t.Fatal(err) + } + p.w.put(delivery.MergesBucket, "feat/other", other) } have, _ := delivery.EncodeHave(delivery.Have{FP: c.at, Composed: was, Phase: delivery.PhaseWaitingYes, Health: delivery.HealthOK, Judgement: delivery.JudgedRecreates, Waits: waiting, Detail: "it recreates web.server", BuildAlone: c.alone}) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go index 751b6c2c..1f33e577 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go @@ -82,16 +82,17 @@ func (w *world) putEntry(bucket, key string, value []byte, revision uint64, at t } // release is the release whose Approve carries module's build fp (ADR 0601 §1), and whether one does: the trunk's value of -// the module names fp, the source revision it was first merged at names the release that moved it, and the gate's merge of -// that release, which it writes on the operator's Approve, holds the module's own trunk merge among its merged pull -// requests. Any of them unknown: none, and the move is asked of a person. +// the module names fp, the source revision at which that fingerprint last changed (FPSrc, never the Src of its last merge, +// which a recompute with no merge keeps) names the release that moved it, and the gate's merge of that release, which it +// writes on the operator's Approve, holds the module's own trunk merge among its merged pull requests. Any of them +// unknown: none, and the move is asked of a person. func (w *world) release(module, fp string) (string, bool) { t, ok := w.trunk()[module] - if !ok || fp == "" || t.FP2 != fp || t.Src == 0 || t.Commit == "" { + if !ok || fp == "" || t.FP2 != fp || t.FPSrc == 0 || t.Commit == "" { return "", false } w.mu.Lock() - group := w.sourceGroups[t.Src] + group := w.sourceGroups[t.FPSrc] w.mu.Unlock() key, err := delivery.MergesKey(group) if group == "" || err != nil { diff --git a/modules/mesh-delivery-indexer/internal/index/source.go b/modules/mesh-delivery-indexer/internal/index/source.go index f71a3674..b7318bf4 100644 --- a/modules/mesh-delivery-indexer/internal/index/source.go +++ b/modules/mesh-delivery-indexer/internal/index/source.go @@ -14,6 +14,13 @@ import ( // with it (Module.Src) when the head the source names for its repository is the merge commit the module's fingerprint // was computed at: from the modules bucket when the merge was heard first, and when the merge is heard after the source. +// sourceRev is the latest source revision seen, 0 before any. +func (ix *Indexer) sourceRev() uint64 { + ix.mu.Lock() + defer ix.mu.Unlock() + return ix.source.rev +} + // sourceNow is the latest source revision seen. type sourceNow struct { rev uint64 @@ -82,6 +89,9 @@ func (ix *Indexer) ApplySource(ctx context.Context, rev uint64, s delivery.Sourc return nil, false, nil } cur.Src = rev + if cur.FPSrc == 0 { + cur.FPSrc = rev // its fingerprint moved at this merge, whose revision is heard only now (ADR 0601 §1) + } data, err := delivery.EncodeModule(cur) return data, false, err }) diff --git a/modules/mesh-delivery-indexer/internal/index/source_test.go b/modules/mesh-delivery-indexer/internal/index/source_test.go index 45260ab8..8bb56ebd 100644 --- a/modules/mesh-delivery-indexer/internal/index/source_test.go +++ b/modules/mesh-delivery-indexer/internal/index/source_test.go @@ -31,7 +31,7 @@ func TestASourceRevisionStampsTheModulesOfTheRepositoriesHeadItNames(t *testing. } m, ok := moduleOf(t, mods, "a") want := before - want.Src = 7 + want.Src, want.FPSrc = 7, 7 // its fingerprint moved at that merge (ADR 0601 §1) if !ok || !reflect.DeepEqual(m, want) { t.Errorf("module a = %+v (%v), want %+v: the fingerprint and the commit as the merge wrote them, with Src 7", m, ok, want) } diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains.go b/modules/mesh-delivery-indexer/internal/index/toolchains.go index 51f668e4..c8cee117 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains.go @@ -312,6 +312,11 @@ func (ix *Indexer) recomputeModules(ctx context.Context, overlay map[string]deli if cur.FP2 == r.fp && cur.None2 == r.none && reflect.DeepEqual(cur.Toolchains, r.tcs) && reflect.DeepEqual(cur.Packages, r.pkgs) { return nil, false, nil } + if cur.FP2 != r.fp { + // Moved with no merge of its own repository (a toolchain, a package, a base): the source revision of now, whose + // release does not carry it, never the Src of the merge it was last merged at (ADR 0601 §1). + cur.FPSrc = ix.sourceRev() + } cur.FP2, cur.None2, cur.Toolchains, cur.Packages = r.fp, r.none, r.tcs, r.pkgs cur.Inputs2, cur.ArtifactPaths = inputs, paths data, err := delivery.EncodeModule(cur) @@ -355,6 +360,13 @@ func (ix *Indexer) writeOverlay(ctx context.Context, overlay, finals map[string] // refused or outgrown module as accepted; only the judge changes it. next.Verdict, next.Problems, next.Unknown, next.Warnings = cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings } + // The source revision at which FP2 last changed (ADR 0601 §1): this merge's when it moved it (0 until the + // source revision naming the merge is heard, which stamps it), else the one held. + if held && cur.FP2 == next.FP2 { + next.FPSrc = cur.FPSrc + } else { + next.FPSrc = next.Src + } if held && sameButWhere(cur, next) { return nil, false, nil } @@ -379,7 +391,7 @@ func (ix *Indexer) writeOverlay(ctx context.Context, overlay, finals map[string] // which only the judge writes: the module is unchanged. func sameButWhere(cur, next delivery.Module) bool { next = withV(next) - next.Commit, next.Src = cur.Commit, cur.Src + next.Commit, next.Src, next.FPSrc = cur.Commit, cur.Src, cur.FPSrc next.Verdict, next.Problems, next.Unknown, next.Warnings = cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings return reflect.DeepEqual(cur, next) } diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go index 603241dd..368f0602 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go @@ -113,12 +113,20 @@ func TestAgreedToolchainChangesAGoModulesReasonWithNoMerge(t *testing.T) { // 14 if m, _ := moduleOf(t, mods, "avahi"); m.None2 != delivery.ToolchainNoReport { t.Fatalf("before: none2 = %q", m.None2) } + ix.mu.Lock() + ix.source.rev = 11 // the source revision of now, whose release merged nothing of avahi's + ix.mu.Unlock() reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1") reportToolchain(t, ix, "mesh-build-go", "ace", "go", "go1.27.1") m, _ := moduleOf(t, mods, "avahi") if m.None2 != "" || m.FP2 == "" { t.Errorf("none2 = %q fp2 = %q, want a fingerprint: no builder input is waited for (ADR 0530 §1)", m.None2, m.FP2) } + // ADR 0601 §1: a fingerprint moved with no merge is stamped with the source revision of the moment, never the Src of + // the merge it was last merged at, so no earlier release is taken to carry it. + if m.FPSrc != 11 || m.Src == 11 { + t.Errorf("a fingerprint moved by a recompute: fp-src %d, src %d; want fp-src 11", m.FPSrc, m.Src) + } } func TestNodesReportingDifferentVersionsLeaveTheReasonNamingBoth(t *testing.T) { // 1444/AC6 From 0fa00d5e6c945f1e1e11fa4b5a7a6c3c52237cea Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 14:02:01 +0100 Subject: [PATCH 13/49] =?UTF-8?q?The=20watchman=20raises=20what=20a=20node?= =?UTF-8?q?=20keeps=20of=20a=20module=20as=20it=20runs,=20for=20a=20manife?= =?UTF-8?q?st=20that=20does=20not=20yet=20say=20it,=20from=20the=20module'?= =?UTF-8?q?s=20have,=20and=20clears=20it=20once=20the=20have=20says=20none?= =?UTF-8?q?=20(ADR=200601=20=C2=A74;=20review=20round=201,=20finding=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/watch/findings_test.go | 23 ++++++++++++++ .../internal/watch/signals.go | 30 +++++++++++++++++++ .../internal/watch/watch.go | 8 +++-- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/modules/mesh-delivery-watchman/internal/watch/findings_test.go b/modules/mesh-delivery-watchman/internal/watch/findings_test.go index 3e94a361..57f26142 100644 --- a/modules/mesh-delivery-watchman/internal/watch/findings_test.go +++ b/modules/mesh-delivery-watchman/internal/watch/findings_test.go @@ -72,6 +72,29 @@ func TestTheWatchmansFindings(t *testing.T) { } w.ModuleVerdict("nats", value(""), Put) }, map[string]string{}}, + {"ADR 0601 §4: what a node keeps of a module as it runs, for a manifest that does not say it yet, is a warning until the have says none", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { + have := func(kept ...string) []byte { + b, err := delivery.EncodeHave(delivery.Have{FP: "src2:" + strings.Repeat("a", 64), Phase: delivery.PhaseRunning, Health: delivery.HealthOK, Kept: kept}) + if err != nil { + t.Fatal(err) + } + return b + } + k := FindingKey(ScopeModule, "runner-x.ace", "kept") + w.Have("ace.runner-x", have("runner-x runs the tool runner's process here"), Put) + f := o.findings[k] + if f.Kind != KindKeptAsRuns || f.Severity != Warning || !strings.Contains(f.Summary, "the tool runner's process") { + t.Fatalf("findings = %v", o.findings) + } + c.t = c.t.Add(time.Minute) + w.Have("ace.runner-x", have("runner-x runs the tool runner's process here"), Put) + if !o.findings[k].Since.Equal(f.Since) { + t.Errorf("a have written again moved the finding's since: %v, was %v", o.findings[k].Since, f.Since) + } + w.Have("ace.prov-y", have("prov-y carries its own secrets sealed"), Put) + w.Have("ace.runner-x", have(), Put) // its manifest says tool-runner now + w.Have("ace.prov-y", nil, Delete) + }, map[string]string{}}, {"WM4: a stuck key not moved is its finding, and its move deletes it", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { w.Step("go", "n1.ask1", runningStep("ask1", t0), Put) expire(w, c, pulseKeyOf(t, checksKey, "ask1"), 120*time.Second) diff --git a/modules/mesh-delivery-watchman/internal/watch/signals.go b/modules/mesh-delivery-watchman/internal/watch/signals.go index e5bc2346..ee44118a 100644 --- a/modules/mesh-delivery-watchman/internal/watch/signals.go +++ b/modules/mesh-delivery-watchman/internal/watch/signals.go @@ -199,6 +199,7 @@ const ( KindUnhealthy = "module-unhealthy" KindNeedsOperator = "needs-operator" KindDeadLetters = "max-deliveries" + KindKeptAsRuns = "kept-as-it-runs" ) func stamp(t time.Time) string { return t.UTC().Format("2006-01-02 15:04 MST") } @@ -476,11 +477,13 @@ func (w *Watchman) haveFindings(key string, node, module string, h *delivery.Hav w.unsignal("unhealthy-confirm", module+"."+node) w.unsignal("unhealthy", module+"."+node) w.unfind(unhealthyKey) + w.keptFinding(key, node, module, nil) w.applyCheck(node) return } w.haveState[key] = *h now := w.now() + w.keptFinding(key, node, module, h.Kept) // S21: a have refused or applied in part says phase failed, with the node's words in its detail. if h.Phase == delivery.PhaseFailed { @@ -531,6 +534,33 @@ func (w *Watchman) haveFindings(key string, node, module string, h *delivery.Hav } } +// keptFinding raises, as a warning, what a node keeps of a module as it runs because its manifest does not yet say what shared +// code no longer infers (its have's Kept, novox/hq ADR 0601 §4: the tool runner, the core's credentials), naming each; and +// clears it once the have says none, or is gone. Its since is the first have that said it, so a have written again moves +// nothing. +func (w *Watchman) keptFinding(key, node, module string, kept []string) { + id := module + "." + node + fkey := FindingKey(ScopeModule, id, "kept") + if len(kept) == 0 { + delete(w.keptSince, key) + w.unfind(fkey) + return + } + if w.keptSince == nil { + w.keptSince = map[string]time.Time{} + } + since, ok := w.keptSince[key] + if !ok { + since = w.now() + w.keptSince[key] = since + } + words := strings.Join(kept, "; ") + w.find(Finding{Scope: ScopeModule, ID: id, Token: "kept", Kind: KindKeptAsRuns, Machine: node, Severity: Warning, + Source: "node-engine", Since: since, Summary: fmt.Sprintf("%s on %s is kept as it runs: %s", module, node, words), + Said: firstLine(words), Headline: fmt.Sprintf("%s kept as it runs on %s", module, node), + Needs: "a build of " + module + " whose manifest says it (tool-runner, core-credentials)"}) +} + // ControlNodeModule is the module whose have names a control node: the bus module, so the control node is the bus node, // the one stably reachable node every part depends on (novox/hq ADR 0590, issue 1126: the controller that named it ended). // The same name the bus module's events are heard under (SlowConsumerSubject). diff --git a/modules/mesh-delivery-watchman/internal/watch/watch.go b/modules/mesh-delivery-watchman/internal/watch/watch.go index fa6d5b7d..3c769493 100644 --- a/modules/mesh-delivery-watchman/internal/watch/watch.go +++ b/modules/mesh-delivery-watchman/internal/watch/watch.go @@ -60,9 +60,11 @@ type Watchman struct { presence map[string]presence pulses map[string]*pulse // live pulses, by pulse key stuck map[string]delivery.Stuck - wants map[string]string // the fingerprint of each want that stands, by want key - haves map[string]string // the fingerprint each node runs of a module, by have key - bound time.Duration + // keptSince is, by have key, when its have first said what the node keeps of the module as it runs (keptFinding). + keptSince map[string]time.Time + wants map[string]string // the fingerprint of each want that stands, by want key + haves map[string]string // the fingerprint each node runs of a module, by have key + bound time.Duration // findings is every finding written, by key, as written; seededFindings those read at the start and not yet observed // again. findings map[string][]byte From 6d249221724bd254ec1c3fb0a4ec70677a85d12e Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 14:02:01 +0100 Subject: [PATCH 14/49] =?UTF-8?q?The=20node-engine's=20witness=20ping=20is?= =?UTF-8?q?=20granted=20by=20the=20tool=20runner's=20name=20its=20manifest?= =?UTF-8?q?=20gives,=20any=20service's=20instance=20on=20its=20own=20node?= =?UTF-8?q?=20while=20none=20says=20so,=20never=20a=20name=20in=20mesh-gra?= =?UTF-8?q?nts=20(ADR=200601=20=C2=A74;=20review=20round=201,=20finding=20?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/buscompose/buscompose_test.go | 26 +++++++++++++++++++ .../mesh-grants/internal/buscompose/nats.go | 18 +++++++++---- .../buscompose/testdata/composed.conf | 2 +- .../buscompose/testdata/user-lists.json | 6 ++--- .../mesh-grants/internal/buscompose/users.go | 7 +++-- 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/modules/mesh-grants/internal/buscompose/buscompose_test.go b/modules/mesh-grants/internal/buscompose/buscompose_test.go index 2b00cff9..f67098bc 100644 --- a/modules/mesh-grants/internal/buscompose/buscompose_test.go +++ b/modules/mesh-grants/internal/buscompose/buscompose_test.go @@ -91,6 +91,32 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { t.Fatal(err) } golden(t, "user-lists.json", append(raw, '\n')) + // ADR 0601 §4: the node-engine's witness pings the tool runner by the name its manifest gives it, and with none saying so + // any service's instance on its own node, never a name written here. + t.Run("the witness pings the tool runner by its manifest", func(t *testing.T) { + for runner, want := range map[string]string{"runner-x": "$SRV.PING.runner-x.a", "": "$SRV.PING.*.a"} { + placed := []Declared{{Module: "web"}} + if runner != "" { + placed = append(placed, Declared{Module: runner, ToolRunner: true}) + } + users, err := Users(Records{Nodes: []string{"a"}, Assigned: map[string][]Declared{"a": placed}}) + if err != nil { + t.Fatal(err) + } + nodes := 0 + for _, u := range users { + if u.Kind == KindNode { + nodes++ + } + if u.Kind == KindNode && (u.Runner != runner || !slices.Contains(WitnessSubjects(u), want)) { + t.Errorf("runner %q: the node principal %+v witnesses %v, want %s", runner, u, WitnessSubjects(u), want) + } + } + if nodes == 0 { + t.Errorf("runner %q: no node principal composed", runner) + } + } + }) // An asker on a seat pulled by kind hears what that seat's workers gave up on, and no other stream's dead letters // (novox/hq issue 734: a check ask given up during a bus reload left its try running for ever). t.Run("an asker reads its seat's dead letters", func(t *testing.T) { diff --git a/modules/mesh-grants/internal/buscompose/nats.go b/modules/mesh-grants/internal/buscompose/nats.go index d8314b75..7f85009a 100644 --- a/modules/mesh-grants/internal/buscompose/nats.go +++ b/modules/mesh-grants/internal/buscompose/nats.go @@ -208,6 +208,9 @@ type Principal struct { // `.` (novox/hq ADR 0240, to-be 48 §3): asked of the instance on its own machine and // nowhere else. Checks []string + // Runner is, for a node, the module placed there whose manifest says it is the tool runner (`tool-runner`): the + // service the node-engine's witness pings (WitnessSubjects). Empty when none says so. + Runner string // MovesIntoTheVault is a machine principal whose node-engine may ask the vault's `move-in` in its own name (design // 57's V3, issue 676): set while the vault's holder serves it, so the engine can hand in the values sealed to its // machine, which nothing else can open. @@ -1603,7 +1606,7 @@ func discovering() []string { // WitnessSubjects are the subjects a machine's node-engine publishes to witness the core builds it // places (novox/hq ADR 0236): its own node tools' PING. The answers come to the machine's own inbox. func WitnessSubjects(p Principal) []string { - return append([]string{pingSubject(p.Node)}, CheckSubjects(p)...) + return append([]string{pingSubject(p.Runner, p.Node)}, CheckSubjects(p)...) } // CheckSubjects are the tools a machine's node-engine asks as declared health checks (novox/hq ADR 0240, @@ -1627,10 +1630,15 @@ func CheckSubjects(p Principal) []string { return out } -// pingSubject is a node-engine's witness's ping of its own node's tools (novox/hq ADR 0236). -func pingSubject(node string) string { return "$SRV.PING." + nodeToolsService + "." + node } - -const nodeToolsService = "node-tools" +// pingSubject is a node-engine's witness's ping of its own node's tool runner (novox/hq ADR 0236), named as the runner's +// manifest says it is (ADR 0601 §4), never by a name here. With no manifest on the node saying so yet (an older build's, +// the runner kept as it runs), any service's instance on this node: the ping of its own node alone, whatever it is called. +func pingSubject(runner, node string) string { + if runner == "" { + runner = "*" + } + return "$SRV.PING." + runner + "." + node +} // VaultOwnCall is the subject a machine principal's node-engine asks the vault's `own` on, in its own name. func VaultOwnCall(p Principal) string { diff --git a/modules/mesh-grants/internal/buscompose/testdata/composed.conf b/modules/mesh-grants/internal/buscompose/testdata/composed.conf index 70ee7b5e..f65a1ba8 100644 --- a/modules/mesh-grants/internal/buscompose/testdata/composed.conf +++ b/modules/mesh-grants/internal/buscompose/testdata/composed.conf @@ -33,7 +33,7 @@ accounts { subscribe: { allow: ["_INBOX.enrol.one.>"] } } } { user: "node.one", password: "$2a$11$nnnnnnnnnnnnnnnnnnnnnn", permissions: { - publish: { allow: ["$JS.ACK.NODES.one.>", "$JS.API.CONSUMER.CREATE.KV_composed-one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.>", "$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.one", "$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.one", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.one.>", "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node", "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.>", "$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*", "$JS.API.CONSUMER.DELETE.KV_composed-one.>", "$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.>", "$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.>", "$JS.API.CONSUMER.DELETE.KV_mesh-host_data.>", "$JS.API.CONSUMER.DELETE.KV_mesh-host_health.>", "$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.>", "$JS.API.CONSUMER.DELETE.KV_node-engine_have.>", "$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*", "$JS.API.CONSUMER.INFO.NODES.one", "$JS.API.DIRECT.GET.KV_composed-one.>", "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.one.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.>", "$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.one.>", "$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.one.>", "$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.one", "$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.one", "$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.one.>", "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node", "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.>", "$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*", "$JS.API.STREAM.INFO.KV_composed-one", "$JS.API.STREAM.INFO.KV_mesh-config_resolved", "$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates", "$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement", "$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants", "$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds", "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates", "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests", "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules", "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts", "$JS.API.STREAM.INFO.KV_mesh-host_acts", "$JS.API.STREAM.INFO.KV_mesh-host_data", "$JS.API.STREAM.INFO.KV_mesh-host_health", "$JS.API.STREAM.INFO.KV_mesh-host_packages", "$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions", "$JS.API.STREAM.INFO.KV_node-engine_have", "$JS.FC.KV_composed-one.>", "$JS.FC.KV_mesh-config_resolved.>", "$JS.FC.KV_mesh-delivery-assigner_candidates.>", "$JS.FC.KV_mesh-delivery-assigner_placement.>", "$JS.FC.KV_mesh-delivery-assigner_wants.>", "$JS.FC.KV_mesh-delivery-builder_builds.>", "$JS.FC.KV_mesh-delivery-indexer_candidates.>", "$JS.FC.KV_mesh-delivery-indexer_manifests.>", "$JS.FC.KV_mesh-delivery-indexer_modules.>", "$JS.FC.KV_mesh-delivery-indexer_verdicts.>", "$JS.FC.KV_mesh-host_acts.>", "$JS.FC.KV_mesh-host_data.>", "$JS.FC.KV_mesh-host_health.>", "$JS.FC.KV_mesh-host_packages.>", "$JS.FC.KV_mesh-provisioner_provisions.>", "$JS.FC.KV_node-engine_have.>", "$JS.FC.OBJ_declaration-files-one.>", "$KV.composed-one.>", "$KV.mesh-host_acts.one.>", "$KV.mesh-host_data.one.>", "$KV.mesh-host_health.one", "$KV.mesh-host_packages.one", "$KV.node-engine_have.one.>", "$SRV.INFO", "$SRV.INFO.>", "$SRV.PING", "$SRV.PING.>", "$SRV.PING.node-tools.one", "mesh.control.one.>", "mesh.seat.node-backup.call.backed-up.one.node~one", "mesh.seat.node-backup.call.now.one.node~one", "mesh.seat.node-backup.tool.backed-up.one", "mesh.seat.node-backup.tool.now.one", "mesh.seat.node-engine.event.*.one"] } + publish: { allow: ["$JS.ACK.NODES.one.>", "$JS.API.CONSUMER.CREATE.KV_composed-one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.>", "$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.one", "$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.one", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.one.>", "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node", "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.>", "$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*", "$JS.API.CONSUMER.DELETE.KV_composed-one.>", "$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.>", "$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.>", "$JS.API.CONSUMER.DELETE.KV_mesh-host_data.>", "$JS.API.CONSUMER.DELETE.KV_mesh-host_health.>", "$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.>", "$JS.API.CONSUMER.DELETE.KV_node-engine_have.>", "$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*", "$JS.API.CONSUMER.INFO.NODES.one", "$JS.API.DIRECT.GET.KV_composed-one.>", "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.one.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.>", "$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.one.>", "$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.one.>", "$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.one", "$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.one", "$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.one.>", "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node", "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.>", "$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*", "$JS.API.STREAM.INFO.KV_composed-one", "$JS.API.STREAM.INFO.KV_mesh-config_resolved", "$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates", "$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement", "$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants", "$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds", "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates", "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests", "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules", "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts", "$JS.API.STREAM.INFO.KV_mesh-host_acts", "$JS.API.STREAM.INFO.KV_mesh-host_data", "$JS.API.STREAM.INFO.KV_mesh-host_health", "$JS.API.STREAM.INFO.KV_mesh-host_packages", "$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions", "$JS.API.STREAM.INFO.KV_node-engine_have", "$JS.FC.KV_composed-one.>", "$JS.FC.KV_mesh-config_resolved.>", "$JS.FC.KV_mesh-delivery-assigner_candidates.>", "$JS.FC.KV_mesh-delivery-assigner_placement.>", "$JS.FC.KV_mesh-delivery-assigner_wants.>", "$JS.FC.KV_mesh-delivery-builder_builds.>", "$JS.FC.KV_mesh-delivery-indexer_candidates.>", "$JS.FC.KV_mesh-delivery-indexer_manifests.>", "$JS.FC.KV_mesh-delivery-indexer_modules.>", "$JS.FC.KV_mesh-delivery-indexer_verdicts.>", "$JS.FC.KV_mesh-host_acts.>", "$JS.FC.KV_mesh-host_data.>", "$JS.FC.KV_mesh-host_health.>", "$JS.FC.KV_mesh-host_packages.>", "$JS.FC.KV_mesh-provisioner_provisions.>", "$JS.FC.KV_node-engine_have.>", "$JS.FC.OBJ_declaration-files-one.>", "$KV.composed-one.>", "$KV.mesh-host_acts.one.>", "$KV.mesh-host_data.one.>", "$KV.mesh-host_health.one", "$KV.mesh-host_packages.one", "$KV.node-engine_have.one.>", "$SRV.INFO", "$SRV.INFO.>", "$SRV.PING", "$SRV.PING.*.one", "$SRV.PING.>", "mesh.control.one.>", "mesh.seat.node-backup.call.backed-up.one.node~one", "mesh.seat.node-backup.call.now.one.node~one", "mesh.seat.node-backup.tool.backed-up.one", "mesh.seat.node-backup.tool.now.one", "mesh.seat.node-engine.event.*.one"] } subscribe: { allow: ["$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates", "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement", "$SRV.INFO", "$SRV.INFO.node-engine", "$SRV.INFO.node-engine.>", "$SRV.PING", "$SRV.PING.node-engine", "$SRV.PING.node-engine.>", "$SRV.STATS", "$SRV.STATS.node-engine", "$SRV.STATS.node-engine.>", "_DELIVER.one", "_DELIVER.one.>", "_INBOX.node.one.>", "mesh.mod.restic.event.backed-up", "mesh.node.one.ask.access-act", "mesh.node.one.ask.hand-over", "mesh.node.one.ask.judge-directories", "mesh.node.one.ask.move-secret", "mesh.node.one.ask.report", "mesh.node.one.ask.setuid-search", "mesh.node.one.declare", "mesh.seat.node-engine.call.*.one.*", "mesh.seat.node-engine.tool.*.one"] } allow_responses: { max: 1, ttl: "1m" } } } diff --git a/modules/mesh-grants/internal/buscompose/testdata/user-lists.json b/modules/mesh-grants/internal/buscompose/testdata/user-lists.json index a66303c9..842a9753 100644 --- a/modules/mesh-grants/internal/buscompose/testdata/user-lists.json +++ b/modules/mesh-grants/internal/buscompose/testdata/user-lists.json @@ -8,7 +8,7 @@ }, { "Name": "the controller's someRecords", - "List": "# The mesh's users, composed by the grants part. Do not edit: the next\n# composition overwrites it. Permissions are derived from what each module\n# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\naccounts {\n MESH {\n jetstream: enabled\n imports = [\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"mesh.bus.connect\" }\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"mesh.bus.disconnect\" }\n { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"mesh.bus.connz\" }\n ]\n users = [\n { user: \"node.one\", password: \"$2a$11$node.onenode.onenode.ofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.one\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.one\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*\", \"$JS.API.CONSUMER.INFO.NODES.one\", \"$JS.API.DIRECT.GET.KV_composed-one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.one\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.one\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.one.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*\", \"$JS.API.STREAM.INFO.KV_composed-one\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-one.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-one.\u003e\", \"$KV.composed-one.\u003e\", \"$KV.mesh-host_acts.one.\u003e\", \"$KV.mesh-host_data.one.\u003e\", \"$KV.mesh-host_health.one\", \"$KV.mesh-host_packages.one\", \"$KV.node-engine_have.one.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"$SRV.PING.node-tools.one\", \"mesh.control.one.\u003e\", \"mesh.seat.node-backup.call.backed-up.one.node~one\", \"mesh.seat.node-backup.call.now.one.node~one\", \"mesh.seat.node-backup.tool.backed-up.one\", \"mesh.seat.node-backup.tool.now.one\", \"mesh.seat.node-engine.event.*.one\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.one\", \"_DELIVER.one.\u003e\", \"_INBOX.node.one.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.one.ask.access-act\", \"mesh.node.one.ask.hand-over\", \"mesh.node.one.ask.judge-directories\", \"mesh.node.one.ask.move-secret\", \"mesh.node.one.ask.report\", \"mesh.node.one.ask.setuid-search\", \"mesh.node.one.declare\", \"mesh.seat.node-engine.call.*.one.*\", \"mesh.seat.node-engine.tool.*.one\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.two\", password: \"$2a$11$node.twonode.twonode.tfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.two\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.two\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.two.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-two.*.$O.declaration-files-two.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-two.*\", \"$JS.API.CONSUMER.INFO.NODES.two\", \"$JS.API.DIRECT.GET.KV_composed-two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.two\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.two\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.two.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.two.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-two.$O.declaration-files-two.M.*\", \"$JS.API.STREAM.INFO.KV_composed-two\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-two.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-two.\u003e\", \"$KV.composed-two.\u003e\", \"$KV.mesh-host_acts.two.\u003e\", \"$KV.mesh-host_data.two.\u003e\", \"$KV.mesh-host_health.two\", \"$KV.mesh-host_packages.two\", \"$KV.node-engine_have.two.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"$SRV.PING.node-tools.two\", \"mesh.control.two.\u003e\", \"mesh.seat.node-backup.call.backed-up.two.node~two\", \"mesh.seat.node-backup.call.now.two.node~two\", \"mesh.seat.node-backup.tool.backed-up.two\", \"mesh.seat.node-backup.tool.now.two\", \"mesh.seat.node-engine.event.*.two\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.two\", \"_DELIVER.two.\u003e\", \"_INBOX.node.two.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.two.ask.access-act\", \"mesh.node.two.ask.hand-over\", \"mesh.node.two.ask.judge-directories\", \"mesh.node.two.ask.move-secret\", \"mesh.node.two.ask.report\", \"mesh.node.two.ask.setuid-search\", \"mesh.node.two.declare\", \"mesh.seat.node-engine.call.*.two.*\", \"mesh.seat.node-engine.tool.*.two\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.telegram\", password: \"$2a$11$one.telegramone.telegrfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_telegram.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.one.telegram\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_telegram\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_telegram\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.one.telegram\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.telegram\", \"$SRV.INFO.telegram.\u003e\", \"$SRV.PING\", \"$SRV.PING.telegram\", \"$SRV.PING.telegram.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.telegram\", \"$SRV.STATS.telegram.\u003e\", \"_INBOX.one.telegram.\u003e\", \"mesh.mod.telegram.call.\u003e\", \"mesh.mod.telegram.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"person.ada\", password: \"$2a$11$person.adaperson.adapefixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"mesh.mod.mesh-catalog.call.catalog_tools.*.person~ada\", \"mesh.mod.mesh-catalog.call.catalog_tools.person~ada\", \"mesh.mod.mesh-catalog.tool.catalog_tools\", \"mesh.mod.mesh-catalog.tool.catalog_tools.*\"] }\n subscribe: { allow: [\"_INBOX.person.ada.\u003e\"] }\n } }\n { user: \"two.shop\", password: \"$2a$11$two.shoptwo.shoptwo.shfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.two_shop.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.two.shop\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.two_shop\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_shop\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.two.shop\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"mesh.mod.shop.event.order.placed\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.shop\", \"$SRV.INFO.shop.\u003e\", \"$SRV.PING\", \"$SRV.PING.shop\", \"$SRV.PING.shop.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.shop\", \"$SRV.STATS.shop.\u003e\", \"_INBOX.two.shop.\u003e\", \"mesh.mod.shop.call.\u003e\", \"mesh.mod.shop.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n \"$SYS\" {\n exports = [\n { stream: \"$SYS.ACCOUNT.MESH.CONNECT\", accounts: [\"MESH\"] }\n { stream: \"$SYS.ACCOUNT.MESH.DISCONNECT\", accounts: [\"MESH\"] }\n { service: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\", accounts: [\"MESH\"] }\n ]\n }\n}\n", + "List": "# The mesh's users, composed by the grants part. Do not edit: the next\n# composition overwrites it. Permissions are derived from what each module\n# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\naccounts {\n MESH {\n jetstream: enabled\n imports = [\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"mesh.bus.connect\" }\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"mesh.bus.disconnect\" }\n { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"mesh.bus.connz\" }\n ]\n users = [\n { user: \"node.one\", password: \"$2a$11$node.onenode.onenode.ofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.one\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.one\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*\", \"$JS.API.CONSUMER.INFO.NODES.one\", \"$JS.API.DIRECT.GET.KV_composed-one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.one\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.one\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.one.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*\", \"$JS.API.STREAM.INFO.KV_composed-one\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-one.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-one.\u003e\", \"$KV.composed-one.\u003e\", \"$KV.mesh-host_acts.one.\u003e\", \"$KV.mesh-host_data.one.\u003e\", \"$KV.mesh-host_health.one\", \"$KV.mesh-host_packages.one\", \"$KV.node-engine_have.one.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.one\", \"$SRV.PING.\u003e\", \"mesh.control.one.\u003e\", \"mesh.seat.node-backup.call.backed-up.one.node~one\", \"mesh.seat.node-backup.call.now.one.node~one\", \"mesh.seat.node-backup.tool.backed-up.one\", \"mesh.seat.node-backup.tool.now.one\", \"mesh.seat.node-engine.event.*.one\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.one\", \"_DELIVER.one.\u003e\", \"_INBOX.node.one.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.one.ask.access-act\", \"mesh.node.one.ask.hand-over\", \"mesh.node.one.ask.judge-directories\", \"mesh.node.one.ask.move-secret\", \"mesh.node.one.ask.report\", \"mesh.node.one.ask.setuid-search\", \"mesh.node.one.declare\", \"mesh.seat.node-engine.call.*.one.*\", \"mesh.seat.node-engine.tool.*.one\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.two\", password: \"$2a$11$node.twonode.twonode.tfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.two\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.two\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.two.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-two.*.$O.declaration-files-two.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-two.*\", \"$JS.API.CONSUMER.INFO.NODES.two\", \"$JS.API.DIRECT.GET.KV_composed-two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.two\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.two\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.two.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.two.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-two.$O.declaration-files-two.M.*\", \"$JS.API.STREAM.INFO.KV_composed-two\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-two.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-two.\u003e\", \"$KV.composed-two.\u003e\", \"$KV.mesh-host_acts.two.\u003e\", \"$KV.mesh-host_data.two.\u003e\", \"$KV.mesh-host_health.two\", \"$KV.mesh-host_packages.two\", \"$KV.node-engine_have.two.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.two\", \"$SRV.PING.\u003e\", \"mesh.control.two.\u003e\", \"mesh.seat.node-backup.call.backed-up.two.node~two\", \"mesh.seat.node-backup.call.now.two.node~two\", \"mesh.seat.node-backup.tool.backed-up.two\", \"mesh.seat.node-backup.tool.now.two\", \"mesh.seat.node-engine.event.*.two\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.two\", \"_DELIVER.two.\u003e\", \"_INBOX.node.two.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.two.ask.access-act\", \"mesh.node.two.ask.hand-over\", \"mesh.node.two.ask.judge-directories\", \"mesh.node.two.ask.move-secret\", \"mesh.node.two.ask.report\", \"mesh.node.two.ask.setuid-search\", \"mesh.node.two.declare\", \"mesh.seat.node-engine.call.*.two.*\", \"mesh.seat.node-engine.tool.*.two\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.telegram\", password: \"$2a$11$one.telegramone.telegrfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_telegram.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.one.telegram\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_telegram\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_telegram\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.one.telegram\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.telegram\", \"$SRV.INFO.telegram.\u003e\", \"$SRV.PING\", \"$SRV.PING.telegram\", \"$SRV.PING.telegram.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.telegram\", \"$SRV.STATS.telegram.\u003e\", \"_INBOX.one.telegram.\u003e\", \"mesh.mod.telegram.call.\u003e\", \"mesh.mod.telegram.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"person.ada\", password: \"$2a$11$person.adaperson.adapefixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"mesh.mod.mesh-catalog.call.catalog_tools.*.person~ada\", \"mesh.mod.mesh-catalog.call.catalog_tools.person~ada\", \"mesh.mod.mesh-catalog.tool.catalog_tools\", \"mesh.mod.mesh-catalog.tool.catalog_tools.*\"] }\n subscribe: { allow: [\"_INBOX.person.ada.\u003e\"] }\n } }\n { user: \"two.shop\", password: \"$2a$11$two.shoptwo.shoptwo.shfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.two_shop.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.two.shop\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.two_shop\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_shop\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.two.shop\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"mesh.mod.shop.event.order.placed\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.shop\", \"$SRV.INFO.shop.\u003e\", \"$SRV.PING\", \"$SRV.PING.shop\", \"$SRV.PING.shop.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.shop\", \"$SRV.STATS.shop.\u003e\", \"_INBOX.two.shop.\u003e\", \"mesh.mod.shop.call.\u003e\", \"mesh.mod.shop.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n \"$SYS\" {\n exports = [\n { stream: \"$SYS.ACCOUNT.MESH.CONNECT\", accounts: [\"MESH\"] }\n { stream: \"$SYS.ACCOUNT.MESH.DISCONNECT\", accounts: [\"MESH\"] }\n { service: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\", accounts: [\"MESH\"] }\n ]\n }\n}\n", "Missing": null, "Err": "", "Memberships": { @@ -46,7 +46,7 @@ }, { "Name": "seats, consumers, people and a hashless user", - "List": "# The mesh's users, composed by the grants part. Do not edit: the next\n# composition overwrites it. Permissions are derived from what each module\n# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\naccounts {\n MESH {\n jetstream: enabled\n imports = [\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"mesh.bus.connect\" }\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"mesh.bus.disconnect\" }\n { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"mesh.bus.connz\" }\n ]\n users = [\n { user: \"node.one\", password: \"$2a$11$node.onenode.onenode.ofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_user-list.*.$KV.mesh-grants_user-list.sealed\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.one\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.one\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*\", \"$JS.API.CONSUMER.CREATE.OBJ_mesh-grants-user-list.*.$O.mesh-grants-user-list.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_user-list.*\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*\", \"$JS.API.CONSUMER.DELETE.OBJ_mesh-grants-user-list.*\", \"$JS.API.CONSUMER.INFO.NODES.one\", \"$JS.API.DIRECT.GET.KV_composed-one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-grants_user-list.$KV.mesh-grants_user-list.sealed\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.one\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.one\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.one.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*\", \"$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.$O.mesh-grants-user-list.M.*\", \"$JS.API.STREAM.INFO.KV_composed-one\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-grants_user-list\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-one.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-grants_user-list.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-one.\u003e\", \"$JS.FC.OBJ_mesh-grants-user-list.\u003e\", \"$KV.composed-one.\u003e\", \"$KV.mesh-host_acts.one.\u003e\", \"$KV.mesh-host_data.one.\u003e\", \"$KV.mesh-host_health.one\", \"$KV.mesh-host_packages.one\", \"$KV.node-engine_have.one.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"$SRV.PING.node-tools.one\", \"mesh.control.one.\u003e\", \"mesh.seat.node-backup.call.backed-up.one.node~one\", \"mesh.seat.node-backup.call.now.one.node~one\", \"mesh.seat.node-backup.tool.backed-up.one\", \"mesh.seat.node-backup.tool.now.one\", \"mesh.seat.node-engine.event.*.one\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.one\", \"_DELIVER.one.\u003e\", \"_INBOX.node.one.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.one.ask.access-act\", \"mesh.node.one.ask.hand-over\", \"mesh.node.one.ask.judge-directories\", \"mesh.node.one.ask.move-secret\", \"mesh.node.one.ask.report\", \"mesh.node.one.ask.setuid-search\", \"mesh.node.one.declare\", \"mesh.seat.node-engine.call.*.one.*\", \"mesh.seat.node-engine.tool.*.one\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.two\", password: \"$2a$11$node.twonode.twonode.tfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.two\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.two\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.two.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-two.*.$O.declaration-files-two.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-two.*\", \"$JS.API.CONSUMER.INFO.NODES.two\", \"$JS.API.DIRECT.GET.KV_composed-two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.two\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.two\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.two.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.two.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-two.$O.declaration-files-two.M.*\", \"$JS.API.STREAM.INFO.KV_composed-two\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-two.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-two.\u003e\", \"$KV.composed-two.\u003e\", \"$KV.mesh-host_acts.two.\u003e\", \"$KV.mesh-host_data.two.\u003e\", \"$KV.mesh-host_health.two\", \"$KV.mesh-host_packages.two\", \"$KV.node-engine_have.two.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"$SRV.PING.node-tools.two\", \"mesh.control.two.\u003e\", \"mesh.seat.node-backup.call.backed-up.two.node~two\", \"mesh.seat.node-backup.call.now.two.node~two\", \"mesh.seat.node-backup.tool.backed-up.two\", \"mesh.seat.node-backup.tool.now.two\", \"mesh.seat.node-engine.event.*.two\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.two\", \"_DELIVER.two.\u003e\", \"_INBOX.node.two.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.two.ask.access-act\", \"mesh.node.two.ask.hand-over\", \"mesh.node.two.ask.judge-directories\", \"mesh.node.two.ask.move-secret\", \"mesh.node.two.ask.report\", \"mesh.node.two.ask.setuid-search\", \"mesh.node.two.declare\", \"mesh.seat.node-engine.call.*.two.*\", \"mesh.seat.node-engine.tool.*.two\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.mesh-grants\", password: \"$2a$11$one.mesh-grantsone.mesfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_mesh-grants.\u003e\", \"$JS.API.CONSUMER.CREATE.EVENTS.two_audit\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.one.mesh-grants\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_mesh-grants\", \"$JS.API.CONSUMER.INFO.EVENTS.two_audit\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_mesh-grants\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.*.*\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.one.mesh-grants\", \"$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.\u003e\", \"$JS.API.STREAM.CREATE.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.API.STREAM.INFO.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.MSG.GET.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.PURGE.OBJ_mesh-grants-user-list\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"$O.mesh-grants-user-list.\u003e\", \"mesh.assignment.*.*\", \"mesh.mod.mesh-grants.event.membership-changed\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-grants\", \"$SRV.INFO.mesh-grants.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-grants\", \"$SRV.PING.mesh-grants.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-grants\", \"$SRV.STATS.mesh-grants.\u003e\", \"_INBOX.one.mesh-grants.\u003e\", \"mesh.mod.mesh-grants.call.\u003e\", \"mesh.mod.mesh-grants.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.mesh-watchman\", password: \"$2a$11$one.mesh-watchmanone.mfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_mesh-watchman.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.one.mesh-watchman\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_mesh-watchman\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_mesh-watchman\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.one.mesh-watchman\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-watchman\", \"$SRV.INFO.mesh-watchman.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-watchman\", \"$SRV.PING.mesh-watchman.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-watchman\", \"$SRV.STATS.mesh-watchman.\u003e\", \"_INBOX.one.mesh-watchman.\u003e\", \"mesh.mod.mesh-watchman.call.\u003e\", \"mesh.mod.mesh-watchman.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.nats\", password: \"$2a$11$one.natsone.natsone.nafixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.API.STREAM.INFO.*\", \"$JS.API.STREAM.NAMES\", \"$JS.API.STREAM.SNAPSHOT.*\", \"$JS.SNAPSHOT.ACK.\u003e\"] }\n subscribe: { allow: [\"_INBOX.one.nats.\u003e\"] }\n } }\n { user: \"one.telegram\", password: \"$2a$11$one.telegramone.telegrfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_telegram.\u003e\", \"$JS.ACK.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.one.telegram\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_telegram\", \"$JS.API.CONSUMER.INFO.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_telegram\", \"$JS.API.CONSUMER.MSG.NEXT.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.one.telegram\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"mesh.seat.telegram-sender.event.delivered\", \"mesh.seat.telegram-sender.event.failed\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.telegram\", \"$SRV.INFO.telegram.\u003e\", \"$SRV.PING\", \"$SRV.PING.telegram\", \"$SRV.PING.telegram.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.telegram\", \"$SRV.STATS.telegram.\u003e\", \"_INBOX.one.telegram.\u003e\", \"mesh.mod.telegram.call.\u003e\", \"mesh.mod.telegram.tool.\u003e\", \"mesh.seat.telegram-sender.accept.send\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"person.ada\", password: \"$2a$11$person.adaperson.adapefixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"mesh.mod.*.call.*.*.person~ada\", \"mesh.mod.*.call.*.person~ada\", \"mesh.mod.*.tool.\u003e\", \"mesh.seat.*.call.*.*.person~ada\", \"mesh.seat.*.call.*.person~ada\", \"mesh.seat.*.tool.\u003e\"], deny: [\"mesh.seat.node-launcher.tool.secret\", \"mesh.seat.node-launcher.tool.secret.*\", \"mesh.mod.*.tool.node-launcher.secret\", \"mesh.mod.*.tool.node-launcher.secret.*\", \"mesh.seat.node-launcher.call.secret.\u003e\", \"mesh.mod.*.call.node-launcher.secret.\u003e\"] }\n subscribe: { allow: [\"_INBOX.person.ada.\u003e\"] }\n } }\n { user: \"person.bob\", password: \"$2a$11$person.bobperson.bobpefixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"mesh.mod.shop.call.status.*.person~bob\", \"mesh.mod.shop.call.status.person~bob\", \"mesh.mod.shop.tool.status\", \"mesh.mod.shop.tool.status.*\"] }\n subscribe: { allow: [\"_INBOX.person.bob.\u003e\"] }\n } }\n { user: \"two.shop\", password: \"$2a$11$two.shoptwo.shoptwo.shfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.two_shop.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.two.shop\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.two_shop\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_shop\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.two.shop\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"mesh.mod.shop.event.order.placed\", \"mesh.seat.telegram-sender.accept.send\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.shop\", \"$SRV.INFO.shop.\u003e\", \"$SRV.PING\", \"$SRV.PING.shop\", \"$SRV.PING.shop.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.shop\", \"$SRV.STATS.shop.\u003e\", \"_INBOX.two.shop.\u003e\", \"mesh.mod.shop.call.\u003e\", \"mesh.mod.shop.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n \"$SYS\" {\n exports = [\n { stream: \"$SYS.ACCOUNT.MESH.CONNECT\", accounts: [\"MESH\"] }\n { stream: \"$SYS.ACCOUNT.MESH.DISCONNECT\", accounts: [\"MESH\"] }\n { service: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\", accounts: [\"MESH\"] }\n ]\n }\n}\n", + "List": "# The mesh's users, composed by the grants part. Do not edit: the next\n# composition overwrites it. Permissions are derived from what each module\n# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\naccounts {\n MESH {\n jetstream: enabled\n imports = [\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"mesh.bus.connect\" }\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"mesh.bus.disconnect\" }\n { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"mesh.bus.connz\" }\n ]\n users = [\n { user: \"node.one\", password: \"$2a$11$node.onenode.onenode.ofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_user-list.*.$KV.mesh-grants_user-list.sealed\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.one\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.one\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*\", \"$JS.API.CONSUMER.CREATE.OBJ_mesh-grants-user-list.*.$O.mesh-grants-user-list.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_user-list.*\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*\", \"$JS.API.CONSUMER.DELETE.OBJ_mesh-grants-user-list.*\", \"$JS.API.CONSUMER.INFO.NODES.one\", \"$JS.API.DIRECT.GET.KV_composed-one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-grants_user-list.$KV.mesh-grants_user-list.sealed\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.one\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.one\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.one.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*\", \"$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.$O.mesh-grants-user-list.M.*\", \"$JS.API.STREAM.INFO.KV_composed-one\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-grants_user-list\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-one.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-grants_user-list.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-one.\u003e\", \"$JS.FC.OBJ_mesh-grants-user-list.\u003e\", \"$KV.composed-one.\u003e\", \"$KV.mesh-host_acts.one.\u003e\", \"$KV.mesh-host_data.one.\u003e\", \"$KV.mesh-host_health.one\", \"$KV.mesh-host_packages.one\", \"$KV.node-engine_have.one.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.one\", \"$SRV.PING.\u003e\", \"mesh.control.one.\u003e\", \"mesh.seat.node-backup.call.backed-up.one.node~one\", \"mesh.seat.node-backup.call.now.one.node~one\", \"mesh.seat.node-backup.tool.backed-up.one\", \"mesh.seat.node-backup.tool.now.one\", \"mesh.seat.node-engine.event.*.one\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.one\", \"_DELIVER.one.\u003e\", \"_INBOX.node.one.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.one.ask.access-act\", \"mesh.node.one.ask.hand-over\", \"mesh.node.one.ask.judge-directories\", \"mesh.node.one.ask.move-secret\", \"mesh.node.one.ask.report\", \"mesh.node.one.ask.setuid-search\", \"mesh.node.one.declare\", \"mesh.seat.node-engine.call.*.one.*\", \"mesh.seat.node-engine.tool.*.one\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.two\", password: \"$2a$11$node.twonode.twonode.tfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.two\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.two\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.two.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-two.*.$O.declaration-files-two.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-two.*\", \"$JS.API.CONSUMER.INFO.NODES.two\", \"$JS.API.DIRECT.GET.KV_composed-two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.two\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.two\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.two.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.two.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-two.$O.declaration-files-two.M.*\", \"$JS.API.STREAM.INFO.KV_composed-two\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-two.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-two.\u003e\", \"$KV.composed-two.\u003e\", \"$KV.mesh-host_acts.two.\u003e\", \"$KV.mesh-host_data.two.\u003e\", \"$KV.mesh-host_health.two\", \"$KV.mesh-host_packages.two\", \"$KV.node-engine_have.two.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.two\", \"$SRV.PING.\u003e\", \"mesh.control.two.\u003e\", \"mesh.seat.node-backup.call.backed-up.two.node~two\", \"mesh.seat.node-backup.call.now.two.node~two\", \"mesh.seat.node-backup.tool.backed-up.two\", \"mesh.seat.node-backup.tool.now.two\", \"mesh.seat.node-engine.event.*.two\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.two\", \"_DELIVER.two.\u003e\", \"_INBOX.node.two.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.two.ask.access-act\", \"mesh.node.two.ask.hand-over\", \"mesh.node.two.ask.judge-directories\", \"mesh.node.two.ask.move-secret\", \"mesh.node.two.ask.report\", \"mesh.node.two.ask.setuid-search\", \"mesh.node.two.declare\", \"mesh.seat.node-engine.call.*.two.*\", \"mesh.seat.node-engine.tool.*.two\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.mesh-grants\", password: \"$2a$11$one.mesh-grantsone.mesfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_mesh-grants.\u003e\", \"$JS.API.CONSUMER.CREATE.EVENTS.two_audit\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.one.mesh-grants\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_mesh-grants\", \"$JS.API.CONSUMER.INFO.EVENTS.two_audit\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_mesh-grants\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.*.*\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.one.mesh-grants\", \"$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.\u003e\", \"$JS.API.STREAM.CREATE.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.API.STREAM.INFO.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.MSG.GET.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.PURGE.OBJ_mesh-grants-user-list\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"$O.mesh-grants-user-list.\u003e\", \"mesh.assignment.*.*\", \"mesh.mod.mesh-grants.event.membership-changed\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-grants\", \"$SRV.INFO.mesh-grants.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-grants\", \"$SRV.PING.mesh-grants.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-grants\", \"$SRV.STATS.mesh-grants.\u003e\", \"_INBOX.one.mesh-grants.\u003e\", \"mesh.mod.mesh-grants.call.\u003e\", \"mesh.mod.mesh-grants.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.mesh-watchman\", password: \"$2a$11$one.mesh-watchmanone.mfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_mesh-watchman.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.one.mesh-watchman\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_mesh-watchman\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_mesh-watchman\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.one.mesh-watchman\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-watchman\", \"$SRV.INFO.mesh-watchman.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-watchman\", \"$SRV.PING.mesh-watchman.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-watchman\", \"$SRV.STATS.mesh-watchman.\u003e\", \"_INBOX.one.mesh-watchman.\u003e\", \"mesh.mod.mesh-watchman.call.\u003e\", \"mesh.mod.mesh-watchman.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.nats\", password: \"$2a$11$one.natsone.natsone.nafixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.API.STREAM.INFO.*\", \"$JS.API.STREAM.NAMES\", \"$JS.API.STREAM.SNAPSHOT.*\", \"$JS.SNAPSHOT.ACK.\u003e\"] }\n subscribe: { allow: [\"_INBOX.one.nats.\u003e\"] }\n } }\n { user: \"one.telegram\", password: \"$2a$11$one.telegramone.telegrfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_telegram.\u003e\", \"$JS.ACK.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.one.telegram\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_telegram\", \"$JS.API.CONSUMER.INFO.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_telegram\", \"$JS.API.CONSUMER.MSG.NEXT.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.one.telegram\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"mesh.seat.telegram-sender.event.delivered\", \"mesh.seat.telegram-sender.event.failed\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.telegram\", \"$SRV.INFO.telegram.\u003e\", \"$SRV.PING\", \"$SRV.PING.telegram\", \"$SRV.PING.telegram.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.telegram\", \"$SRV.STATS.telegram.\u003e\", \"_INBOX.one.telegram.\u003e\", \"mesh.mod.telegram.call.\u003e\", \"mesh.mod.telegram.tool.\u003e\", \"mesh.seat.telegram-sender.accept.send\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"person.ada\", password: \"$2a$11$person.adaperson.adapefixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"mesh.mod.*.call.*.*.person~ada\", \"mesh.mod.*.call.*.person~ada\", \"mesh.mod.*.tool.\u003e\", \"mesh.seat.*.call.*.*.person~ada\", \"mesh.seat.*.call.*.person~ada\", \"mesh.seat.*.tool.\u003e\"], deny: [\"mesh.seat.node-launcher.tool.secret\", \"mesh.seat.node-launcher.tool.secret.*\", \"mesh.mod.*.tool.node-launcher.secret\", \"mesh.mod.*.tool.node-launcher.secret.*\", \"mesh.seat.node-launcher.call.secret.\u003e\", \"mesh.mod.*.call.node-launcher.secret.\u003e\"] }\n subscribe: { allow: [\"_INBOX.person.ada.\u003e\"] }\n } }\n { user: \"person.bob\", password: \"$2a$11$person.bobperson.bobpefixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"mesh.mod.shop.call.status.*.person~bob\", \"mesh.mod.shop.call.status.person~bob\", \"mesh.mod.shop.tool.status\", \"mesh.mod.shop.tool.status.*\"] }\n subscribe: { allow: [\"_INBOX.person.bob.\u003e\"] }\n } }\n { user: \"two.shop\", password: \"$2a$11$two.shoptwo.shoptwo.shfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.two_shop.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.two.shop\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.two_shop\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_shop\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.two.shop\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"mesh.mod.shop.event.order.placed\", \"mesh.seat.telegram-sender.accept.send\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.shop\", \"$SRV.INFO.shop.\u003e\", \"$SRV.PING\", \"$SRV.PING.shop\", \"$SRV.PING.shop.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.shop\", \"$SRV.STATS.shop.\u003e\", \"_INBOX.two.shop.\u003e\", \"mesh.mod.shop.call.\u003e\", \"mesh.mod.shop.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n \"$SYS\" {\n exports = [\n { stream: \"$SYS.ACCOUNT.MESH.CONNECT\", accounts: [\"MESH\"] }\n { stream: \"$SYS.ACCOUNT.MESH.DISCONNECT\", accounts: [\"MESH\"] }\n { service: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\", accounts: [\"MESH\"] }\n ]\n }\n}\n", "Missing": [ "two.audit" ], @@ -152,7 +152,7 @@ }, { "Name": "state, logs, streams, queues and processes", - "List": "# The mesh's users, composed by the grants part. Do not edit: the next\n# composition overwrites it. Permissions are derived from what each module\n# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\naccounts {\n MESH {\n jetstream: enabled\n imports = [\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"mesh.bus.connect\" }\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"mesh.bus.disconnect\" }\n { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"mesh.bus.connz\" }\n ]\n users = [\n { user: \"ace.plex\", password: \"$2a$11$ace.plexace.plexace.plfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.ace_plex.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.ace.plex\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.ace_plex\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.ace_plex\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.ace.plex\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.plex\", \"$SRV.INFO.plex.\u003e\", \"$SRV.PING\", \"$SRV.PING.plex\", \"$SRV.PING.plex.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.plex\", \"$SRV.STATS.plex.\u003e\", \"_INBOX.ace.plex.\u003e\", \"mesh.mod.plex.call.\u003e\", \"mesh.mod.plex.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.ace\", password: \"$2a$11$node.acenode.acenode.afixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.ace\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.ace\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-ace.*.$O.declaration-files-ace.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-ace.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-ace.*\", \"$JS.API.CONSUMER.INFO.NODES.ace\", \"$JS.API.DIRECT.GET.KV_composed-ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.ace\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.ace\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.ace.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-ace.$O.declaration-files-ace.M.*\", \"$JS.API.STREAM.INFO.KV_composed-ace\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-ace.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-ace.\u003e\", \"$KV.composed-ace.\u003e\", \"$KV.mesh-host_acts.ace.\u003e\", \"$KV.mesh-host_data.ace.\u003e\", \"$KV.mesh-host_health.ace\", \"$KV.mesh-host_packages.ace\", \"$KV.node-engine_have.ace.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"$SRV.PING.node-tools.ace\", \"mesh.control.ace.\u003e\", \"mesh.mod.plex.call.plex_status.ace.node~ace\", \"mesh.mod.plex.tool.plex_status.ace\", \"mesh.seat.node-backup.call.backed-up.ace.node~ace\", \"mesh.seat.node-backup.call.now.ace.node~ace\", \"mesh.seat.node-backup.tool.backed-up.ace\", \"mesh.seat.node-backup.tool.now.ace\", \"mesh.seat.node-engine.event.*.ace\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.ace\", \"_DELIVER.ace.\u003e\", \"_INBOX.node.ace.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.ace.ask.access-act\", \"mesh.node.ace.ask.hand-over\", \"mesh.node.ace.ask.judge-directories\", \"mesh.node.ace.ask.move-secret\", \"mesh.node.ace.ask.report\", \"mesh.node.ace.ask.setuid-search\", \"mesh.node.ace.declare\", \"mesh.seat.node-engine.call.*.ace.*\", \"mesh.seat.node-engine.tool.*.ace\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.novox\", password: \"$2a$11$node.novoxnode.novoxnofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-novox.*.$O.declaration-files-novox.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-novox.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-novox.*\", \"$JS.API.CONSUMER.INFO.NODES.novox\", \"$JS.API.DIRECT.GET.KV_composed-novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.novox\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.novox\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.novox.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-novox.$O.declaration-files-novox.M.*\", \"$JS.API.STREAM.INFO.KV_composed-novox\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-novox.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-novox.\u003e\", \"$KV.composed-novox.\u003e\", \"$KV.mesh-host_acts.novox.\u003e\", \"$KV.mesh-host_data.novox.\u003e\", \"$KV.mesh-host_health.novox\", \"$KV.mesh-host_packages.novox\", \"$KV.node-engine_have.novox.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"$SRV.PING.node-tools.novox\", \"mesh.control.novox.\u003e\", \"mesh.seat.node-backup.call.backed-up.novox.node~novox\", \"mesh.seat.node-backup.call.now.novox.node~novox\", \"mesh.seat.node-backup.tool.backed-up.novox\", \"mesh.seat.node-backup.tool.now.novox\", \"mesh.seat.node-engine.event.*.novox\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.novox\", \"_DELIVER.novox.\u003e\", \"_INBOX.node.novox.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.novox.ask.access-act\", \"mesh.node.novox.ask.hand-over\", \"mesh.node.novox.ask.judge-directories\", \"mesh.node.novox.ask.move-secret\", \"mesh.node.novox.ask.report\", \"mesh.node.novox.ask.setuid-search\", \"mesh.node.novox.declare\", \"mesh.seat.node-engine.call.*.novox.*\", \"mesh.seat.node-engine.tool.*.novox\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-delivery-builder\", password: \"$2a$11$novox.mesh-delivery-bufixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-delivery-builder.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_presence.*.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.novox.mesh-delivery-builder\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-delivery-builder\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-delivery-builder\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_presence.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.novox.mesh-delivery-builder\", \"$JS.API.DIRECT.GET.LOG_mesh-delivery-builder_builds\", \"$JS.API.DIRECT.GET.LOG_mesh-delivery-builder_builds.\u003e\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.API.STREAM.INFO.LOG_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_NODE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_NODE\", \"$JS.FC.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"$KV.mesh-delivery-builder_presence.novox\", \"$KV.mesh-delivery-builder_steps.\u003e\", \"mesh.log.mesh-delivery-builder.builds.\u003e\", \"mesh.mod.gitea.call.gitea_commit.*.novox~mesh-delivery-builder\", \"mesh.mod.gitea.call.gitea_commit.novox~mesh-delivery-builder\", \"mesh.mod.gitea.tool.gitea_commit\", \"mesh.mod.gitea.tool.gitea_commit.*\", \"mesh.mod.mesh-delivery-builder.event.built\", \"mesh.mod.mesh-delivery-builder.event.states\", \"mesh.stream.mesh-delivery-builder.event.ask\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_presence\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_steps\", \"$SRV.INFO\", \"$SRV.INFO.mesh-delivery-builder\", \"$SRV.INFO.mesh-delivery-builder.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-delivery-builder\", \"$SRV.PING.mesh-delivery-builder.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-delivery-builder\", \"$SRV.STATS.mesh-delivery-builder.\u003e\", \"_INBOX.novox.mesh-delivery-builder.\u003e\", \"mesh.mod.mesh-delivery-builder.call.\u003e\", \"mesh.mod.mesh-delivery-builder.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-delivery-builder.worker\", password: \"$2a$11$novox.mesh-delivery-bufixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.asks.novox_mesh-delivery-builder_worker.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.INFO.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.CONSUMER.MSG.NEXT.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.FC.KV_mesh-delivery-builder_steps.\u003e\", \"$KV.mesh-delivery-builder_steps.\u003e\", \"mesh.mod.gitea.call.gitea_commit.*.novox~mesh-delivery-builder~worker\", \"mesh.mod.gitea.call.gitea_commit.novox~mesh-delivery-builder~worker\", \"mesh.mod.gitea.tool.gitea_commit\", \"mesh.mod.gitea.tool.gitea_commit.*\", \"mesh.mod.mesh-delivery-builder.event.built\", \"mesh.mod.mesh-delivery-builder.event.states\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_steps\", \"_INBOX.novox.mesh-delivery-builder.worker.\u003e\"] }\n } }\n { user: \"novox.mesh-delivery-view\", password: \"$2a$11$novox.mesh-delivery-vifixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-delivery-view.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_presence.*.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.novox.mesh-delivery-view\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-delivery-view\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_CHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_DELIVERY_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_RECHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_IMAGE.SEAT_NODE_BUILD_IMAGE_CHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_IMAGE.SEAT_NODE_BUILD_IMAGE_DELIVERY_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_IMAGE.SEAT_NODE_BUILD_IMAGE_RECHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_NODE.SEAT_NODE_BUILD_NODE_CHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_NODE.SEAT_NODE_BUILD_NODE_DELIVERY_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_NODE.SEAT_NODE_BUILD_NODE_RECHECK_worker\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-delivery-view\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_presence.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.novox.mesh-delivery-view\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_NODE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_NODE\", \"$JS.FC.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_presence\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_steps\", \"$SRV.INFO\", \"$SRV.INFO.mesh-delivery-view\", \"$SRV.INFO.mesh-delivery-view.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-delivery-view\", \"$SRV.PING.mesh-delivery-view.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-delivery-view\", \"$SRV.STATS.mesh-delivery-view.\u003e\", \"_INBOX.novox.mesh-delivery-view.\u003e\", \"mesh.mod.mesh-delivery-view.call.\u003e\", \"mesh.mod.mesh-delivery-view.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-grants\", password: \"$2a$11$novox.mesh-grantsnovoxfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-grants.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_assertions.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.novox.mesh-grants\", \"$JS.API.CONSUMER.CREATE.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_assertions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-grants\", \"$JS.API.CONSUMER.INFO.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-grants\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.*.*\", \"$JS.API.DIRECT.GET.KV_mesh-grants_assertions.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.novox.mesh-grants\", \"$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.\u003e\", \"$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.CREATE.KV_mesh-grants_assertions\", \"$JS.API.STREAM.CREATE.LOG_mesh-delivery-builder_builds\", \"$JS.API.STREAM.CREATE.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.CREATE.asks\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.INFO.KV_mesh-grants_assertions\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.API.STREAM.INFO.LOG_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.INFO.asks\", \"$JS.API.STREAM.MSG.GET.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.PURGE.OBJ_mesh-grants-user-list\", \"$JS.FC.KV_mesh-grants_assertions.\u003e\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"$KV.mesh-grants_assertions.\u003e\", \"$O.mesh-grants-user-list.\u003e\", \"mesh.assignment.*.*\", \"mesh.mod.mesh-grants.event.membership-changed\", \"mesh.mod.mesh-grants.event.states\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-grants_assertions\", \"$SRV.INFO\", \"$SRV.INFO.mesh-grants\", \"$SRV.INFO.mesh-grants.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-grants\", \"$SRV.PING.mesh-grants.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-grants\", \"$SRV.STATS.mesh-grants.\u003e\", \"_INBOX.novox.mesh-grants.\u003e\", \"mesh.mod.mesh-grants.call.\u003e\", \"mesh.mod.mesh-grants.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n \"$SYS\" {\n exports = [\n { stream: \"$SYS.ACCOUNT.MESH.CONNECT\", accounts: [\"MESH\"] }\n { stream: \"$SYS.ACCOUNT.MESH.DISCONNECT\", accounts: [\"MESH\"] }\n { service: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\", accounts: [\"MESH\"] }\n ]\n }\n}\n", + "List": "# The mesh's users, composed by the grants part. Do not edit: the next\n# composition overwrites it. Permissions are derived from what each module\n# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\naccounts {\n MESH {\n jetstream: enabled\n imports = [\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"mesh.bus.connect\" }\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"mesh.bus.disconnect\" }\n { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"mesh.bus.connz\" }\n ]\n users = [\n { user: \"ace.plex\", password: \"$2a$11$ace.plexace.plexace.plfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.ace_plex.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.ace.plex\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.ace_plex\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.ace_plex\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.ace.plex\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.plex\", \"$SRV.INFO.plex.\u003e\", \"$SRV.PING\", \"$SRV.PING.plex\", \"$SRV.PING.plex.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.plex\", \"$SRV.STATS.plex.\u003e\", \"_INBOX.ace.plex.\u003e\", \"mesh.mod.plex.call.\u003e\", \"mesh.mod.plex.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.ace\", password: \"$2a$11$node.acenode.acenode.afixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.ace\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.ace\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-ace.*.$O.declaration-files-ace.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-ace.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-ace.*\", \"$JS.API.CONSUMER.INFO.NODES.ace\", \"$JS.API.DIRECT.GET.KV_composed-ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.ace\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.ace\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.ace.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-ace.$O.declaration-files-ace.M.*\", \"$JS.API.STREAM.INFO.KV_composed-ace\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-ace.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-ace.\u003e\", \"$KV.composed-ace.\u003e\", \"$KV.mesh-host_acts.ace.\u003e\", \"$KV.mesh-host_data.ace.\u003e\", \"$KV.mesh-host_health.ace\", \"$KV.mesh-host_packages.ace\", \"$KV.node-engine_have.ace.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.ace\", \"$SRV.PING.\u003e\", \"mesh.control.ace.\u003e\", \"mesh.mod.plex.call.plex_status.ace.node~ace\", \"mesh.mod.plex.tool.plex_status.ace\", \"mesh.seat.node-backup.call.backed-up.ace.node~ace\", \"mesh.seat.node-backup.call.now.ace.node~ace\", \"mesh.seat.node-backup.tool.backed-up.ace\", \"mesh.seat.node-backup.tool.now.ace\", \"mesh.seat.node-engine.event.*.ace\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.ace\", \"_DELIVER.ace.\u003e\", \"_INBOX.node.ace.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.ace.ask.access-act\", \"mesh.node.ace.ask.hand-over\", \"mesh.node.ace.ask.judge-directories\", \"mesh.node.ace.ask.move-secret\", \"mesh.node.ace.ask.report\", \"mesh.node.ace.ask.setuid-search\", \"mesh.node.ace.declare\", \"mesh.seat.node-engine.call.*.ace.*\", \"mesh.seat.node-engine.tool.*.ace\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.novox\", password: \"$2a$11$node.novoxnode.novoxnofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-novox.*.$O.declaration-files-novox.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-novox.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-novox.*\", \"$JS.API.CONSUMER.INFO.NODES.novox\", \"$JS.API.DIRECT.GET.KV_composed-novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.novox\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.novox\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.novox.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-novox.$O.declaration-files-novox.M.*\", \"$JS.API.STREAM.INFO.KV_composed-novox\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-novox.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-novox.\u003e\", \"$KV.composed-novox.\u003e\", \"$KV.mesh-host_acts.novox.\u003e\", \"$KV.mesh-host_data.novox.\u003e\", \"$KV.mesh-host_health.novox\", \"$KV.mesh-host_packages.novox\", \"$KV.node-engine_have.novox.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.novox\", \"$SRV.PING.\u003e\", \"mesh.control.novox.\u003e\", \"mesh.seat.node-backup.call.backed-up.novox.node~novox\", \"mesh.seat.node-backup.call.now.novox.node~novox\", \"mesh.seat.node-backup.tool.backed-up.novox\", \"mesh.seat.node-backup.tool.now.novox\", \"mesh.seat.node-engine.event.*.novox\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.novox\", \"_DELIVER.novox.\u003e\", \"_INBOX.node.novox.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.novox.ask.access-act\", \"mesh.node.novox.ask.hand-over\", \"mesh.node.novox.ask.judge-directories\", \"mesh.node.novox.ask.move-secret\", \"mesh.node.novox.ask.report\", \"mesh.node.novox.ask.setuid-search\", \"mesh.node.novox.declare\", \"mesh.seat.node-engine.call.*.novox.*\", \"mesh.seat.node-engine.tool.*.novox\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-delivery-builder\", password: \"$2a$11$novox.mesh-delivery-bufixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-delivery-builder.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_presence.*.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.novox.mesh-delivery-builder\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-delivery-builder\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-delivery-builder\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_presence.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.novox.mesh-delivery-builder\", \"$JS.API.DIRECT.GET.LOG_mesh-delivery-builder_builds\", \"$JS.API.DIRECT.GET.LOG_mesh-delivery-builder_builds.\u003e\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.API.STREAM.INFO.LOG_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_NODE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_NODE\", \"$JS.FC.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"$KV.mesh-delivery-builder_presence.novox\", \"$KV.mesh-delivery-builder_steps.\u003e\", \"mesh.log.mesh-delivery-builder.builds.\u003e\", \"mesh.mod.gitea.call.gitea_commit.*.novox~mesh-delivery-builder\", \"mesh.mod.gitea.call.gitea_commit.novox~mesh-delivery-builder\", \"mesh.mod.gitea.tool.gitea_commit\", \"mesh.mod.gitea.tool.gitea_commit.*\", \"mesh.mod.mesh-delivery-builder.event.built\", \"mesh.mod.mesh-delivery-builder.event.states\", \"mesh.stream.mesh-delivery-builder.event.ask\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_presence\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_steps\", \"$SRV.INFO\", \"$SRV.INFO.mesh-delivery-builder\", \"$SRV.INFO.mesh-delivery-builder.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-delivery-builder\", \"$SRV.PING.mesh-delivery-builder.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-delivery-builder\", \"$SRV.STATS.mesh-delivery-builder.\u003e\", \"_INBOX.novox.mesh-delivery-builder.\u003e\", \"mesh.mod.mesh-delivery-builder.call.\u003e\", \"mesh.mod.mesh-delivery-builder.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-delivery-builder.worker\", password: \"$2a$11$novox.mesh-delivery-bufixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.asks.novox_mesh-delivery-builder_worker.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.INFO.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.CONSUMER.MSG.NEXT.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.FC.KV_mesh-delivery-builder_steps.\u003e\", \"$KV.mesh-delivery-builder_steps.\u003e\", \"mesh.mod.gitea.call.gitea_commit.*.novox~mesh-delivery-builder~worker\", \"mesh.mod.gitea.call.gitea_commit.novox~mesh-delivery-builder~worker\", \"mesh.mod.gitea.tool.gitea_commit\", \"mesh.mod.gitea.tool.gitea_commit.*\", \"mesh.mod.mesh-delivery-builder.event.built\", \"mesh.mod.mesh-delivery-builder.event.states\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_steps\", \"_INBOX.novox.mesh-delivery-builder.worker.\u003e\"] }\n } }\n { user: \"novox.mesh-delivery-view\", password: \"$2a$11$novox.mesh-delivery-vifixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-delivery-view.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_presence.*.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.novox.mesh-delivery-view\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-delivery-view\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_CHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_DELIVERY_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_RECHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_IMAGE.SEAT_NODE_BUILD_IMAGE_CHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_IMAGE.SEAT_NODE_BUILD_IMAGE_DELIVERY_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_IMAGE.SEAT_NODE_BUILD_IMAGE_RECHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_NODE.SEAT_NODE_BUILD_NODE_CHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_NODE.SEAT_NODE_BUILD_NODE_DELIVERY_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_NODE.SEAT_NODE_BUILD_NODE_RECHECK_worker\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-delivery-view\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_presence.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.novox.mesh-delivery-view\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_NODE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_NODE\", \"$JS.FC.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_presence\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_steps\", \"$SRV.INFO\", \"$SRV.INFO.mesh-delivery-view\", \"$SRV.INFO.mesh-delivery-view.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-delivery-view\", \"$SRV.PING.mesh-delivery-view.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-delivery-view\", \"$SRV.STATS.mesh-delivery-view.\u003e\", \"_INBOX.novox.mesh-delivery-view.\u003e\", \"mesh.mod.mesh-delivery-view.call.\u003e\", \"mesh.mod.mesh-delivery-view.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-grants\", password: \"$2a$11$novox.mesh-grantsnovoxfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-grants.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_assertions.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.novox.mesh-grants\", \"$JS.API.CONSUMER.CREATE.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_assertions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-grants\", \"$JS.API.CONSUMER.INFO.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-grants\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.*.*\", \"$JS.API.DIRECT.GET.KV_mesh-grants_assertions.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.novox.mesh-grants\", \"$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.\u003e\", \"$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.CREATE.KV_mesh-grants_assertions\", \"$JS.API.STREAM.CREATE.LOG_mesh-delivery-builder_builds\", \"$JS.API.STREAM.CREATE.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.CREATE.asks\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.INFO.KV_mesh-grants_assertions\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.API.STREAM.INFO.LOG_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.INFO.asks\", \"$JS.API.STREAM.MSG.GET.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.PURGE.OBJ_mesh-grants-user-list\", \"$JS.FC.KV_mesh-grants_assertions.\u003e\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"$KV.mesh-grants_assertions.\u003e\", \"$O.mesh-grants-user-list.\u003e\", \"mesh.assignment.*.*\", \"mesh.mod.mesh-grants.event.membership-changed\", \"mesh.mod.mesh-grants.event.states\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-grants_assertions\", \"$SRV.INFO\", \"$SRV.INFO.mesh-grants\", \"$SRV.INFO.mesh-grants.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-grants\", \"$SRV.PING.mesh-grants.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-grants\", \"$SRV.STATS.mesh-grants.\u003e\", \"_INBOX.novox.mesh-grants.\u003e\", \"mesh.mod.mesh-grants.call.\u003e\", \"mesh.mod.mesh-grants.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n \"$SYS\" {\n exports = [\n { stream: \"$SYS.ACCOUNT.MESH.CONNECT\", accounts: [\"MESH\"] }\n { stream: \"$SYS.ACCOUNT.MESH.DISCONNECT\", accounts: [\"MESH\"] }\n { service: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\", accounts: [\"MESH\"] }\n ]\n }\n}\n", "Missing": null, "Err": "", "Memberships": { diff --git a/modules/mesh-grants/internal/buscompose/users.go b/modules/mesh-grants/internal/buscompose/users.go index 5a667a96..9df3e1dd 100644 --- a/modules/mesh-grants/internal/buscompose/users.go +++ b/modules/mesh-grants/internal/buscompose/users.go @@ -175,17 +175,20 @@ func Users(r Records) ([]Principal, error) { grants := -1 for _, node := range sortedCopy(r.Nodes) { var checks []string - bus := false + bus, runner := false, "" for _, d := range r.Assigned[node] { if d.SnapshotsTheBus { bus = true } + if d.ToolRunner { + runner = d.Module // the node-engine's witness pings it by the name its manifest gives (ADR 0601 §4) + } checks = append(checks, d.Checks...) } // **One user per login the node has** (novox/hq ADR 0583 §1): the one in use, and while it rotates the other too, // each with the node's permissions. for _, login := range nodeLoginsOf(r, node) { - out = append(out, Principal{Kind: KindNode, Node: node, Login: login, Checks: checks, + out = append(out, Principal{Kind: KindNode, Node: node, Login: login, Checks: checks, Runner: runner, MovesIntoTheVault: servesMoveIn(r), StatesItsKeyToTheVault: servesVerb(r, VaultNodeKey), AsksItsOwnSecrets: servesVerb(r, VaultOwn), AsksTheVaultToSign: servesVerb(r, VaultSign), Uses: asksSeatOf(r), FollowsGrantsUserList: bus, ServedReads: servedOn(served, node), RotatesItsLogin: servesVerb(r, VaultRotateNode)}) From 478f0110e98bb7188314f8d7db3c3cba8094602f Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 14:05:07 +0100 Subject: [PATCH 15/49] Pin mesh-sdk 825483043549, the group's head of feat/l3b-bus (review round 1) --- modules/mesh-delivery-assigner/go.mod | 2 +- modules/mesh-delivery-assigner/go.sum | 4 ++-- modules/mesh-delivery-builder/buildrun/go.mod | 2 +- modules/mesh-delivery-builder/buildrun/go.sum | 4 ++-- modules/mesh-delivery-builder/go.mod | 2 +- modules/mesh-delivery-builder/go.sum | 4 ++-- modules/mesh-delivery-checker/go.mod | 2 +- modules/mesh-delivery-checker/go.sum | 4 ++-- modules/mesh-delivery-forge-bridge/go.mod | 2 +- modules/mesh-delivery-forge-bridge/go.sum | 4 ++-- modules/mesh-delivery-teller/go.mod | 2 +- modules/mesh-delivery-teller/go.sum | 4 ++-- modules/mesh-delivery-view/go.mod | 2 +- modules/mesh-delivery-view/go.sum | 4 ++-- modules/mesh-delivery-watchman/go.mod | 2 +- modules/mesh-delivery-watchman/go.sum | 4 ++-- modules/mesh-enrolment/go.mod | 2 +- modules/mesh-enrolment/go.sum | 4 ++-- 18 files changed, 27 insertions(+), 27 deletions(-) diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index 2c5e40b6..1c70e394 100644 --- a/modules/mesh-delivery-assigner/go.mod +++ b/modules/mesh-delivery-assigner/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-assigner go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-assigner/go.sum b/modules/mesh-delivery-assigner/go.sum index 271c7949..9f1230bd 100644 --- a/modules/mesh-delivery-assigner/go.sum +++ b/modules/mesh-delivery-assigner/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 h1:DAxLU3IjZ9Kof8F3kVnINcauLqzCdQ1smwMpzX8yi7I= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 h1:YihsvVnQ6LQwsuUx7if6Gp5rEB7hmx2kcdzZzhWqiVw= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/buildrun/go.mod b/modules/mesh-delivery-builder/buildrun/go.mod index b79058c5..c8f16756 100644 --- a/modules/mesh-delivery-builder/buildrun/go.mod +++ b/modules/mesh-delivery-builder/buildrun/go.mod @@ -3,7 +3,7 @@ module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/buildrun/go.sum b/modules/mesh-delivery-builder/buildrun/go.sum index 43cc4ec7..9f1230bd 100644 --- a/modules/mesh-delivery-builder/buildrun/go.sum +++ b/modules/mesh-delivery-builder/buildrun/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 h1:YihsvVnQ6LQwsuUx7if6Gp5rEB7hmx2kcdzZzhWqiVw= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/go.mod b/modules/mesh-delivery-builder/go.mod index f22b87b5..62ce4c13 100644 --- a/modules/mesh-delivery-builder/go.mod +++ b/modules/mesh-delivery-builder/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-builder go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/go.sum b/modules/mesh-delivery-builder/go.sum index ad942d9c..48e8715c 100644 --- a/modules/mesh-delivery-builder/go.sum +++ b/modules/mesh-delivery-builder/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 h1:DAxLU3IjZ9Kof8F3kVnINcauLqzCdQ1smwMpzX8yi7I= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 h1:YihsvVnQ6LQwsuUx7if6Gp5rEB7hmx2kcdzZzhWqiVw= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index 00c23872..4a06d36d 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-checker go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-checker/go.sum b/modules/mesh-delivery-checker/go.sum index ad942d9c..48e8715c 100644 --- a/modules/mesh-delivery-checker/go.sum +++ b/modules/mesh-delivery-checker/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 h1:DAxLU3IjZ9Kof8F3kVnINcauLqzCdQ1smwMpzX8yi7I= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 h1:YihsvVnQ6LQwsuUx7if6Gp5rEB7hmx2kcdzZzhWqiVw= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-forge-bridge/go.mod b/modules/mesh-delivery-forge-bridge/go.mod index 8513b089..40c9f6a7 100644 --- a/modules/mesh-delivery-forge-bridge/go.mod +++ b/modules/mesh-delivery-forge-bridge/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-forge-bridge go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-forge-bridge/go.sum b/modules/mesh-delivery-forge-bridge/go.sum index 271c7949..9f1230bd 100644 --- a/modules/mesh-delivery-forge-bridge/go.sum +++ b/modules/mesh-delivery-forge-bridge/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 h1:DAxLU3IjZ9Kof8F3kVnINcauLqzCdQ1smwMpzX8yi7I= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 h1:YihsvVnQ6LQwsuUx7if6Gp5rEB7hmx2kcdzZzhWqiVw= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-teller/go.mod b/modules/mesh-delivery-teller/go.mod index 2143e487..745a9866 100644 --- a/modules/mesh-delivery-teller/go.mod +++ b/modules/mesh-delivery-teller/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-teller go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-teller/go.sum b/modules/mesh-delivery-teller/go.sum index 271c7949..9f1230bd 100644 --- a/modules/mesh-delivery-teller/go.sum +++ b/modules/mesh-delivery-teller/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 h1:DAxLU3IjZ9Kof8F3kVnINcauLqzCdQ1smwMpzX8yi7I= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 h1:YihsvVnQ6LQwsuUx7if6Gp5rEB7hmx2kcdzZzhWqiVw= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index 5138e158..9c70e966 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-view go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index 271c7949..9f1230bd 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 h1:DAxLU3IjZ9Kof8F3kVnINcauLqzCdQ1smwMpzX8yi7I= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 h1:YihsvVnQ6LQwsuUx7if6Gp5rEB7hmx2kcdzZzhWqiVw= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-watchman/go.mod b/modules/mesh-delivery-watchman/go.mod index 1eb115ff..5aa5ed3d 100644 --- a/modules/mesh-delivery-watchman/go.mod +++ b/modules/mesh-delivery-watchman/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-watchman go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-watchman/go.sum b/modules/mesh-delivery-watchman/go.sum index bd3f7f22..92aee3b1 100644 --- a/modules/mesh-delivery-watchman/go.sum +++ b/modules/mesh-delivery-watchman/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 h1:DAxLU3IjZ9Kof8F3kVnINcauLqzCdQ1smwMpzX8yi7I= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 h1:YihsvVnQ6LQwsuUx7if6Gp5rEB7hmx2kcdzZzhWqiVw= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-enrolment/go.mod b/modules/mesh-enrolment/go.mod index 04f71c8a..ef3dca95 100644 --- a/modules/mesh-enrolment/go.mod +++ b/modules/mesh-enrolment/go.mod @@ -3,7 +3,7 @@ module mesh-enrolment go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-enrolment/go.sum b/modules/mesh-enrolment/go.sum index 271c7949..9f1230bd 100644 --- a/modules/mesh-enrolment/go.sum +++ b/modules/mesh-enrolment/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551 h1:DAxLU3IjZ9Kof8F3kVnINcauLqzCdQ1smwMpzX8yi7I= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103121808-09ec82cb3551/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 h1:YihsvVnQ6LQwsuUx7if6Gp5rEB7hmx2kcdzZzhWqiVw= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= From 22aedbe97fcc9a858523da4f3c1dc0b86cc896b6 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 14:10:08 +0100 Subject: [PATCH 16/49] =?UTF-8?q?A=20recompute=20with=20no=20merge=20write?= =?UTF-8?q?s=20the=20fingerprint's=20source=20revision=20as=20not=20known,?= =?UTF-8?q?=20so=20no=20release's=20Approve=20answers=20a=20build=20it=20n?= =?UTF-8?q?ever=20carried,=20though=20the=20source=20revision=20is=20still?= =?UTF-8?q?=20that=20release's=20(ADR=200601=20=C2=A71;=20review=20round?= =?UTF-8?q?=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmd/mesh-delivery-assigner/pass_test.go | 9 ++++--- .../internal/index/source.go | 7 ----- .../internal/index/toolchains.go | 8 +++--- .../internal/index/toolchains_test.go | 26 ++++++++++++++----- 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go index 799ef530..d9912d36 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go @@ -811,10 +811,11 @@ func aReleasesApproveAnswersTheMoveOfItsBuildAlone(t *testing.T) { {"a setting changed at the build it runs", true, fpTest, commit, 7, false, true}, {"no approved release carries it", true, fpOld, "", 7, false, true}, {"the release merged another commit", true, fpOld, strings.Repeat("2", 40), 7, false, true}, - // Review round 1: its fingerprint moved by a recompute after the release (a toolchain, a package), with no merge of - // web: stamped with that moment's revision, whose release carries nothing of web; the earlier Approve answers nothing. - {"a fingerprint moved by a recompute after the release", true, fpOld, commit, 8, false, true}, - {"a fingerprint whose revision is not known", true, fpOld, commit, 0, false, true}, + // Its fingerprint last moved at another release's revision, which merged nothing of web: that Approve answers nothing. + {"a fingerprint another release moved", true, fpOld, commit, 8, false, true}, + // Review rounds 1 and 2: moved by a recompute after the release (a toolchain, a package, a base) with no merge, the + // indexer writes fp-src 0, not known, though Src is still the release's: asked of a person. + {"a fingerprint moved by a recompute after the release", true, fpOld, commit, 0, false, true}, } { asksKV := &memKV{values: map[string][]byte{}} p := newPasser(buckets{delivery.AssignerAsksBucket: asksKV}, time.Now()) diff --git a/modules/mesh-delivery-indexer/internal/index/source.go b/modules/mesh-delivery-indexer/internal/index/source.go index b7318bf4..7dd92d34 100644 --- a/modules/mesh-delivery-indexer/internal/index/source.go +++ b/modules/mesh-delivery-indexer/internal/index/source.go @@ -14,13 +14,6 @@ import ( // with it (Module.Src) when the head the source names for its repository is the merge commit the module's fingerprint // was computed at: from the modules bucket when the merge was heard first, and when the merge is heard after the source. -// sourceRev is the latest source revision seen, 0 before any. -func (ix *Indexer) sourceRev() uint64 { - ix.mu.Lock() - defer ix.mu.Unlock() - return ix.source.rev -} - // sourceNow is the latest source revision seen. type sourceNow struct { rev uint64 diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains.go b/modules/mesh-delivery-indexer/internal/index/toolchains.go index c8cee117..269f8fd1 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains.go @@ -313,9 +313,11 @@ func (ix *Indexer) recomputeModules(ctx context.Context, overlay map[string]deli return nil, false, nil } if cur.FP2 != r.fp { - // Moved with no merge of its own repository (a toolchain, a package, a base): the source revision of now, whose - // release does not carry it, never the Src of the merge it was last merged at (ADR 0601 §1). - cur.FPSrc = ix.sourceRev() + // Moved with no merge (a toolchain, a package, a base): no release carries this build, and the source revision + // moves only at a merge, so it is not known (0) and the move is asked of a person; never the revision of now, + // which may be the very release that last merged the module (ADR 0601 §1). The source stamp leaves it, since + // Src is set. + cur.FPSrc = 0 } cur.FP2, cur.None2, cur.Toolchains, cur.Packages = r.fp, r.none, r.tcs, r.pkgs cur.Inputs2, cur.ArtifactPaths = inputs, paths diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go index 368f0602..5ee8da5b 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go @@ -113,19 +113,31 @@ func TestAgreedToolchainChangesAGoModulesReasonWithNoMerge(t *testing.T) { // 14 if m, _ := moduleOf(t, mods, "avahi"); m.None2 != delivery.ToolchainNoReport { t.Fatalf("before: none2 = %q", m.None2) } - ix.mu.Lock() - ix.source.rev = 11 // the source revision of now, whose release merged nothing of avahi's - ix.mu.Unlock() + // The release that last merged avahi: its source revision names avahi's commit, so Src (and FPSrc) are 7. + merged, _ := moduleOf(t, mods, "avahi") + src := delivery.Source{Group: "g", Heads: map[string]string{merged.Repository: merged.Commit}} + if err := ix.ApplySource(context.Background(), 7, src); err != nil { + t.Fatal(err) + } + if m, _ := moduleOf(t, mods, "avahi"); m.Src != 7 || m.FPSrc != 7 { + t.Fatalf("before: src %d fp-src %d, want 7 and 7", m.Src, m.FPSrc) + } reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1") reportToolchain(t, ix, "mesh-build-go", "ace", "go", "go1.27.1") m, _ := moduleOf(t, mods, "avahi") if m.None2 != "" || m.FP2 == "" { t.Errorf("none2 = %q fp2 = %q, want a fingerprint: no builder input is waited for (ADR 0530 §1)", m.None2, m.FP2) } - // ADR 0601 §1: a fingerprint moved with no merge is stamped with the source revision of the moment, never the Src of - // the merge it was last merged at, so no earlier release is taken to carry it. - if m.FPSrc != 11 || m.Src == 11 { - t.Errorf("a fingerprint moved by a recompute: fp-src %d, src %d; want fp-src 11", m.FPSrc, m.Src) + // ADR 0601 §1 (review round 2): a fingerprint moved with no merge has no release that carries it: fp-src 0, not known, + // though the source revision is still the release's at the module's own Src; a source revision heard again stamps nothing. + if m.FPSrc != 0 { + t.Errorf("a fingerprint moved by a recompute: fp-src %d, want 0", m.FPSrc) + } + if err := ix.ApplySource(context.Background(), 8, src); err != nil { + t.Fatal(err) + } + if again, _ := moduleOf(t, mods, "avahi"); again.FPSrc != 0 { + t.Errorf("a source revision stamped the recomputed fingerprint: fp-src %d", again.FPSrc) } } From 2f387a3f4c1e428beadddb6bee68d0c7e943e117 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 14:12:50 +0100 Subject: [PATCH 17/49] Pin mesh-sdk e7ce86ff62b5, the group's head of feat/l3b-bus (review round 2) --- modules/mesh-delivery-assigner/go.mod | 2 +- modules/mesh-delivery-assigner/go.sum | 4 ++-- modules/mesh-delivery-builder/buildrun/go.mod | 2 +- modules/mesh-delivery-builder/buildrun/go.sum | 4 ++-- modules/mesh-delivery-builder/go.mod | 2 +- modules/mesh-delivery-builder/go.sum | 4 ++-- modules/mesh-delivery-checker/go.mod | 2 +- modules/mesh-delivery-checker/go.sum | 4 ++-- modules/mesh-delivery-forge-bridge/go.mod | 2 +- modules/mesh-delivery-forge-bridge/go.sum | 4 ++-- modules/mesh-delivery-teller/go.mod | 2 +- modules/mesh-delivery-teller/go.sum | 4 ++-- modules/mesh-delivery-view/go.mod | 2 +- modules/mesh-delivery-view/go.sum | 4 ++-- modules/mesh-delivery-watchman/go.mod | 2 +- modules/mesh-delivery-watchman/go.sum | 4 ++-- modules/mesh-enrolment/go.mod | 2 +- modules/mesh-enrolment/go.sum | 4 ++-- 18 files changed, 27 insertions(+), 27 deletions(-) diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index 1c70e394..e3023d64 100644 --- a/modules/mesh-delivery-assigner/go.mod +++ b/modules/mesh-delivery-assigner/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-assigner go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-assigner/go.sum b/modules/mesh-delivery-assigner/go.sum index 9f1230bd..310e531a 100644 --- a/modules/mesh-delivery-assigner/go.sum +++ b/modules/mesh-delivery-assigner/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 h1:YihsvVnQ6LQwsuUx7if6Gp5rEB7hmx2kcdzZzhWqiVw= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 h1:snMd7KsMT+tKlWYtrXSXrHF4u60DElvHmbe/aq58DqU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/buildrun/go.mod b/modules/mesh-delivery-builder/buildrun/go.mod index c8f16756..e8fab913 100644 --- a/modules/mesh-delivery-builder/buildrun/go.mod +++ b/modules/mesh-delivery-builder/buildrun/go.mod @@ -3,7 +3,7 @@ module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/buildrun/go.sum b/modules/mesh-delivery-builder/buildrun/go.sum index 9f1230bd..310e531a 100644 --- a/modules/mesh-delivery-builder/buildrun/go.sum +++ b/modules/mesh-delivery-builder/buildrun/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 h1:YihsvVnQ6LQwsuUx7if6Gp5rEB7hmx2kcdzZzhWqiVw= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 h1:snMd7KsMT+tKlWYtrXSXrHF4u60DElvHmbe/aq58DqU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/go.mod b/modules/mesh-delivery-builder/go.mod index 62ce4c13..4204c6fd 100644 --- a/modules/mesh-delivery-builder/go.mod +++ b/modules/mesh-delivery-builder/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-builder go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/go.sum b/modules/mesh-delivery-builder/go.sum index 48e8715c..c4dd22a5 100644 --- a/modules/mesh-delivery-builder/go.sum +++ b/modules/mesh-delivery-builder/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 h1:YihsvVnQ6LQwsuUx7if6Gp5rEB7hmx2kcdzZzhWqiVw= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 h1:snMd7KsMT+tKlWYtrXSXrHF4u60DElvHmbe/aq58DqU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index 4a06d36d..f8d87659 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-checker go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-checker/go.sum b/modules/mesh-delivery-checker/go.sum index 48e8715c..c4dd22a5 100644 --- a/modules/mesh-delivery-checker/go.sum +++ b/modules/mesh-delivery-checker/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 h1:YihsvVnQ6LQwsuUx7if6Gp5rEB7hmx2kcdzZzhWqiVw= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 h1:snMd7KsMT+tKlWYtrXSXrHF4u60DElvHmbe/aq58DqU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-forge-bridge/go.mod b/modules/mesh-delivery-forge-bridge/go.mod index 40c9f6a7..c0ac15cf 100644 --- a/modules/mesh-delivery-forge-bridge/go.mod +++ b/modules/mesh-delivery-forge-bridge/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-forge-bridge go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-forge-bridge/go.sum b/modules/mesh-delivery-forge-bridge/go.sum index 9f1230bd..310e531a 100644 --- a/modules/mesh-delivery-forge-bridge/go.sum +++ b/modules/mesh-delivery-forge-bridge/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 h1:YihsvVnQ6LQwsuUx7if6Gp5rEB7hmx2kcdzZzhWqiVw= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 h1:snMd7KsMT+tKlWYtrXSXrHF4u60DElvHmbe/aq58DqU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-teller/go.mod b/modules/mesh-delivery-teller/go.mod index 745a9866..db55fe4f 100644 --- a/modules/mesh-delivery-teller/go.mod +++ b/modules/mesh-delivery-teller/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-teller go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-teller/go.sum b/modules/mesh-delivery-teller/go.sum index 9f1230bd..310e531a 100644 --- a/modules/mesh-delivery-teller/go.sum +++ b/modules/mesh-delivery-teller/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 h1:YihsvVnQ6LQwsuUx7if6Gp5rEB7hmx2kcdzZzhWqiVw= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 h1:snMd7KsMT+tKlWYtrXSXrHF4u60DElvHmbe/aq58DqU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index 9c70e966..5e0b38bd 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-view go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index 9f1230bd..310e531a 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 h1:YihsvVnQ6LQwsuUx7if6Gp5rEB7hmx2kcdzZzhWqiVw= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 h1:snMd7KsMT+tKlWYtrXSXrHF4u60DElvHmbe/aq58DqU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-watchman/go.mod b/modules/mesh-delivery-watchman/go.mod index 5aa5ed3d..1eddb567 100644 --- a/modules/mesh-delivery-watchman/go.mod +++ b/modules/mesh-delivery-watchman/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-watchman go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-watchman/go.sum b/modules/mesh-delivery-watchman/go.sum index 92aee3b1..0f0eb24b 100644 --- a/modules/mesh-delivery-watchman/go.sum +++ b/modules/mesh-delivery-watchman/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 h1:YihsvVnQ6LQwsuUx7if6Gp5rEB7hmx2kcdzZzhWqiVw= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 h1:snMd7KsMT+tKlWYtrXSXrHF4u60DElvHmbe/aq58DqU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-enrolment/go.mod b/modules/mesh-enrolment/go.mod index ef3dca95..d1fe64aa 100644 --- a/modules/mesh-enrolment/go.mod +++ b/modules/mesh-enrolment/go.mod @@ -3,7 +3,7 @@ module mesh-enrolment go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-enrolment/go.sum b/modules/mesh-enrolment/go.sum index 9f1230bd..310e531a 100644 --- a/modules/mesh-enrolment/go.sum +++ b/modules/mesh-enrolment/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549 h1:YihsvVnQ6LQwsuUx7if6Gp5rEB7hmx2kcdzZzhWqiVw= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103125438-825483043549/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 h1:snMd7KsMT+tKlWYtrXSXrHF4u60DElvHmbe/aq58DqU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= From 5ee3b4c15fdfad29db451187d4d8813f40e2d37f Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 16:25:31 +0100 Subject: [PATCH 18/49] =?UTF-8?q?Pin=20mesh-sdk=2025b371bc8397,=20the=20gr?= =?UTF-8?q?oup's=20head=20of=20feat/l3b-bus=20with=20the=20module=20graph?= =?UTF-8?q?=20and=20the=20seat=20mesh-catalog's=20contract=20(ADR=200603?= =?UTF-8?q?=20=C2=A72,=20ADR=200604=20=C2=A71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/mesh-delivery-assigner/go.mod | 2 +- modules/mesh-delivery-assigner/go.sum | 4 ++-- modules/mesh-delivery-builder/buildrun/go.mod | 2 +- modules/mesh-delivery-builder/buildrun/go.sum | 4 ++-- modules/mesh-delivery-builder/go.mod | 2 +- modules/mesh-delivery-builder/go.sum | 4 ++-- modules/mesh-delivery-checker/go.mod | 2 +- modules/mesh-delivery-checker/go.sum | 4 ++-- modules/mesh-delivery-forge-bridge/go.mod | 2 +- modules/mesh-delivery-forge-bridge/go.sum | 4 ++-- modules/mesh-delivery-teller/go.mod | 2 +- modules/mesh-delivery-teller/go.sum | 4 ++-- modules/mesh-delivery-view/go.mod | 2 +- modules/mesh-delivery-view/go.sum | 4 ++-- modules/mesh-delivery-watchman/go.mod | 2 +- modules/mesh-delivery-watchman/go.sum | 4 ++-- modules/mesh-enrolment/go.mod | 2 +- modules/mesh-enrolment/go.sum | 4 ++-- 18 files changed, 27 insertions(+), 27 deletions(-) diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index e3023d64..898173e7 100644 --- a/modules/mesh-delivery-assigner/go.mod +++ b/modules/mesh-delivery-assigner/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-assigner go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-assigner/go.sum b/modules/mesh-delivery-assigner/go.sum index 310e531a..cf2e5165 100644 --- a/modules/mesh-delivery-assigner/go.sum +++ b/modules/mesh-delivery-assigner/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 h1:snMd7KsMT+tKlWYtrXSXrHF4u60DElvHmbe/aq58DqU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 h1:DqBOfy5j5+g8168XK+ibmZugqVehD8F+PDtQxDTpGEE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/buildrun/go.mod b/modules/mesh-delivery-builder/buildrun/go.mod index e8fab913..d4e86ab4 100644 --- a/modules/mesh-delivery-builder/buildrun/go.mod +++ b/modules/mesh-delivery-builder/buildrun/go.mod @@ -3,7 +3,7 @@ module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/buildrun/go.sum b/modules/mesh-delivery-builder/buildrun/go.sum index 310e531a..cf2e5165 100644 --- a/modules/mesh-delivery-builder/buildrun/go.sum +++ b/modules/mesh-delivery-builder/buildrun/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 h1:snMd7KsMT+tKlWYtrXSXrHF4u60DElvHmbe/aq58DqU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 h1:DqBOfy5j5+g8168XK+ibmZugqVehD8F+PDtQxDTpGEE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/go.mod b/modules/mesh-delivery-builder/go.mod index 4204c6fd..ea4bcfc0 100644 --- a/modules/mesh-delivery-builder/go.mod +++ b/modules/mesh-delivery-builder/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-builder go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/go.sum b/modules/mesh-delivery-builder/go.sum index c4dd22a5..da4e4d5e 100644 --- a/modules/mesh-delivery-builder/go.sum +++ b/modules/mesh-delivery-builder/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 h1:snMd7KsMT+tKlWYtrXSXrHF4u60DElvHmbe/aq58DqU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 h1:DqBOfy5j5+g8168XK+ibmZugqVehD8F+PDtQxDTpGEE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index f8d87659..fca9f123 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-checker go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-checker/go.sum b/modules/mesh-delivery-checker/go.sum index c4dd22a5..da4e4d5e 100644 --- a/modules/mesh-delivery-checker/go.sum +++ b/modules/mesh-delivery-checker/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 h1:snMd7KsMT+tKlWYtrXSXrHF4u60DElvHmbe/aq58DqU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 h1:DqBOfy5j5+g8168XK+ibmZugqVehD8F+PDtQxDTpGEE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-forge-bridge/go.mod b/modules/mesh-delivery-forge-bridge/go.mod index c0ac15cf..6aabb07e 100644 --- a/modules/mesh-delivery-forge-bridge/go.mod +++ b/modules/mesh-delivery-forge-bridge/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-forge-bridge go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-forge-bridge/go.sum b/modules/mesh-delivery-forge-bridge/go.sum index 310e531a..cf2e5165 100644 --- a/modules/mesh-delivery-forge-bridge/go.sum +++ b/modules/mesh-delivery-forge-bridge/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 h1:snMd7KsMT+tKlWYtrXSXrHF4u60DElvHmbe/aq58DqU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 h1:DqBOfy5j5+g8168XK+ibmZugqVehD8F+PDtQxDTpGEE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-teller/go.mod b/modules/mesh-delivery-teller/go.mod index db55fe4f..3e4716d6 100644 --- a/modules/mesh-delivery-teller/go.mod +++ b/modules/mesh-delivery-teller/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-teller go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-teller/go.sum b/modules/mesh-delivery-teller/go.sum index 310e531a..cf2e5165 100644 --- a/modules/mesh-delivery-teller/go.sum +++ b/modules/mesh-delivery-teller/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 h1:snMd7KsMT+tKlWYtrXSXrHF4u60DElvHmbe/aq58DqU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 h1:DqBOfy5j5+g8168XK+ibmZugqVehD8F+PDtQxDTpGEE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index 5e0b38bd..6d129e0e 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-view go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index 310e531a..cf2e5165 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 h1:snMd7KsMT+tKlWYtrXSXrHF4u60DElvHmbe/aq58DqU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 h1:DqBOfy5j5+g8168XK+ibmZugqVehD8F+PDtQxDTpGEE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-watchman/go.mod b/modules/mesh-delivery-watchman/go.mod index 1eddb567..6ff48a5f 100644 --- a/modules/mesh-delivery-watchman/go.mod +++ b/modules/mesh-delivery-watchman/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-watchman go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-watchman/go.sum b/modules/mesh-delivery-watchman/go.sum index 0f0eb24b..acc8e351 100644 --- a/modules/mesh-delivery-watchman/go.sum +++ b/modules/mesh-delivery-watchman/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 h1:snMd7KsMT+tKlWYtrXSXrHF4u60DElvHmbe/aq58DqU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 h1:DqBOfy5j5+g8168XK+ibmZugqVehD8F+PDtQxDTpGEE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-enrolment/go.mod b/modules/mesh-enrolment/go.mod index d1fe64aa..945d2ac2 100644 --- a/modules/mesh-enrolment/go.mod +++ b/modules/mesh-enrolment/go.mod @@ -3,7 +3,7 @@ module mesh-enrolment go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-enrolment/go.sum b/modules/mesh-enrolment/go.sum index 310e531a..cf2e5165 100644 --- a/modules/mesh-enrolment/go.sum +++ b/modules/mesh-enrolment/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5 h1:snMd7KsMT+tKlWYtrXSXrHF4u60DElvHmbe/aq58DqU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103131008-e7ce86ff62b5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 h1:DqBOfy5j5+g8168XK+ibmZugqVehD8F+PDtQxDTpGEE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= From f70faed5b67be3a83ad6adbe7e6090ecea9b3add Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 16:27:57 +0100 Subject: [PATCH 19/49] =?UTF-8?q?The=20indexer=20keeps=20the=20module=20gr?= =?UTF-8?q?aph=20as=20its=20state=20graph:=20each=20module's=20build=20and?= =?UTF-8?q?=20provision=20edges=20written=20at=20every=20recompute=20when?= =?UTF-8?q?=20they=20change,=20at=20most=202000=20modules,=20a=20module's?= =?UTF-8?q?=20key=20deleted=20once=20the=20modules=20state=20no=20longer?= =?UTF-8?q?=20holds=20it=20(ADR=200603=20=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmd/mesh-delivery-indexer/main.go | 4 +- .../internal/index/candidates.go | 2 + .../internal/index/graph.go | 131 ++++++++++++++++++ .../internal/index/manifests_test.go | 2 +- .../internal/index/toolchains.go | 10 +- .../internal/index/toolchains_test.go | 54 ++++++++ .../mesh-delivery-indexer/manifest_test.go | 3 +- modules/mesh-delivery-indexer/module.json | 3 +- 8 files changed, 203 insertions(+), 6 deletions(-) create mode 100644 modules/mesh-delivery-indexer/internal/index/graph.go diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go index bbb31c68..eb111122 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go @@ -299,12 +299,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // How many modules use each old section (issue 1966 D4): a state this delivery brings, bound when it appears; until then // the judge refuses no use of an old section. oldSections := process.AwaitBucket(ctx, js, conn, index.OldSectionsBucket, "the count of the old sections is not kept, so the judge refuses no use of one", logger.Printf) + // The module graph (ADR 0603 §2): a state this delivery brings, bound when it appears; until then no graph is written. + graph := process.AwaitBucket(ctx, js, conn, delivery.GraphBucket, "no module graph is written", logger.Printf) ix := &index.Indexer{OldSections: awaitedStore{oldSections}, Store: kvStore{candidates}, Modules: kvStore{modules}, Manifests: kvStore{manifests}, Taken: awaitedStore{taken}, Builds: known, Verdicts: awaitedStore{verdicts}, SayRefused: sayRefused(js, cred, logger.Printf), SayAccepted: sayAccepted(js, cred, logger.Printf), Predefined: awaitedStore{predefined}, ConfigCandidates: awaitedStore{configCandidates}, Logf: logger.Printf, Named: namedVia(calls.Call), Trunk: trunkVia(calls.Call), Contains: containsVia(calls.Call), Read: readVia(calls.Call), ReadOpen: openVia(calls.Call), Packages: awaitedStore{packages}, Files: filesVia(calls.Call), GoSum: goSumVia(calls.Call), GoMod: goModVia(calls.Call), - OwnBuild: ownBuildVia(calls.Call, cred.Node)} + OwnBuild: ownBuildVia(calls.Call, cred.Node), Graph: awaitedStore{graph}} // The log's entries at the start are read before any fingerprint is computed or registered module indexed (below); each // new one indexes the module it names when that module is registered and the bucket holds none (registered.go). var started atomic.Bool diff --git a/modules/mesh-delivery-indexer/internal/index/candidates.go b/modules/mesh-delivery-indexer/internal/index/candidates.go index 2e207599..e0cba4fe 100644 --- a/modules/mesh-delivery-indexer/internal/index/candidates.go +++ b/modules/mesh-delivery-indexer/internal/index/candidates.go @@ -67,6 +67,8 @@ type Indexer struct { // Packages is the packages state (delivery.PackagesBucket, packages.go; novox/hq ADR 0593 §2); nil publishes none, // and every fingerprint takes the version line. Packages Store + // Graph is the module graph's state (delivery.GraphBucket, graph.go; novox/hq ADR 0603 §2); nil writes no graph. + Graph Store // Files asks the forge for every blob under each directory at a commit, by directory then path relative to it, each // with its git object id (gitea.gitea_commit_trees with files): the listing a package's digest is computed from. // Nil publishes no package. diff --git a/modules/mesh-delivery-indexer/internal/index/graph.go b/modules/mesh-delivery-indexer/internal/index/graph.go new file mode 100644 index 00000000..efcfad44 --- /dev/null +++ b/modules/mesh-delivery-indexer/internal/index/graph.go @@ -0,0 +1,131 @@ +package index + +import ( + "context" + "fmt" + "maps" + "slices" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// The module graph (novox/hq ADR 0603 §2): at every recompute the indexer writes, in its state graph +// (delivery.GraphBucket), one key per module it holds the inputs of, its build and provision edges as that recompute +// computed them, and deletes the key of a module the modules state no longer holds. A key is written only when its value +// changes, so its history of 5 is five changes of the module's edges. The delivery view, the checker and the seat +// mesh-catalog's verbs read it; nothing stores an order (ADR 0603 §1). + +// GraphMost is the most modules the graph holds (ADR 0603 §2): past it, the modules after the first GraphMost in name +// order are not written, and it is said in the log. The cap is the indexer's code, not a manifest field, until a judge +// that knows such a field runs (the lead's decision of L3). +const GraphMost = 2000 + +// graphEdges is what a recompute knows of one module for its graph node. +type graphEdges struct { + module string + cur delivery.Module + manifest catalogue.Manifest + bases []string + fp, none string + tcs map[string]delivery.ToolchainUsed + pkgs delivery.PackageVersions +} + +// graphNode is the graph node of e: its edges, the awaited provisions judged against providers (catalogue.Awaited, as +// the assigner judges them). +func graphNode(e graphEdges, providers func(string) []catalogue.Manifest) delivery.GraphNode { + if e.fp == "" && e.none == "" { + e.none = "its fingerprint was not computed" + } + n := delivery.GraphNode{FP: e.fp, None: e.none, Repository: e.cur.Repository, Path: e.cur.Path, Bases: e.bases, + Packages: slices.Sorted(maps.Keys(e.pkgs))} + tcs := e.tcs + if len(tcs) == 0 { + tcs = e.cur.Toolchains // no fingerprint now: the toolchain it was last computed with still builds it + } + for language, t := range tcs { + builder, known := ToolchainModules[language] + if !known { + continue + } + if n.Toolchains == nil { + n.Toolchains = map[string]delivery.GraphToolchain{} + } + n.Toolchains[language] = delivery.GraphToolchain{Module: builder, Version: t.Version, Recipe: t.Recipe} + } + for _, o := range e.manifest.Provides { + n.Provides = append(n.Provides, o.Name) + } + for _, r := range e.manifest.Requires { + if !e.manifest.OptionalRequires[r] { + n.Requires = append(n.Requires, r) + } + } + n.Awaits = catalogue.Awaited(e.manifest, providers) + return n +} + +// writeGraph writes the graph node of every module of edges whose value changed, at most GraphMost of them, then +// deletes each graph key whose module the modules state no longer holds. Nil Graph writes nothing. +func (ix *Indexer) writeGraph(ctx context.Context, edges map[string]graphEdges) error { + if ix.Graph == nil { + return nil + } + names := slices.Sorted(maps.Keys(edges)) + if len(names) > GraphMost { + ix.logf("the module graph holds at most %d modules: %d are known, so %s and the %d after it are not written", + GraphMost, len(names), names[GraphMost], len(names)-GraphMost-1) + names = names[:GraphMost] + } + providers := func(provision string) []catalogue.Manifest { + var out []catalogue.Manifest + for _, name := range slices.Sorted(maps.Keys(edges)) { + if slices.Contains(edges[name].manifest.Offers(), provision) { + out = append(out, edges[name].manifest) + } + } + return out + } + for _, name := range names { + key, err := delivery.GraphKey(name) + if err != nil { + ix.logf("graph.%s is not written: %v", name, err) + continue + } + next := graphNode(edges[name], providers) + err = write(ctx, ix.Graph, key, func(value []byte, found bool) ([]byte, bool, error) { + if found { + if cur, err := delivery.DecodeGraphNode(value); err == nil && delivery.SameGraphNode(cur, next) { + return nil, false, nil + } + } + data, err := delivery.EncodeGraphNode(next) + if err != nil { + return nil, false, fmt.Errorf("graph.%s: %w", key, err) + } + return data, false, nil + }) + if err != nil { + ix.logf("graph.%s is not written: %v", key, err) + } + } + held, err := ix.Modules.Keys(ctx) + if err != nil { + return fmt.Errorf("listing the modules for the graph: %w", err) + } + keys, err := ix.Graph.Keys(ctx) + if err != nil { + return fmt.Errorf("listing the graph: %w", err) + } + for _, key := range keys { + if slices.Contains(held, key) { + continue + } + if err := write(ctx, ix.Graph, key, func(_ []byte, found bool) ([]byte, bool, error) { return nil, found, nil }); err != nil { + return fmt.Errorf("graph.%s: %w", key, err) + } + ix.logf("graph.%s deleted: the modules state holds no module %s", key, key) + } + return nil +} diff --git a/modules/mesh-delivery-indexer/internal/index/manifests_test.go b/modules/mesh-delivery-indexer/internal/index/manifests_test.go index 064dc488..835092ca 100644 --- a/modules/mesh-delivery-indexer/internal/index/manifests_test.go +++ b/modules/mesh-delivery-indexer/internal/index/manifests_test.go @@ -33,7 +33,7 @@ func manifestsIndexer(t *testing.T, recs ...[2]any) (*Indexer, *memStore, *memSt t.Helper() ix, mods := modulesIndexer(t, recs...) manifests := newMem() - ix.Manifests = manifests + ix.Manifests, ix.Graph = manifests, newMem() return ix, mods, manifests } diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains.go b/modules/mesh-delivery-indexer/internal/index/toolchains.go index 269f8fd1..78d0dc83 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains.go @@ -111,6 +111,7 @@ type toolchainOrNone struct { type known struct { key string cur delivery.Module + manifest catalogue.Manifest languages map[string]string bases []string contexts bool @@ -196,7 +197,7 @@ func (ix *Indexer) recomputeModules(ctx context.Context, overlay map[string]deli cur, stale[key] = again, true held[key] = cur } - all[key] = &known{key: key, cur: cur, languages: languagesOf(m), bases: basesOf(m), contexts: hasContext(m), held: before} + all[key] = &known{key: key, cur: cur, manifest: m, languages: languagesOf(m), bases: basesOf(m), contexts: hasContext(m), held: before} } tools := ix.agreed() published := ix.publishedAll(ctx) @@ -278,9 +279,11 @@ func (ix *Indexer) recomputeModules(ctx context.Context, overlay map[string]deli return r } finals := map[string]delivery.Module{} + edges := map[string]graphEdges{} // the module graph's nodes of this recompute (graph.go) for _, key := range slices.Sorted(maps.Keys(all)) { k := all[key] r := of(key, nil) + edges[key] = graphEdges{module: key, cur: k.cur, manifest: k.manifest, bases: k.bases, fp: r.fp, none: r.none, tcs: r.tcs, pkgs: r.pkgs} if _, merged := overlay[key]; merged { final := overlay[key] final.FP2, final.None2, final.Inputs2, final.Toolchains, final.Packages = r.fp, r.none, k.cur.Inputs2, r.tcs, r.pkgs @@ -332,7 +335,10 @@ func (ix *Indexer) recomputeModules(ctx context.Context, overlay map[string]deli w.wrote(key) } } - return ix.writeOverlay(ctx, overlay, finals, w) + if err := ix.writeOverlay(ctx, overlay, finals, w); err != nil { + return err + } + return ix.writeGraph(ctx, edges) } // writeOverlay writes the records of a merge, each once and only if it differs from what the bucket holds; finals has diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go index 5ee8da5b..b9bd4efc 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go @@ -60,6 +60,20 @@ type mod struct { // merged applies one merge of the catalogue carrying these modules (every one a Go bundle's go.sum, a TypeScript bundle's // package-lock.json) and returns the indexer. +// graphOf is the graph node the indexer holds of module. +func graphOf(t *testing.T, ix *Indexer, module string) delivery.GraphNode { + t.Helper() + v, ok := ix.Graph.(*memStore).vals[module] + if !ok { + t.Fatalf("graph.%s is not held", module) + } + g, err := delivery.DecodeGraphNode(v) + if err != nil { + t.Fatal(err) + } + return g +} + func newFormIndexer(t *testing.T, mods ...mod) (*Indexer, *memStore) { t.Helper() ix, store, f := newFormIndexerUnmerged(t, mods...) @@ -122,9 +136,24 @@ func TestAgreedToolchainChangesAGoModulesReasonWithNoMerge(t *testing.T) { // 14 if m, _ := moduleOf(t, mods, "avahi"); m.Src != 7 || m.FPSrc != 7 { t.Fatalf("before: src %d fp-src %d, want 7 and 7", m.Src, m.FPSrc) } + if g := graphOf(t, ix, "avahi"); g.None != delivery.ToolchainNoReport || g.Toolchains != nil { + t.Errorf("before: graph.avahi = %+v, want no toolchain edge and the reason", g) + } reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1") reportToolchain(t, ix, "mesh-build-go", "ace", "go", "go1.27.1") m, _ := moduleOf(t, mods, "avahi") + // L3 (ADR 0603 §2, §3): the agreement moving rewrites the module's toolchain edge, and again when it moves on. + if g := graphOf(t, ix, "avahi"); g.FP != m.FP2 || g.Toolchains["go"].Version != "go1.27.1" || g.Toolchains["go"].Module != "mesh-build-go" { + t.Errorf("after: graph.avahi = %+v, want the toolchain edge of go1.27.1", g) + } + reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.28.0") + reportToolchain(t, ix, "mesh-build-go", "ace", "go", "go1.28.0") + if g := graphOf(t, ix, "avahi"); g.Toolchains["go"].Version != "go1.28.0" { + t.Errorf("moved on: graph.avahi = %+v, want the toolchain edge of go1.28.0", g) + } + reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1") + reportToolchain(t, ix, "mesh-build-go", "ace", "go", "go1.27.1") + m, _ = moduleOf(t, mods, "avahi") if m.None2 != "" || m.FP2 == "" { t.Errorf("none2 = %q fp2 = %q, want a fingerprint: no builder input is waited for (ADR 0530 §1)", m.None2, m.FP2) } @@ -202,6 +231,31 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { // if m, _ := moduleOf(t, mods, "other"); m.FP2 != "" || !strings.Contains(m.None2, "ts-base") || !strings.Contains(m.None2, "base") { t.Errorf("other: fp2 = %q none2 = %q, want none2 naming the base ts-base", m.FP2, m.None2) } + // L3 (ADR 0603 §2): the module graph holds each module's edges as this recompute computed them. + goTool := map[string]delivery.GraphToolchain{"go": {Module: "mesh-build-go", Version: "go1.27.1", Recipe: "1"}} + if g := graphOf(t, ix, "avahi"); g.FP != want["avahi"] || !reflect.DeepEqual(g.Bases, []string{"mesh-tools"}) || + !reflect.DeepEqual(g.Toolchains, goTool) || g.Repository != catalogueRepo || g.Path != "modules/avahi" { + t.Errorf("graph.avahi = %+v", g) + } + if g := graphOf(t, ix, "other"); g.FP != "" || !strings.Contains(g.None, "ts-base") || !reflect.DeepEqual(g.Bases, []string{"ts-base"}) { + t.Errorf("graph.other = %+v, want its base and its reason", g) + } + graph := ix.Graph.(*memStore) + written := graph.writes + if err := ix.Recompute(context.Background()); err != nil || graph.writes != written { + t.Errorf("a recompute that moves nothing wrote the graph %d times (%v)", graph.writes-written, err) + } + // A module the modules state no longer holds (a merge dropped it, issue 1900) leaves the graph at the next recompute. + delete(mods.vals, "other") + if err := ix.Recompute(context.Background()); err != nil { + t.Fatal(err) + } + if _, held := graph.vals["other"]; held { + t.Error("graph.other is still held after its module went") + } + if _, held := graph.vals["avahi"]; !held { + t.Error("graph.avahi went with another module") + } a := agentBuild("build-1792587792086548572", agentSrcY, "2026-10-21T15:03:00Z") _ = ix.Builds.Put(a[1].([]byte)) if err := ix.Recompute(context.Background()); err != nil { diff --git a/modules/mesh-delivery-indexer/manifest_test.go b/modules/mesh-delivery-indexer/manifest_test.go index 0c6c61f5..c77c7b80 100644 --- a/modules/mesh-delivery-indexer/manifest_test.go +++ b/modules/mesh-delivery-indexer/manifest_test.go @@ -90,7 +90,8 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build t.Errorf("emits = %v, want manifest-refused and manifest-accepted", m.Emits) } // 1126 note 76: heard keeps the last 64 FORGE messages the indexer handled, for indexer_heard. - if want := []state{{Name: "candidates", History: 10}, {Name: "heard", History: 64}, {Name: "manifests", History: 1}, {Name: "modules", History: 20}, {Name: "predefined", History: 5}, {Name: "config-candidates", History: 5}, {Name: "taken", History: 1}, {Name: "takeins", History: 1}, {Name: "verdicts", History: 5}, {Name: "oldsections", History: 1}, {Name: delivery.PackagesState, History: delivery.PackagesHistory}}; !reflect.DeepEqual(m.State, want) { + if want := []state{{Name: "candidates", History: 10}, {Name: "heard", History: 64}, {Name: "manifests", History: 1}, {Name: "modules", History: 20}, {Name: "predefined", History: 5}, {Name: "config-candidates", History: 5}, {Name: "taken", History: 1}, {Name: "takeins", History: 1}, {Name: "verdicts", History: 5}, {Name: "oldsections", History: 1}, {Name: delivery.PackagesState, History: delivery.PackagesHistory}, + {Name: delivery.GraphState, History: delivery.GraphHistory}}; !reflect.DeepEqual(m.State, want) { t.Errorf("state = %+v, want %+v", m.State, want) } if want := []string{"mesh-build-go.toolchains", "mesh-build-node.toolchains", "mesh-delivery-assigner.placement", "mesh-delivery-builder.build-log", "mesh-delivery-checker.previews", "mesh-delivery-gate.source"}; !reflect.DeepEqual(sorted(m.Reads), want) { diff --git a/modules/mesh-delivery-indexer/module.json b/modules/mesh-delivery-indexer/module.json index 45067500..11fac677 100644 --- a/modules/mesh-delivery-indexer/module.json +++ b/modules/mesh-delivery-indexer/module.json @@ -25,7 +25,8 @@ { "name": "takeins", "history": 1 }, { "name": "verdicts", "history": 5 }, { "name": "oldsections", "history": 1 }, - { "name": "packages", "history": 5 } + { "name": "packages", "history": 5 }, + { "name": "graph", "history": 5 } ], "emits": [ "manifest-refused", From a901b840a0aa8f7cd304ca0a3c31f8098d8b403d Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 16:29:34 +0100 Subject: [PATCH 20/49] =?UTF-8?q?Pin=20mesh-sdk=20acad60547c84,=20and=20th?= =?UTF-8?q?e=20indexer's=20graph=20nodes=20take=20their=20bases=20and=20pr?= =?UTF-8?q?ovision=20edges=20from=20the=20contract's=20GraphEdgesOf=20(ADR?= =?UTF-8?q?=200603=20=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/mesh-delivery-assigner/go.mod | 2 +- modules/mesh-delivery-assigner/go.sum | 4 ++-- modules/mesh-delivery-builder/buildrun/go.mod | 2 +- modules/mesh-delivery-builder/buildrun/go.sum | 4 ++-- modules/mesh-delivery-builder/go.mod | 2 +- modules/mesh-delivery-builder/go.sum | 4 ++-- modules/mesh-delivery-checker/go.mod | 2 +- modules/mesh-delivery-checker/go.sum | 4 ++-- modules/mesh-delivery-forge-bridge/go.mod | 2 +- modules/mesh-delivery-forge-bridge/go.sum | 4 ++-- .../mesh-delivery-indexer/internal/index/graph.go | 15 +++------------ .../internal/index/toolchains.go | 2 +- modules/mesh-delivery-teller/go.mod | 2 +- modules/mesh-delivery-teller/go.sum | 4 ++-- modules/mesh-delivery-view/go.mod | 2 +- modules/mesh-delivery-view/go.sum | 4 ++-- modules/mesh-delivery-watchman/go.mod | 2 +- modules/mesh-delivery-watchman/go.sum | 4 ++-- modules/mesh-enrolment/go.mod | 2 +- modules/mesh-enrolment/go.sum | 4 ++-- 20 files changed, 31 insertions(+), 40 deletions(-) diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index 898173e7..c8e94054 100644 --- a/modules/mesh-delivery-assigner/go.mod +++ b/modules/mesh-delivery-assigner/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-assigner go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-assigner/go.sum b/modules/mesh-delivery-assigner/go.sum index cf2e5165..b648acf6 100644 --- a/modules/mesh-delivery-assigner/go.sum +++ b/modules/mesh-delivery-assigner/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 h1:DqBOfy5j5+g8168XK+ibmZugqVehD8F+PDtQxDTpGEE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 h1:P3xwzhRDMSvyjVQmJcPc6PozA/5UZkchlPpjmlwek8Q= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/buildrun/go.mod b/modules/mesh-delivery-builder/buildrun/go.mod index d4e86ab4..c8c67e14 100644 --- a/modules/mesh-delivery-builder/buildrun/go.mod +++ b/modules/mesh-delivery-builder/buildrun/go.mod @@ -3,7 +3,7 @@ module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/buildrun/go.sum b/modules/mesh-delivery-builder/buildrun/go.sum index cf2e5165..b648acf6 100644 --- a/modules/mesh-delivery-builder/buildrun/go.sum +++ b/modules/mesh-delivery-builder/buildrun/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 h1:DqBOfy5j5+g8168XK+ibmZugqVehD8F+PDtQxDTpGEE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 h1:P3xwzhRDMSvyjVQmJcPc6PozA/5UZkchlPpjmlwek8Q= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/go.mod b/modules/mesh-delivery-builder/go.mod index ea4bcfc0..feeb0d5b 100644 --- a/modules/mesh-delivery-builder/go.mod +++ b/modules/mesh-delivery-builder/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-builder go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/go.sum b/modules/mesh-delivery-builder/go.sum index da4e4d5e..1639e118 100644 --- a/modules/mesh-delivery-builder/go.sum +++ b/modules/mesh-delivery-builder/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 h1:DqBOfy5j5+g8168XK+ibmZugqVehD8F+PDtQxDTpGEE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 h1:P3xwzhRDMSvyjVQmJcPc6PozA/5UZkchlPpjmlwek8Q= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index fca9f123..faab4cbe 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-checker go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-checker/go.sum b/modules/mesh-delivery-checker/go.sum index da4e4d5e..1639e118 100644 --- a/modules/mesh-delivery-checker/go.sum +++ b/modules/mesh-delivery-checker/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 h1:DqBOfy5j5+g8168XK+ibmZugqVehD8F+PDtQxDTpGEE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 h1:P3xwzhRDMSvyjVQmJcPc6PozA/5UZkchlPpjmlwek8Q= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-forge-bridge/go.mod b/modules/mesh-delivery-forge-bridge/go.mod index 6aabb07e..abe178c0 100644 --- a/modules/mesh-delivery-forge-bridge/go.mod +++ b/modules/mesh-delivery-forge-bridge/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-forge-bridge go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-forge-bridge/go.sum b/modules/mesh-delivery-forge-bridge/go.sum index cf2e5165..b648acf6 100644 --- a/modules/mesh-delivery-forge-bridge/go.sum +++ b/modules/mesh-delivery-forge-bridge/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 h1:DqBOfy5j5+g8168XK+ibmZugqVehD8F+PDtQxDTpGEE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 h1:P3xwzhRDMSvyjVQmJcPc6PozA/5UZkchlPpjmlwek8Q= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-indexer/internal/index/graph.go b/modules/mesh-delivery-indexer/internal/index/graph.go index efcfad44..deaac724 100644 --- a/modules/mesh-delivery-indexer/internal/index/graph.go +++ b/modules/mesh-delivery-indexer/internal/index/graph.go @@ -26,7 +26,6 @@ type graphEdges struct { module string cur delivery.Module manifest catalogue.Manifest - bases []string fp, none string tcs map[string]delivery.ToolchainUsed pkgs delivery.PackageVersions @@ -38,8 +37,9 @@ func graphNode(e graphEdges, providers func(string) []catalogue.Manifest) delive if e.fp == "" && e.none == "" { e.none = "its fingerprint was not computed" } - n := delivery.GraphNode{FP: e.fp, None: e.none, Repository: e.cur.Repository, Path: e.cur.Path, Bases: e.bases, - Packages: slices.Sorted(maps.Keys(e.pkgs))} + n := delivery.GraphEdgesOf(e.manifest, providers) + n.FP, n.None, n.Repository, n.Path = e.fp, e.none, e.cur.Repository, e.cur.Path + n.Packages = slices.Sorted(maps.Keys(e.pkgs)) tcs := e.tcs if len(tcs) == 0 { tcs = e.cur.Toolchains // no fingerprint now: the toolchain it was last computed with still builds it @@ -54,15 +54,6 @@ func graphNode(e graphEdges, providers func(string) []catalogue.Manifest) delive } n.Toolchains[language] = delivery.GraphToolchain{Module: builder, Version: t.Version, Recipe: t.Recipe} } - for _, o := range e.manifest.Provides { - n.Provides = append(n.Provides, o.Name) - } - for _, r := range e.manifest.Requires { - if !e.manifest.OptionalRequires[r] { - n.Requires = append(n.Requires, r) - } - } - n.Awaits = catalogue.Awaited(e.manifest, providers) return n } diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains.go b/modules/mesh-delivery-indexer/internal/index/toolchains.go index 78d0dc83..2b13d2f7 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains.go @@ -283,7 +283,7 @@ func (ix *Indexer) recomputeModules(ctx context.Context, overlay map[string]deli for _, key := range slices.Sorted(maps.Keys(all)) { k := all[key] r := of(key, nil) - edges[key] = graphEdges{module: key, cur: k.cur, manifest: k.manifest, bases: k.bases, fp: r.fp, none: r.none, tcs: r.tcs, pkgs: r.pkgs} + edges[key] = graphEdges{module: key, cur: k.cur, manifest: k.manifest, fp: r.fp, none: r.none, tcs: r.tcs, pkgs: r.pkgs} if _, merged := overlay[key]; merged { final := overlay[key] final.FP2, final.None2, final.Inputs2, final.Toolchains, final.Packages = r.fp, r.none, k.cur.Inputs2, r.tcs, r.pkgs diff --git a/modules/mesh-delivery-teller/go.mod b/modules/mesh-delivery-teller/go.mod index 3e4716d6..f1eb10ab 100644 --- a/modules/mesh-delivery-teller/go.mod +++ b/modules/mesh-delivery-teller/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-teller go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-teller/go.sum b/modules/mesh-delivery-teller/go.sum index cf2e5165..b648acf6 100644 --- a/modules/mesh-delivery-teller/go.sum +++ b/modules/mesh-delivery-teller/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 h1:DqBOfy5j5+g8168XK+ibmZugqVehD8F+PDtQxDTpGEE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 h1:P3xwzhRDMSvyjVQmJcPc6PozA/5UZkchlPpjmlwek8Q= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index 6d129e0e..9d769386 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-view go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index cf2e5165..b648acf6 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 h1:DqBOfy5j5+g8168XK+ibmZugqVehD8F+PDtQxDTpGEE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 h1:P3xwzhRDMSvyjVQmJcPc6PozA/5UZkchlPpjmlwek8Q= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-watchman/go.mod b/modules/mesh-delivery-watchman/go.mod index 6ff48a5f..e4b6be67 100644 --- a/modules/mesh-delivery-watchman/go.mod +++ b/modules/mesh-delivery-watchman/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-watchman go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-watchman/go.sum b/modules/mesh-delivery-watchman/go.sum index acc8e351..584c4fe7 100644 --- a/modules/mesh-delivery-watchman/go.sum +++ b/modules/mesh-delivery-watchman/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 h1:DqBOfy5j5+g8168XK+ibmZugqVehD8F+PDtQxDTpGEE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 h1:P3xwzhRDMSvyjVQmJcPc6PozA/5UZkchlPpjmlwek8Q= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-enrolment/go.mod b/modules/mesh-enrolment/go.mod index 945d2ac2..2665b566 100644 --- a/modules/mesh-enrolment/go.mod +++ b/modules/mesh-enrolment/go.mod @@ -3,7 +3,7 @@ module mesh-enrolment go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-enrolment/go.sum b/modules/mesh-enrolment/go.sum index cf2e5165..b648acf6 100644 --- a/modules/mesh-enrolment/go.sum +++ b/modules/mesh-enrolment/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397 h1:DqBOfy5j5+g8168XK+ibmZugqVehD8F+PDtQxDTpGEE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152407-25b371bc8397/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 h1:P3xwzhRDMSvyjVQmJcPc6PozA/5UZkchlPpjmlwek8Q= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= From 7ba2668aa8620a3884d023ded92fd3ef1c0e660e Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 16:31:13 +0100 Subject: [PATCH 21/49] =?UTF-8?q?The=20check=20refuses=20a=20change=20whos?= =?UTF-8?q?e=20module=20graph=20has=20a=20cycle=20and=20names=20it,=20work?= =?UTF-8?q?ed=20out=20from=20the=20catalogue=20as=20the=20change=20leaves?= =?UTF-8?q?=20it;=20a=20cycle=20the=20trunk=20already=20has=20among=20the?= =?UTF-8?q?=20same=20modules=20is=20said,=20not=20the=20change's=20failure?= =?UTF-8?q?=20(ADR=200603=20=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmd/mesh-delivery-checker/judge.go | 39 +++++++++- .../internal/check/examiner_test.go | 28 ++++++++ .../internal/check/graph_cycle.go | 72 +++++++++++++++++++ .../internal/check/judge.go | 5 +- 4 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 modules/mesh-delivery-checker/internal/check/graph_cycle.go diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go index c1fc816e..532f5fff 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go @@ -12,6 +12,7 @@ import ( "github.com/nats-io/nats.go/jetstream" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" + "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" @@ -97,7 +98,7 @@ func newJudge(d judgeDeps) check.Judge { nodes = d.nodes } return &check.Examiner{Nodes: nodes, Compositions: d.compositions, Asks: d.asks, Toolchains: d.tools, - Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Predefined: d.predefined, Logf: d.logf} + Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Predefined: d.predefined, Logf: d.logf} } // manifestKV reads a manifest by its digest. @@ -122,6 +123,42 @@ type repoModules struct { repoOf map[string]string // by module: its repository commit map[string]string // by module: the commit its record was computed at (the commit the mesh runs) typed map[string]string // by module: its manifest digest + parsed map[string]catalogue.Manifest // by manifest digest: the manifest as read, once (a digest's manifest never changes) +} + +// Manifest is the kept manifest of a digest, read from the indexer's manifests state once and then from memory. +func (r *repoModules) Manifest(ctx context.Context, digest string) (catalogue.Manifest, error) { + r.mu.Lock() + m, known := r.parsed[digest] + r.mu.Unlock() + if known { + return m, nil + } + key, err := delivery.ManifestsKey(digest) + if err != nil { + return catalogue.Manifest{}, err + } + raw, _, found, err := r.manifests.Get(ctx, key) + if err != nil { + return catalogue.Manifest{}, err + } + if !found { + return catalogue.Manifest{}, fmt.Errorf("the indexer keeps no manifest %s", digest) + } + kept, err := delivery.DecodeManifest(raw) + if err != nil { + return catalogue.Manifest{}, err + } + if m, err = parseManifest(kept.JSON); err != nil { + return catalogue.Manifest{}, err + } + r.mu.Lock() + if r.parsed == nil { + r.parsed = map[string]catalogue.Manifest{} + } + r.parsed[digest] = m + r.mu.Unlock() + return m, nil } // Put takes a change of the indexer's modules bucket in: a module's repository and the languages its manifest names. diff --git a/modules/mesh-delivery-checker/internal/check/examiner_test.go b/modules/mesh-delivery-checker/internal/check/examiner_test.go index 6e5b4734..7da5b258 100644 --- a/modules/mesh-delivery-checker/internal/check/examiner_test.go +++ b/modules/mesh-delivery-checker/internal/check/examiner_test.go @@ -20,6 +20,7 @@ import ( gobuild "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" + "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/composer" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -1000,6 +1001,17 @@ func (k keptList) PredefinedList(context.Context) (json.RawMessage, error) { return json.RawMessage(k), nil } +// fixedManifests are the indexer's kept manifests by digest as a test gives them. +type fixedManifests map[string]catalogue.Manifest + +func (f fixedManifests) Manifest(_ context.Context, digest string) (catalogue.Manifest, error) { + m, ok := f[digest] + if !ok { + return catalogue.Manifest{}, errors.New("no manifest " + digest) + } + return m, nil +} + // fixedDigests are the indexer's manifest digests as a test gives them. type fixedDigests map[string]string @@ -1017,17 +1029,33 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { // untouched is the judge's refusal of mail, which the change leaves as it is, and trunkMail its refusal on the trunk: // a problem the trunk had is a warning, one the change causes fails (ADR 0579 §2) untouched, trunkMail string + // cycle reads the manifests, so the change's module graph is worked out (ADR 0603 §2): the change's web awaits + // mail's site while mail awaits web's database; trunkCycle has the trunk's web await it too. + cycle, trunkCycle bool }{ {name: "accepted"}, {name: "an untouched manifest's problem the trunk had", untouched: "mail says no credential form", trunkMail: "mail says no credential form"}, {name: "a problem the change causes in an untouched manifest", untouched: "mail reads a state web no longer keeps", fails: "refuses the manifest of mail: mail reads a state web no longer keeps"}, + {name: "a cycle the change makes", cycle: true, fails: "the change's module graph has the cycle mail -> web -> mail"}, + {name: "a cycle the trunk has", cycle: true, trunkCycle: true}, {name: "refused", refused: "web requires \"amqp\"\nweb has no id", fails: "the indexer's judge at mesh-delivery's trunk " + strings.Repeat("d", 40) + ` refuses the manifest of web: web requires "amqp"; web has no id`}, } { b := newBench(t) b.ex.Manifests = fixedDigests{"mail": strings.Repeat("a", 64), "web": strings.Repeat("b", 64)} b.ex.Trunks = fixedTrunks{heads: map[string]string{DeliveryRepository: trunk}} b.ex.Predefined = keptList(`{"font-size": {"type": "number", "description": "the size"}}`) + if row.cycle { + offer := func(name string) []catalogue.Offer { + return []catalogue.Offer{{Name: name, Scope: catalogue.ScopeMesh, AnsweredBy: catalogue.AnsweredByHelper}} + } + trunkWeb := catalogue.Manifest{Module: "web", Provides: offer("database")} + if row.trunkCycle { + trunkWeb.Requires = []string{"site"} + } + b.ex.Read = fixedManifests{strings.Repeat("a", 64): {Module: "mail", Provides: offer("site"), Requires: []string{"database"}}, + strings.Repeat("b", 64): trunkWeb, web: {Module: "web", Provides: offer("database"), Requires: []string{"site"}}} + } var judged []delivery.ComposeJob answer := b.asks.Answer b.asks.Answer = func(a delivery.StepAsk) delivery.Step { diff --git a/modules/mesh-delivery-checker/internal/check/graph_cycle.go b/modules/mesh-delivery-checker/internal/check/graph_cycle.go new file mode 100644 index 00000000..0490f9a9 --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/graph_cycle.go @@ -0,0 +1,72 @@ +package check + +import ( + "context" + "fmt" + "maps" + "slices" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/composer" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// The check refuses a change whose module graph has a cycle and names it (novox/hq ADR 0603 §2): modules that would each +// wait for the other for good, through their bases or the provisions they await. The graph is worked out from the +// catalogue as the change leaves it (every trunk manifest, each one a pull request touches replaced by its candidate's), +// with the contract's ManifestGraph, the way the indexer works out its graph's edges. A cycle the trunk already has among +// the same modules is the trunk's: said in the verdict's notes, not the change's failure. + +// ManifestReader reads a kept manifest by its digest (the indexer's manifests state). +type ManifestReader interface { + Manifest(ctx context.Context, digest string) (catalogue.Manifest, error) +} + +// refuseCycle fails the check for a cycle of the change's module graph, naming it; nil when the case touches no manifest, +// when nothing reads manifests, or when there is none. A manifest that cannot be read fails the check, said, never passes it. +func (st *state) refuseCycle(ctx context.Context) *delivery.Verdict { + if st.j.Manifests == nil || st.j.Read == nil || st.c.Try != nil { + return nil + } + trunk := st.j.Manifests.ManifestDigests() + req, touched := judgeRequest(trunk, st.c.Pulls) + if len(touched) == 0 { + return nil + } + read := func(digests map[string]composer.Manifest) (map[string]catalogue.Manifest, error) { + out := map[string]catalogue.Manifest{} + for _, module := range slices.Sorted(maps.Keys(digests)) { + m, err := st.j.Read.Manifest(ctx, digests[module].Digest) + if err != nil { + return nil, fmt.Errorf("the manifest %s of %s: %w", digests[module].Digest, module, err) + } + out[module] = m + } + return out, nil + } + change, err := read(req.Cases[0].Manifests) + if err != nil { + return fail(goModule, "the change's module graph cannot be worked out, so no cycle can be ruled out: "+err.Error()) + } + cycle := delivery.ManifestGraph(change).Cycle() + if cycle == nil { + st.c.note("the change's module graph has no cycle") + return nil + } + words := strings.Join(cycle, " -> ") + if before, err := read(req.Cases[1].Manifests); err == nil { + among := delivery.Graph{} + for name, n := range delivery.ManifestGraph(before) { + if slices.Contains(cycle, name) { + among[name] = n + } + } + if among.Cycle() != nil { + st.c.note("the module graph has the cycle " + words + ", which the trunk already has among the same modules (ADR 0603 §2)") + return nil + } + } + return fail(goModule, fmt.Sprintf("the change's module graph has the cycle %s: these modules would each wait for the other for "+ + "good, through their bases or the provisions they await (ADR 0603 §2)", words)) +} diff --git a/modules/mesh-delivery-checker/internal/check/judge.go b/modules/mesh-delivery-checker/internal/check/judge.go index b561a1fb..9d7a2283 100644 --- a/modules/mesh-delivery-checker/internal/check/judge.go +++ b/modules/mesh-delivery-checker/internal/check/judge.go @@ -114,6 +114,9 @@ type Examiner struct { // Manifests are the indexer's manifest digests by module: the catalogue a change's manifests are judged in by the // indexer's judge (manifests_judge.go, novox/hq ADR 0578 §2); nil judges no manifest. Manifests TrunkManifests + // Read reads a kept manifest by its digest: the change's module graph is worked out from them (graph_cycle.go, novox/hq + // ADR 0603 §2); nil works out no graph. + Read ManifestReader // Predefined reads the catalogue's predefined keys the indexer keeps (manifests_judge.go); nil reads none. Predefined PredefinedList // Compositions makes a check's composition (ADR 0593 §7): the engines' answers (EngineCompositions); nil leaves the @@ -242,7 +245,7 @@ func (j *Examiner) Judge(ctx context.Context, c *Case) (Outcome, error) { // Every member and level runs to its end, and every manifest the change touches is judged by the indexer's judge (ADR // 0578 §2), also after a failure: the verdict names every failure, not only the first (the operator, 2026-11-02). tests := st.judgeAll(ctx) - if v := settled(joinFailures(tests, st.judgeManifests(ctx))); v != nil { + if v := settled(joinFailures(tests, st.judgeManifests(ctx), st.refuseCycle(ctx))); v != nil { return st.finish(ctx, *v), nil } return st.finish(ctx, delivery.Verdict{State: delivery.VerdictPass}), nil From 027c445bae1c5280bb582480f21dfa4f1d48c5c6 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 16:36:14 +0100 Subject: [PATCH 22/49] =?UTF-8?q?Pin=20mesh-sdk=20182d8757dba9,=20the=20gr?= =?UTF-8?q?oup's=20head=20of=20feat/l3b-bus=20with=20the=20graph's=20order?= =?UTF-8?q?ing=20edges=20(ADR=200603=20=C2=A71,=20=C2=A76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/mesh-delivery-assigner/go.mod | 2 +- modules/mesh-delivery-assigner/go.sum | 4 ++-- modules/mesh-delivery-builder/buildrun/go.mod | 2 +- modules/mesh-delivery-builder/buildrun/go.sum | 4 ++-- modules/mesh-delivery-builder/go.mod | 2 +- modules/mesh-delivery-builder/go.sum | 4 ++-- modules/mesh-delivery-checker/go.mod | 2 +- modules/mesh-delivery-checker/go.sum | 4 ++-- modules/mesh-delivery-forge-bridge/go.mod | 2 +- modules/mesh-delivery-forge-bridge/go.sum | 4 ++-- modules/mesh-delivery-teller/go.mod | 2 +- modules/mesh-delivery-teller/go.sum | 4 ++-- modules/mesh-delivery-view/go.mod | 2 +- modules/mesh-delivery-view/go.sum | 4 ++-- modules/mesh-delivery-watchman/go.mod | 2 +- modules/mesh-delivery-watchman/go.sum | 4 ++-- modules/mesh-enrolment/go.mod | 2 +- modules/mesh-enrolment/go.sum | 4 ++-- 18 files changed, 27 insertions(+), 27 deletions(-) diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index c8e94054..63867cb5 100644 --- a/modules/mesh-delivery-assigner/go.mod +++ b/modules/mesh-delivery-assigner/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-assigner go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-assigner/go.sum b/modules/mesh-delivery-assigner/go.sum index b648acf6..51006e69 100644 --- a/modules/mesh-delivery-assigner/go.sum +++ b/modules/mesh-delivery-assigner/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 h1:P3xwzhRDMSvyjVQmJcPc6PozA/5UZkchlPpjmlwek8Q= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 h1:BdYMFwpcDLBj7xkNYvmJESgJqxG0P/ozPs1eQHZE43k= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/buildrun/go.mod b/modules/mesh-delivery-builder/buildrun/go.mod index c8c67e14..67030533 100644 --- a/modules/mesh-delivery-builder/buildrun/go.mod +++ b/modules/mesh-delivery-builder/buildrun/go.mod @@ -3,7 +3,7 @@ module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/buildrun/go.sum b/modules/mesh-delivery-builder/buildrun/go.sum index b648acf6..51006e69 100644 --- a/modules/mesh-delivery-builder/buildrun/go.sum +++ b/modules/mesh-delivery-builder/buildrun/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 h1:P3xwzhRDMSvyjVQmJcPc6PozA/5UZkchlPpjmlwek8Q= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 h1:BdYMFwpcDLBj7xkNYvmJESgJqxG0P/ozPs1eQHZE43k= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/go.mod b/modules/mesh-delivery-builder/go.mod index feeb0d5b..7dd28acb 100644 --- a/modules/mesh-delivery-builder/go.mod +++ b/modules/mesh-delivery-builder/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-builder go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/go.sum b/modules/mesh-delivery-builder/go.sum index 1639e118..7c582d73 100644 --- a/modules/mesh-delivery-builder/go.sum +++ b/modules/mesh-delivery-builder/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 h1:P3xwzhRDMSvyjVQmJcPc6PozA/5UZkchlPpjmlwek8Q= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 h1:BdYMFwpcDLBj7xkNYvmJESgJqxG0P/ozPs1eQHZE43k= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index faab4cbe..c1d5f529 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-checker go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-checker/go.sum b/modules/mesh-delivery-checker/go.sum index 1639e118..7c582d73 100644 --- a/modules/mesh-delivery-checker/go.sum +++ b/modules/mesh-delivery-checker/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 h1:P3xwzhRDMSvyjVQmJcPc6PozA/5UZkchlPpjmlwek8Q= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 h1:BdYMFwpcDLBj7xkNYvmJESgJqxG0P/ozPs1eQHZE43k= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-forge-bridge/go.mod b/modules/mesh-delivery-forge-bridge/go.mod index abe178c0..4188e76d 100644 --- a/modules/mesh-delivery-forge-bridge/go.mod +++ b/modules/mesh-delivery-forge-bridge/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-forge-bridge go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-forge-bridge/go.sum b/modules/mesh-delivery-forge-bridge/go.sum index b648acf6..51006e69 100644 --- a/modules/mesh-delivery-forge-bridge/go.sum +++ b/modules/mesh-delivery-forge-bridge/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 h1:P3xwzhRDMSvyjVQmJcPc6PozA/5UZkchlPpjmlwek8Q= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 h1:BdYMFwpcDLBj7xkNYvmJESgJqxG0P/ozPs1eQHZE43k= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-teller/go.mod b/modules/mesh-delivery-teller/go.mod index f1eb10ab..c8fbba19 100644 --- a/modules/mesh-delivery-teller/go.mod +++ b/modules/mesh-delivery-teller/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-teller go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-teller/go.sum b/modules/mesh-delivery-teller/go.sum index b648acf6..51006e69 100644 --- a/modules/mesh-delivery-teller/go.sum +++ b/modules/mesh-delivery-teller/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 h1:P3xwzhRDMSvyjVQmJcPc6PozA/5UZkchlPpjmlwek8Q= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 h1:BdYMFwpcDLBj7xkNYvmJESgJqxG0P/ozPs1eQHZE43k= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index 9d769386..d4a65d31 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-view go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index b648acf6..51006e69 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 h1:P3xwzhRDMSvyjVQmJcPc6PozA/5UZkchlPpjmlwek8Q= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 h1:BdYMFwpcDLBj7xkNYvmJESgJqxG0P/ozPs1eQHZE43k= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-watchman/go.mod b/modules/mesh-delivery-watchman/go.mod index e4b6be67..a3f976e9 100644 --- a/modules/mesh-delivery-watchman/go.mod +++ b/modules/mesh-delivery-watchman/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-watchman go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-watchman/go.sum b/modules/mesh-delivery-watchman/go.sum index 584c4fe7..5d2a70e4 100644 --- a/modules/mesh-delivery-watchman/go.sum +++ b/modules/mesh-delivery-watchman/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 h1:P3xwzhRDMSvyjVQmJcPc6PozA/5UZkchlPpjmlwek8Q= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 h1:BdYMFwpcDLBj7xkNYvmJESgJqxG0P/ozPs1eQHZE43k= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-enrolment/go.mod b/modules/mesh-enrolment/go.mod index 2665b566..ba7f8092 100644 --- a/modules/mesh-enrolment/go.mod +++ b/modules/mesh-enrolment/go.mod @@ -3,7 +3,7 @@ module mesh-enrolment go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-enrolment/go.sum b/modules/mesh-enrolment/go.sum index b648acf6..51006e69 100644 --- a/modules/mesh-enrolment/go.sum +++ b/modules/mesh-enrolment/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84 h1:P3xwzhRDMSvyjVQmJcPc6PozA/5UZkchlPpjmlwek8Q= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103152903-acad60547c84/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 h1:BdYMFwpcDLBj7xkNYvmJESgJqxG0P/ozPs1eQHZE43k= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= From 4f6e1021c60ff8a8f461322c2c1f323a86e28830 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 16:36:15 +0100 Subject: [PATCH 23/49] =?UTF-8?q?view=5Ftiers:=20a=20source=20revision's?= =?UTF-8?q?=20tiers=20worked=20out=20from=20the=20module=20graph=20at=20ea?= =?UTF-8?q?ch=20call,=20and=20per=20module=20what=20it=20waits=20on=20now,?= =?UTF-8?q?=20its=20build,=20each=20node's=20want=20and=20have=20and=20eac?= =?UTF-8?q?h=20provision=20it=20awaits,=20the=20nodes=20offline=20named=20?= =?UTF-8?q?away=20(ADR=200603=20=C2=A76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmd/mesh-delivery-view-tools/main.go | 15 +- .../cmd/mesh-delivery-view-tools/main_test.go | 1 + .../tiers_rows_test.go | 120 ++++++ .../mesh-delivery-view/internal/view/read.go | 3 + .../mesh-delivery-view/internal/view/tiers.go | 383 ++++++++++++++++++ modules/mesh-delivery-view/manifest_test.go | 4 +- modules/mesh-delivery-view/module.json | 5 +- 7 files changed, 527 insertions(+), 4 deletions(-) create mode 100644 modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go create mode 100644 modules/mesh-delivery-view/internal/view/tiers.go diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go index d795395b..69e67c73 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go @@ -13,6 +13,7 @@ import ( "mesh-delivery-view/internal/view" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/provisions" stdio "git.novox.be/novox/mesh-sdk/go" ) @@ -43,6 +44,16 @@ func Tools(r view.Reader, u view.UsageReader) []stdio.Tool { module, _ := args["module"].(string) return r.Reach(repository, module) }}, + {Name: view.TiersTool, + Description: "A source revision's tiers, worked out now and never stored (novox/hq ADR 0603 §6): the modules the revision moves " + + "(each whose fingerprint last changed at it), ordered by the module graph (a module after the bases and the providers " + + "of the provisions it awaits among them), and for each module what it waits on now and why: its build key, on each node " + + "that wants it its want (the assigner's rule and words when held back), its have, and each provision it awaits. Nodes " + + "the watchman says are offline are named away and left out. At most 500 modules, said when cut short; said when the " + + "module graph cannot be read. Give revision for an older one, none for the gate's newest. Read-only.", + Input: map[string]any{"type": "object", "properties": map[string]any{ + "revision": map[string]any{"type": "integer", "description": "the source revision; none for the gate's newest"}}}, + Run: func(args map[string]any) (any, error) { return r.TiersOf(args) }}, {Name: view.ModuleHistoryTool, Description: "The history of one module's value in the indexer's modules state (modules.), with times: each value it " + "still keeps (up to 20), newest first, with its revision, when it was written, put or del, the repository and commit " + @@ -307,7 +318,9 @@ func main() { PredefinedState: read(stdio.State(delivery.IndexerModule + "." + delivery.IndexerPredefinedState)), ConfigCandidatesState: read(stdio.State(delivery.IndexerModule + "." + delivery.IndexerConfigCandidatesState)), PackagesState: read(stdio.State(delivery.IndexerModule + "." + delivery.PackagesState)), - BuildLog: stdio.Log(delivery.BuildLogRead).Since} + GraphState: read(stdio.State(delivery.GraphRead)), SourceState: read(stdio.State(delivery.GateModule + ".source")), + ProvisionsState: read(stdio.State(provisions.ProvisionsRead)), + BuildLog: stdio.Log(delivery.BuildLogRead).Since} u := view.UsageReader{Haves: r.HavesState, Resolved: read(stdio.State("mesh-config.resolved")), Tries: read(stdio.State("mesh-delivery-checker.tries")), Steps: map[string]view.StateRead{}, Pauses: map[string]view.StateRead{}, Backlog: backlogOf(socket())} for _, lang := range delivery.AskLanguages { diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main_test.go index 4d54278c..144d9d30 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main_test.go @@ -55,6 +55,7 @@ const ( // 1562/AC1: indexer_module answers the key's value as stored: FP, FP2, None2, Inputs2, Path and Toolchains are all there. func TestModuleToolAnswersTheStoredValue(t *testing.T) { t.Run("indexer_packages answers the published modules", packagesToolAnswersThePublishedModules) + t.Run("view_tiers names each module's wait", tiersToolSaysEachModulesWait) r := fake{"a": withFP2, "b": withNone}.reader() for key, want := range map[string]string{"a": withFP2, "b": withNone} { out, err := run(t, r, view.ModuleReadTool, map[string]any{"module": key}) diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go new file mode 100644 index 00000000..815e1407 --- /dev/null +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go @@ -0,0 +1,120 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + "strings" + "testing" + "time" + + "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/provisions" + + "mesh-delivery-view/internal/view" +) + +// encoded is v as its contract's encoder writes it, failing the test when it refuses. +func encoded(t *testing.T, v []byte, err error) string { + t.Helper() + if err != nil { + t.Fatal(err) + } + return string(v) +} + +// tiersToolSaysEachModulesWait: view_tiers orders a source revision's modules by the module graph and names each one's +// wait (ADR 0603 §6): a consumer after its provider, held by the assigner's provision rule and its provision not provided; +// a module whose build waits for a package; a provider with no wait; a node offline named away; older modules left out. +func tiersToolSaysEachModulesWait(t *testing.T) { + fp := func(c string) string { return "src2:" + strings.Repeat(c, 64) } + manifest := strings.Repeat("d", 64) + mod := func(f string, src uint64) string { + b, err := delivery.EncodeModule(delivery.Module{FP2: f, Manifest: manifest, Src: src, FPSrc: src, Repository: "novox/mesh-catalog", Commit: "c"}) + return encoded(t, b, err) + } + node := func(g delivery.GraphNode) string { + b, err := delivery.EncodeGraphNode(g) + return encoded(t, b, err) + } + build := func(b delivery.BuilderBuild) string { + v, err := delivery.EncodeBuilderBuild(b) + return encoded(t, v, err) + } + want := func(w delivery.Want) string { + v, err := delivery.EncodeWant(w) + return encoded(t, v, err) + } + have := func(h delivery.Have) string { + v, err := delivery.EncodeHave(h) + return encoded(t, v, err) + } + key := func(k string, err error) string { + if err != nil { + t.Fatal(err) + } + return k + } + since := time.Date(2026, 11, 3, 16, 40, 0, 0, time.UTC) + r := fake{"db": mod(fp("a"), 9), "web": mod(fp("b"), 9), "lib": mod(fp("c"), 9), "old": mod(fp("e"), 5)}.reader() + r.GraphState = fake{ + "db": node(delivery.GraphNode{FP: fp("a"), Repository: "novox/mesh-catalog", Provides: []string{"database"}}), + "web": node(delivery.GraphNode{FP: fp("b"), Repository: "novox/mesh-catalog", Requires: []string{"database"}, Awaits: []string{"database"}}), + "lib": node(delivery.GraphNode{FP: fp("c"), Repository: "novox/mesh-catalog"}), + }.reader().State + r.Builds = fake{ + key(delivery.BuilderBuildKey("db", fp("a"))): build(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: manifest}), + key(delivery.BuilderBuildKey("web", fp("b"))): build(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: manifest}), + key(delivery.BuilderBuildKey("lib", fp("c"))): build(delivery.BuilderBuild{State: delivery.BuildQueued, Waits: "@novox/x@1.2.0"}), + }.reader().State + r.WantsState = fake{ + key(delivery.WantKey("ace", "db")): want(delivery.Want{FP: fp("a"), Self: true}), + key(delivery.WantKey("ace", "web")): want(delivery.Want{Self: true, FP: fp("f"), Newest: fp("b"), Rule: delivery.RuleProvisions, Words: "the provision ace.web.database of web on ace is asked, not provided"}), + key(delivery.WantKey("g14", "web")): want(delivery.Want{Self: true, FP: fp("f"), Newest: fp("b"), Rule: delivery.RuleCanary, Words: "the canary"}), + }.reader().State + r.WantsState.History = func(string) ([]view.Revision, error) { return []view.Revision{{At: since}}, nil } + r.HavesState = fake{key(delivery.HaveKey("ace", "db")): have(delivery.Have{FP: fp("a"), Phase: delivery.PhaseRunning, Health: delivery.HealthOK})}.reader().State + p, err := provisions.Encode(provisions.Provision{Provision: "database", Consumer: provisions.End{Module: "web", Node: "ace"}, + Provider: provisions.End{Module: "db", Node: "ace"}, State: provisions.Asked, Why: "the build db runs does not offer it", At: since}) + r.ProvisionsState = fake{key(provisions.Key("ace", "web", "database", "")): encoded(t, p, err)}.reader().State + off, err := delivery.EncodePresence(delivery.Presence{Online: false}) + r.PresenceState = fake{key(delivery.PresenceNodeKey("g14")): encoded(t, off, err)}.reader().State + + out, err := run(t, r, view.TiersTool, map[string]any{"revision": float64(9)}) + got, _ := out.(view.Tiers) + if err != nil || got.Revision != 9 || got.Modules != 3 || len(got.Tiers) != 2 || !reflect.DeepEqual(got.Away, []string{"g14"}) || got.Graph != "" { + t.Fatalf("view_tiers 9 = %+v, %v", out, err) + } + first := fmt.Sprint(got.Tiers[0]) + if len(got.Tiers[0]) != 2 || got.Tiers[0][0].Module != "db" || len(got.Tiers[0][0].Waits) != 0 || got.Tiers[0][1].Module != "lib" || + len(got.Tiers[0][1].Waits) != 1 || !strings.Contains(got.Tiers[0][1].Waits[0].Words, "waiting for the package @novox/x@1.2.0") { + t.Errorf("the first tier = %s, want db with no wait and lib's build waiting for its package", first) + } + web := got.Tiers[1][0] + if web.Module != "web" || !reflect.DeepEqual(web.After, []string{"db"}) || len(web.Waits) != 2 || + web.Waits[0].Edge != "want" || web.Waits[0].Node != "ace" || web.Waits[0].Rule != delivery.RuleProvisions || !web.Waits[0].Since.Equal(since) || + web.Waits[1].Edge != "provision database" || !strings.Contains(web.Waits[1].Words, "asked: the build db runs does not offer it") { + t.Errorf("web = %+v, want after db, held on ace by the provision rule and its provision asked (g14 is away)", web) + } + // No revision named: the gate's newest, its source key's revision. + r.SourceState = view.StateRead{GetEntry: func(string) (json.RawMessage, uint64, error) { return json.RawMessage(`{}`), 9, nil }} + if out, err := run(t, r, view.TiersTool, map[string]any{}); err != nil || out.(view.Tiers).Revision != 9 || out.(view.Tiers).Modules != 3 { + t.Errorf("view_tiers of the gate's newest = %+v, %v", out, err) + } + // The graph not readable: every module in the first tier, said; past 500 modules the answer says it is cut short. + r.GraphState = view.StateRead{Keys: func() ([]string, error) { return nil, errors.New("no bucket") }, Get: r.GraphState.Get} + if out, err := run(t, r, view.TiersTool, map[string]any{"revision": float64(9)}); err != nil || len(out.(view.Tiers).Tiers) != 1 || + !strings.Contains(out.(view.Tiers).Graph, "cannot be read") { + t.Errorf("view_tiers with no graph = %+v, %v", out, err) + } + many := fake{} + for i := 0; i < view.TiersMost+2; i++ { + many[fmt.Sprintf("m%03d", i)] = mod(fp("a"), 9) + } + r.State = many.reader().State + if out, err := run(t, r, view.TiersTool, map[string]any{"revision": "9"}); err != nil || out.(view.Tiers).Modules != view.TiersMost+2 || + !strings.Contains(out.(view.Tiers).Cut, "the first 500") { + t.Errorf("view_tiers past its bound = %+v, %v", out.(view.Tiers).Cut, err) + } +} diff --git a/modules/mesh-delivery-view/internal/view/read.go b/modules/mesh-delivery-view/internal/view/read.go index d9af163e..89d80210 100644 --- a/modules/mesh-delivery-view/internal/view/read.go +++ b/modules/mesh-delivery-view/internal/view/read.go @@ -55,6 +55,9 @@ type Reader struct { PredefinedState, ConfigCandidatesState StateRead // PackagesState is the indexer's published packages (delivery.PackagesBucket, ADR 0593 §2); unset reads none. PackagesState StateRead + // GraphState is the indexer's module graph (delivery.GraphBucket, ADR 0603 §2), SourceState the gate's source and + // ProvisionsState the provisioner's keys: what view_tiers reads (tiers.go). Unset reads none, said in the answer. + GraphState, SourceState, ProvisionsState StateRead // BuildLog reads the builder's log from a time (stdio.Log(delivery.BuildLogRead).Since); unset reads none. BuildLog LogSince } diff --git a/modules/mesh-delivery-view/internal/view/tiers.go b/modules/mesh-delivery-view/internal/view/tiers.go new file mode 100644 index 00000000..6289d96f --- /dev/null +++ b/modules/mesh-delivery-view/internal/view/tiers.go @@ -0,0 +1,383 @@ +package view + +import ( + "encoding/json" + "fmt" + "maps" + "slices" + "strconv" + "strings" + "time" + + "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/provisions" +) + +// The tiers of a source revision are a view, worked out at each call and never stored (novox/hq ADR 0603 §1, §6): the +// modules the revision moves, ordered by the module graph's ordering edges among them (a base, a provision awaited from +// another of them), and for each module what it waits on now: its build key, each node's want and have, and each +// provision it awaits on each node, with the rule and the words of the part that holds it. Nothing in the mesh waits on +// a tier; the tiers say where the revision stands. A node the watchman says is offline is named away and left out of +// what the revision waits on (ADR 0603 §7). + +// TiersTool is the view's read tool of a source revision's tiers. +const TiersTool = "view_tiers" + +// TiersMost is the most modules view_tiers answers (ADR 0603 §2); past it the answer says how many it left out. +const TiersMost = 500 + +// Tiers is a source revision's tiers: Revision the source revision (the gate's source key's revision), Modules how many +// modules it moves (each whose fingerprint last changed at it, delivery.Module.FPSrc), Tiers those modules in order, the +// first tier waiting on none of the others. Cut says when the answer holds fewer than Modules, Graph why the module graph +// could not be read (then every module is in the first tier), Cycle a cycle among them (its modules form the last +// tier), and Away the nodes left out. +type Tiers struct { + Revision uint64 `json:"revision"` + Modules int `json:"modules"` + Tiers [][]TierModule `json:"tiers"` + Cut string `json:"cut,omitempty"` + Graph string `json:"graph,omitempty"` + Cycle []string `json:"cycle,omitempty"` + Away []string `json:"away,omitempty"` +} + +// TierModule is one module of a tier: its fingerprint, the modules of the same revision it waits for (After), and each +// wait of its own now; no wait is a module that reached every node present. +type TierModule struct { + Module string `json:"module"` + FP string `json:"fp,omitempty"` + None string `json:"none,omitempty"` + After []string `json:"after,omitempty"` + Waits []TierWait `json:"waits,omitempty"` +} + +// TierWait is one thing a module waits on: the edge (build, want, have, or provision ), the node it waits on (none +// for its build), the assigner's rule for a want held back, the part's words and since when, where the part says it. +type TierWait struct { + Edge string `json:"edge"` + Node string `json:"node,omitempty"` + Rule int `json:"rule,omitempty"` + Words string `json:"words"` + Since time.Time `json:"since,omitzero"` +} + +// Tiers works out the tiers of a source revision, the gate's newest when revision is 0. +func (r Reader) Tiers(revision uint64) (Tiers, error) { + if revision == 0 { + if r.SourceState.GetEntry == nil { + return Tiers{}, fmt.Errorf("the gate's source cannot be read here: name the source revision") + } + _, rev, err := r.SourceState.GetEntry(delivery.SourceMainKey) + if err != nil { + return Tiers{}, fmt.Errorf("the gate's source: %w", err) + } + if rev == 0 { + return Tiers{}, fmt.Errorf("the gate holds no source revision yet") + } + revision = rev + } + modules, err := values(r.State) + if err != nil { + return Tiers{}, fmt.Errorf("the indexer's modules: %w", err) + } + moved := map[string]delivery.Module{} + for key, raw := range modules { + if m, err := delivery.DecodeModule(raw); err == nil && m.FPSrc == revision { + moved[key] = m + } + } + out := Tiers{Revision: revision, Modules: len(moved)} + names := slices.Sorted(maps.Keys(moved)) + if len(names) > TiersMost { + out.Cut = fmt.Sprintf("the revision moves %d modules: the first %d by name are shown, %s and the %d after it are not", + len(names), TiersMost, names[TiersMost], len(names)-TiersMost-1) + names = names[:TiersMost] + } + graph := delivery.Graph{} + if raws, err := values(r.GraphState); err != nil { + out.Graph = "the module graph cannot be read, so no module is ordered after another: " + err.Error() + } else { + for key, raw := range raws { + if n, err := delivery.DecodeGraphNode(raw); err == nil { + graph[key] = n + } + } + } + away := r.away() + out.Away = slices.Sorted(maps.Keys(away)) + wants, _ := values(r.WantsState) + provided, perr := values(r.ProvisionsState) + + // The ordering among the revision's modules: each one's tier is one past the highest of the modules it waits for. + in := map[string]bool{} + for _, n := range names { + in[n] = true + } + after := map[string][]string{} + for _, n := range names { + for _, b := range graph.Before(n) { + if in[b] { + after[n] = append(after[n], b) + } + } + } + sub := delivery.Graph{} + for _, n := range names { + if g, ok := graph[n]; ok { + sub[n] = g + } + } + out.Cycle = sub.Cycle() + tier := map[string]int{} + var level func(string, map[string]bool) int + level = func(n string, visiting map[string]bool) int { + if t, ok := tier[n]; ok { + return t + } + if visiting[n] { + return -1 + } + visiting[n] = true + t := 0 + for _, b := range after[n] { + lb := level(b, visiting) + if lb < 0 { + return -1 + } + t = max(t, lb+1) + } + delete(visiting, n) + tier[n] = t + return t + } + top := 0 + for _, n := range names { + if t := level(n, map[string]bool{}); t > top { + top = t + } + } + for _, n := range names { + if _, ok := tier[n]; !ok { + tier[n] = top + 1 // on a cycle: after every tier that is ordered + } + } + for _, n := range names { + t := tier[n] + for len(out.Tiers) <= t { + out.Tiers = append(out.Tiers, nil) + } + m := moved[n] + tm := TierModule{Module: n, FP: m.FP2, None: m.None2, After: after[n]} + tm.Waits = r.waitsOf(n, m, graph[n], wants, provided, perr, away) + out.Tiers[t] = append(out.Tiers[t], tm) + } + compact := out.Tiers[:0] + for _, t := range out.Tiers { + if len(t) > 0 { + compact = append(compact, t) + } + } + out.Tiers = compact + return out, nil +} + +// away are the nodes the watchman's presence says are offline. +func (r Reader) away() map[string]bool { + out := map[string]bool{} + presence, err := values(r.PresenceState) + if err != nil { + return out + } + for key, raw := range presence { + node, part, _, err := delivery.ParsePresenceKey(key) + if err != nil || part != "" { + continue + } + if p, err := delivery.DecodePresence(raw); err == nil && !p.Online { + out[node] = true + } + } + return out +} + +// waitsOf is what module m waits on now: its build at its fingerprint, then on each node present that wants it, its want, +// its have, and each provision its graph node awaits. +func (r Reader) waitsOf(module string, m delivery.Module, g delivery.GraphNode, wants, provided map[string]json.RawMessage, + perr error, away map[string]bool) []TierWait { + var out []TierWait + if m.FP2 == "" { + return []TierWait{{Edge: "build", Words: "it has no fingerprint, so nothing is built: " + m.None2}} + } + if w, ok := r.buildWait(module, m.FP2); ok { + out = append(out, w) + } + var nodes []string + for key := range wants { + node, mod, err := delivery.ParseWantKey(key) + if err == nil && mod == module { + nodes = append(nodes, node) + } + } + slices.Sort(nodes) + for _, node := range nodes { + if away[node] { + continue + } + w, err := delivery.DecodeWant(wants[mustWantKey(node, module)]) + if err != nil || w.Removed { + continue + } + if w.FP != m.FP2 { + words := w.Words + if words == "" { + words = "the want names " + short(w.FP) + ", not " + short(m.FP2) + } + out = append(out, TierWait{Edge: "want", Node: node, Rule: w.Rule, Words: words, Since: r.since(r.WantsState, mustWantKey(node, module))}) + } else if hw, ok := r.haveWait(node, module, m.FP2); ok { + out = append(out, hw) + } + for _, a := range g.Awaits { + out = append(out, provisionWaits(node, module, a, provided, perr)...) + } + } + return out +} + +func mustWantKey(node, module string) string { + k, _ := delivery.WantKey(node, module) + return k +} + +// buildWait is the wait of the build key ., none when it is done. +func (r Reader) buildWait(module, fp string) (TierWait, bool) { + key, err := delivery.BuilderBuildKey(module, fp) + if err != nil || r.Builds.Get == nil { + return TierWait{Edge: "build", Words: "the builder's builds cannot be read here"}, true + } + raw, err := r.Builds.Get(key) + switch { + case err != nil: + return TierWait{Edge: "build", Words: "the build key " + key + " cannot be read: " + err.Error()}, true + case raw == nil: + return TierWait{Edge: "build", Words: "the builder holds no build of " + short(fp) + " yet"}, true + } + b, err := delivery.DecodeBuilderBuild(raw) + if err != nil { + return TierWait{Edge: "build", Words: "the build key " + key + " does not read: " + err.Error()}, true + } + switch b.State { + case delivery.BuildDone: + return TierWait{}, false + case delivery.BuildFailed: + return TierWait{Edge: "build", Words: "the build of " + short(fp) + " failed: " + b.Error}, true + } + words := "the build of " + short(fp) + " is " + b.State + if b.Waits != "" { + words += ", waiting for the package " + b.Waits + } + if b.Step != "" { + words += ": " + b.Step + } + return TierWait{Edge: "build", Words: words, Since: r.since(r.Builds, key)}, true +} + +// haveWait is the wait of node's have of module at fp, none when it runs fp healthy. +func (r Reader) haveWait(node, module, fp string) (TierWait, bool) { + key, err := delivery.HaveKey(node, module) + if err != nil || r.HavesState.Get == nil { + return TierWait{}, false + } + raw, err := r.HavesState.Get(key) + if err != nil || raw == nil { + return TierWait{Edge: "have", Node: node, Words: node + " has not yet said what it runs of " + module}, true + } + h, err := delivery.DecodeHave(raw) + if err != nil { + return TierWait{Edge: "have", Node: node, Words: "its have does not read: " + err.Error()}, true + } + if h.FP == fp && h.Phase == delivery.PhaseRunning && h.Health == delivery.HealthOK { + return TierWait{}, false + } + words := fmt.Sprintf("%s runs %s, %s", node, short(h.FP), h.Phase) + if h.Health != "" { + words += ", health " + string(h.Health) + } + if h.Detail != "" { + words += ": " + h.Detail + } + return TierWait{Edge: "have", Node: node, Words: words, Since: r.since(r.HavesState, key)}, true +} + +// provisionWaits are the waits of the provision a that module awaits on node: none once every key of it is provided. +func provisionWaits(node, module, a string, provided map[string]json.RawMessage, perr error) []TierWait { + edge := "provision " + a + if perr != nil { + return []TierWait{{Edge: edge, Node: node, Words: "the provisioner's keys cannot be read: " + perr.Error()}} + } + prefix, _ := provisions.Key(node, module, a, "") + var out []TierWait + found := false + for _, key := range slices.Sorted(maps.Keys(provided)) { + if key != prefix && !strings.HasPrefix(key, prefix+".") { + continue + } + found = true + p, err := provisions.Decode(provided[key]) + if err != nil { + out = append(out, TierWait{Edge: edge, Node: node, Words: "the provision key " + key + " does not read: " + err.Error()}) + continue + } + if p.State == provisions.Provided { + continue + } + words := "the provision " + key + " is " + p.State + if p.Why != "" { + words += ": " + p.Why + } + out = append(out, TierWait{Edge: edge, Node: node, Words: words, Since: p.At}) + } + if !found { + out = append(out, TierWait{Edge: edge, Node: node, Words: "the provisioner holds no key of " + a + " for " + module + " on " + node + " yet"}) + } + return out +} + +// since is when key's value was written, from the state's history, or zero when it cannot say. +func (r Reader) since(s StateRead, key string) time.Time { + if s.History == nil { + return time.Time{} + } + revs, err := s.History(key) + if err != nil || len(revs) == 0 { + return time.Time{} + } + return revs[len(revs)-1].At +} + +// tiersRevision reads the tool's revision argument: a number, as JSON gives it or as text; 0 for the gate's newest. +func tiersRevision(v any) (uint64, error) { + switch x := v.(type) { + case nil: + return 0, nil + case float64: + if x < 0 || x != float64(uint64(x)) { + return 0, fmt.Errorf("the revision %v is no source revision", x) + } + return uint64(x), nil + case string: + if strings.TrimSpace(x) == "" { + return 0, nil + } + return strconv.ParseUint(strings.TrimSpace(x), 10, 64) + } + return 0, fmt.Errorf("the revision %v is no source revision", v) +} + +// TiersOf is view_tiers' answer for its arguments. +func (r Reader) TiersOf(args map[string]any) (any, error) { + revision, err := tiersRevision(args["revision"]) + if err != nil { + return nil, err + } + return r.Tiers(revision) +} diff --git a/modules/mesh-delivery-view/manifest_test.go b/modules/mesh-delivery-view/manifest_test.go index eb5d1e2c..4e65b88f 100644 --- a/modules/mesh-delivery-view/manifest_test.go +++ b/modules/mesh-delivery-view/manifest_test.go @@ -87,7 +87,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7 if m.Module != "mesh-delivery-view" { t.Errorf("module = %q", m.Module) } - want := []string{"mesh-build-go.pauses", "mesh-build-go.steps", "mesh-build-image.pauses", "mesh-build-image.steps", "mesh-build-node.pauses", "mesh-build-node.steps", "mesh-conditions.conditions", "mesh-config.resolved", "mesh-delivery-assigner.asks", "mesh-delivery-assigner.differences", "mesh-delivery-assigner.placement", "mesh-delivery-assigner.wants", "mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-checker.tries", "mesh-delivery-gate.asks", "mesh-delivery-gate.source", "mesh-delivery-indexer.candidates", "mesh-delivery-indexer.config-candidates", "mesh-delivery-indexer.heard", "mesh-delivery-indexer.modules", "mesh-delivery-indexer.packages", "mesh-delivery-indexer.predefined", "mesh-delivery-indexer.taken", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "mesh-healers.attempts", "mesh-vault.signers", "node-engine.have"} // view_status reads the keeper's conditions and the healers' attempts (1952/V1); build_usage reads the build modules' steps and pauses, mesh-config's capacities and the checker's tries; 1168/AC1, 1241/AC4, 1345/AC3, 1297/B8, B9 + want := []string{"mesh-build-go.pauses", "mesh-build-go.steps", "mesh-build-image.pauses", "mesh-build-image.steps", "mesh-build-node.pauses", "mesh-build-node.steps", "mesh-conditions.conditions", "mesh-config.resolved", "mesh-delivery-assigner.asks", "mesh-delivery-assigner.differences", "mesh-delivery-assigner.placement", "mesh-delivery-assigner.wants", "mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-checker.tries", "mesh-delivery-gate.asks", "mesh-delivery-gate.source", "mesh-delivery-indexer.candidates", "mesh-delivery-indexer.config-candidates", "mesh-delivery-indexer.graph", "mesh-delivery-indexer.heard", "mesh-delivery-indexer.modules", "mesh-delivery-indexer.packages", "mesh-delivery-indexer.predefined", "mesh-delivery-indexer.taken", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "mesh-healers.attempts", "mesh-provisioner.provisions", "mesh-vault.signers", "node-engine.have"} // view_tiers reads the module graph and the provisioner's keys (ADR 0603 §6); view_status reads the keeper's conditions and the healers' attempts (1952/V1); build_usage reads the build modules' steps and pauses, mesh-config's capacities and the checker's tries; 1168/AC1, 1241/AC4, 1345/AC3, 1297/B8, B9 got := slices.Clone(m.Reads) slices.Sort(got) if !reflect.DeepEqual(got, want) { @@ -119,7 +119,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7 } // The two read tools of the indexer's modules (issue 1562), carried by a bundle the runtime launches; the indexer // itself serves no tool (ADR 0437 §4). Carrying them gives the tool runner read access to every bucket the view reads. - if want := []string{"view_status", "indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "indexer_predefined", "indexer_config_candidates", "indexer_packages", "builder_build", "builder_builds", "builder_history", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard"}; !reflect.DeepEqual(m.Tools, want) { + if want := []string{"view_status", "indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "indexer_predefined", "indexer_config_candidates", "indexer_packages", "builder_build", "builder_builds", "builder_history", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard", "view_tiers"}; !reflect.DeepEqual(m.Tools, want) { t.Errorf("tools = %v, want %v", m.Tools, want) } for _, a := range m.Build.Artifacts { diff --git a/modules/mesh-delivery-view/module.json b/modules/mesh-delivery-view/module.json index 8ba20327..55bbbfb6 100644 --- a/modules/mesh-delivery-view/module.json +++ b/modules/mesh-delivery-view/module.json @@ -23,7 +23,8 @@ "build_usage", "view_reach", "indexer_module_history", - "indexer_heard" + "indexer_heard", + "view_tiers" ], "emits": [ "delivered", @@ -54,6 +55,7 @@ "mesh-delivery-gate.source", "mesh-delivery-indexer.candidates", "mesh-delivery-indexer.config-candidates", + "mesh-delivery-indexer.graph", "mesh-delivery-indexer.heard", "mesh-delivery-indexer.modules", "mesh-delivery-indexer.packages", @@ -62,6 +64,7 @@ "mesh-delivery-teller.answers", "mesh-conditions.conditions", "mesh-healers.attempts", + "mesh-provisioner.provisions", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "mesh-vault.signers", From 9f49bc92ad0077422f5e169f6a1a3a149970364d Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 16:39:23 +0100 Subject: [PATCH 24/49] =?UTF-8?q?The=20seat=20mesh-catalog's=20four=20verb?= =?UTF-8?q?s=20answered=20from=20the=20indexer's=20modules,=20manifests=20?= =?UTF-8?q?and=20graph,=20with=20the=20old=20catalog's=20field=20names=20(?= =?UTF-8?q?captured=20answers=20as=20fixtures);=20served=20once=20the=20in?= =?UTF-8?q?dexer=20claims=20the=20seat,=20in=20the=20release=20after=20L3'?= =?UTF-8?q?s=20(ADR=200604=20=C2=A71,=20=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mesh-delivery-indexer-tools/catalog.go | 101 +++++++++++ .../internal/index/catalog.go | 171 ++++++++++++++++++ .../internal/index/catalog_test.go | 125 +++++++++++++ .../internal/index/manifests_test.go | 1 + .../testdata/catalog/catalog_dependents.json | 4 + .../testdata/catalog/catalog_module.json | 18 ++ .../testdata/catalog/catalog_modules.json | 22 +++ .../testdata/catalog/catalog_provides.json | 15 ++ 8 files changed, 457 insertions(+) create mode 100644 modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/catalog.go create mode 100644 modules/mesh-delivery-indexer/internal/index/catalog.go create mode 100644 modules/mesh-delivery-indexer/internal/index/catalog_test.go create mode 100644 modules/mesh-delivery-indexer/internal/index/testdata/catalog/catalog_dependents.json create mode 100644 modules/mesh-delivery-indexer/internal/index/testdata/catalog/catalog_module.json create mode 100644 modules/mesh-delivery-indexer/internal/index/testdata/catalog/catalog_modules.json create mode 100644 modules/mesh-delivery-indexer/internal/index/testdata/catalog/catalog_provides.json diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/catalog.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/catalog.go new file mode 100644 index 00000000..24411bac --- /dev/null +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/catalog.go @@ -0,0 +1,101 @@ +package main + +import ( + "fmt" + + stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/delivery" + + "mesh-delivery-indexer/internal/index" +) + +// CatalogTools are the seat mesh-catalog's four verbs as tools named ., with the descriptions and inputs the +// seat's contract gives them (novox/hq ADR 0604 §1, §2), answered from the indexer's own states. They are served once the +// indexer's manifest claims the seat, in the release after L3's (the lead's decision): until then main serves them not. +func CatalogTools(c index.CatalogRead) []stdio.Tool { + seat, ok := catalogue.SeatNamed(catalogue.CatalogSeat) + if !ok { + return nil + } + str := func(args map[string]any, k string) string { s, _ := args[k].(string); return s } + runs := map[string]func(map[string]any) (any, error){ + "modules": func(map[string]any) (any, error) { return c.CatalogModulesAnswer() }, + "module": func(a map[string]any) (any, error) { return c.CatalogModuleAnswer(str(a, "module")) }, + "provides": func(a map[string]any) (any, error) { return c.CatalogProvidesAnswer(str(a, "provision")) }, + "dependents": func(a map[string]any) (any, error) { return c.CatalogDependentsAnswer(str(a, "module")) }, + } + var out []stdio.Tool + for _, v := range seat.Serves { + if run, ok := runs[v.Name]; ok { + out = append(out, stdio.Tool{Name: catalogue.CatalogSeat + "." + v.Name, Description: v.Description, Input: v.Input, Run: run}) + } + } + return out +} + +// catalogRead reads the indexer's modules, manifests and graph states for the seat's verbs. +func catalogRead(modules, manifests, graph stdio.KeptState) index.CatalogRead { + return index.CatalogRead{ + Modules: func() (map[string]delivery.Module, error) { + keys, err := modules.Keys() + if err != nil { + return nil, err + } + out := map[string]delivery.Module{} + for _, k := range keys { + e, err := modules.Get(k) + if err != nil { + return nil, fmt.Errorf("modules.%s: %w", k, err) + } + if e == nil { + continue + } + if m, err := delivery.DecodeModule(e.Value); err == nil { + out[k] = m + } + } + return out, nil + }, + Manifest: func(digest string) (string, error) { + key, err := delivery.ManifestsKey(digest) + if err != nil { + return "", err + } + e, err := manifests.Get(key) + if err != nil { + return "", err + } + if e == nil { + return "", fmt.Errorf("the manifests state holds no %s", digest) + } + kept, err := delivery.DecodeManifest(e.Value) + return kept.JSON, err + }, + Graph: func() (delivery.Graph, error) { + keys, err := graph.Keys() + if err != nil { + return nil, err + } + g := delivery.Graph{} + for _, k := range keys { + e, err := graph.Get(k) + if err != nil { + return nil, fmt.Errorf("graph.%s: %w", k, err) + } + if e == nil { + continue + } + if n, err := delivery.DecodeGraphNode(e.Value); err == nil { + g[k] = n + } + } + return g, nil + }, + } +} + +// catalogTools are the seat's verbs over the indexer's own states, as main serves them once the indexer claims the seat. +func catalogTools() []stdio.Tool { + return CatalogTools(catalogRead(stdio.State("modules"), stdio.State("manifests"), stdio.State(delivery.GraphState))) +} diff --git a/modules/mesh-delivery-indexer/internal/index/catalog.go b/modules/mesh-delivery-indexer/internal/index/catalog.go new file mode 100644 index 00000000..0d5c30ce --- /dev/null +++ b/modules/mesh-delivery-indexer/internal/index/catalog.go @@ -0,0 +1,171 @@ +package index + +import ( + "encoding/json" + "fmt" + "maps" + "slices" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// The seat mesh-catalog's verbs, answered from the indexer's own states (novox/hq ADR 0604 §1, §2): modules, module, +// provides and dependents read the modules state, the manifests state and the module graph, and write nothing. The +// indexer serves no request subject (ADR 0437 §4): its tools bundle serves them as .. Each answer keeps the +// field names of the old catalog module's tool it replaces (catalog_modules, catalog_module, catalog_provides, +// catalog_dependents), so a caller that moves to the seat reads the same fields. + +// CatalogRead reads the states the seat's verbs answer from. +type CatalogRead struct { + // Modules is every value of the modules state, by module. + Modules func() (map[string]delivery.Module, error) + // Manifest is the manifest kept under a digest in the manifests state, as its JSON. + Manifest func(digest string) (string, error) + // Graph is the module graph, by module. + Graph func() (delivery.Graph, error) +} + +// CatalogEntry is one module of the catalog: its repository, its directory there (delivery.RootPath for the root), the +// merge commit it was last changed at, and its 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. +type CatalogEnd struct { + Module string `json:"module"` + Commit string `json:"commit"` + Scope string `json:"scope,omitempty"` +} + +func entryOf(name string, m delivery.Module) CatalogEntry { + return CatalogEntry{Module: name, Repository: m.Repository, Path: m.Path, Commit: m.Commit, FP: m.FP2, None: m.None2} +} + +// CatalogModulesAnswer is the verb modules: every module the modules state holds, by name. +func (c CatalogRead) CatalogModulesAnswer() (map[string]any, error) { + mods, err := c.Modules() + if err != nil { + return nil, fmt.Errorf("the indexer's modules cannot be read: %w", err) + } + out := []CatalogEntry{} + for _, name := range slices.Sorted(maps.Keys(mods)) { + out = append(out, entryOf(name, mods[name])) + } + return map[string]any{"modules": out}, nil +} + +// CatalogModuleAnswer is the verb module: one module whole, its manifest included. +func (c CatalogRead) CatalogModuleAnswer(name string) (map[string]any, error) { + name = strings.TrimSpace(name) + if name == "" { + return nil, fmt.Errorf("name the module") + } + mods, err := c.Modules() + if err != nil { + return nil, fmt.Errorf("the indexer's modules cannot be read: %w", err) + } + m, ok := mods[name] + if !ok { + return nil, fmt.Errorf("the trunk holds no module %s", name) + } + one := CatalogModule{CatalogEntry: entryOf(name, m)} + if m.Manifest == "" { + return nil, fmt.Errorf("the indexer keeps no manifest of %s", name) + } + raw, err := c.Manifest(m.Manifest) + if err != nil { + return nil, fmt.Errorf("the manifest %s of %s cannot be read: %w", m.Manifest, name, err) + } + one.Manifest = json.RawMessage(raw) + return map[string]any{"module": one}, nil +} + +// CatalogProvidesAnswer is the verb provides: the modules that provide a provision, each with its scope (a module provides +// its own name, at node scope), and the modules that require it, from the module graph's provision edges. +func (c CatalogRead) CatalogProvidesAnswer(provision string) (map[string]any, error) { + provision = strings.TrimSpace(provision) + if provision == "" { + return nil, fmt.Errorf("name the provision") + } + g, mods, err := c.graphAndModules() + if err != nil { + return nil, err + } + provides, requires := []CatalogEnd{}, []CatalogEnd{} + for _, name := range g.Providers(provision) { + end := CatalogEnd{Module: name, Commit: mods[name].Commit, Scope: catalogue.ScopeNode} + if name != provision { + end.Scope = c.scopeOf(mods[name], provision) + } + provides = append(provides, end) + } + for _, name := range g.Requirers(provision) { + requires = append(requires, CatalogEnd{Module: name, Commit: mods[name].Commit}) + } + return map[string]any{"provision": provision, "provides": provides, "requires": requires}, nil +} + +// scopeOf is the scope the module's manifest offers provision at, "" when its manifest cannot be read. +func (c CatalogRead) scopeOf(m delivery.Module, provision string) string { + if m.Manifest == "" { + return "" + } + raw, err := c.Manifest(m.Manifest) + if err != nil { + return "" + } + man, err := catalogue.Decode([]byte(raw)) + if err != nil { + return "" + } + for _, o := range man.Provides { + if o.Name == provision { + return o.At() + } + } + return "" +} + +// CatalogDependentsAnswer is the verb dependents: the modules whose build edges reach the module, directly or through +// others, from the module graph. +func (c CatalogRead) CatalogDependentsAnswer(module string) (map[string]any, error) { + module = strings.TrimSpace(module) + if module == "" { + return nil, fmt.Errorf("name the module") + } + g, mods, err := c.graphAndModules() + if err != nil { + return nil, err + } + out := []CatalogEnd{} + for _, name := range g.Dependents(module) { + out = append(out, CatalogEnd{Module: name, Commit: mods[name].Commit}) + } + return map[string]any{"module": module, "dependents": out}, nil +} + +func (c CatalogRead) graphAndModules() (delivery.Graph, map[string]delivery.Module, error) { + g, err := c.Graph() + if err != nil { + return nil, nil, fmt.Errorf("the module graph cannot be read: %w", err) + } + mods, err := c.Modules() + if err != nil { + return nil, nil, fmt.Errorf("the indexer's modules cannot be read: %w", err) + } + return g, mods, nil +} diff --git a/modules/mesh-delivery-indexer/internal/index/catalog_test.go b/modules/mesh-delivery-indexer/internal/index/catalog_test.go new file mode 100644 index 00000000..6cec8c2a --- /dev/null +++ b/modules/mesh-delivery-indexer/internal/index/catalog_test.go @@ -0,0 +1,125 @@ +package index + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "reflect" + "slices" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// capturedKeys are the field names of the first object at path (a list's first element when the value is a list) in the +// old catalog module's answer captured with mesh_call on 2026-11-03 (testdata/catalog; trimmed to a few modules, and the +// manifest of catalog_module to three of its keys). +func capturedKeys(t *testing.T, file string, path ...string) []string { + t.Helper() + raw, err := os.ReadFile(filepath.Join("testdata", "catalog", file)) + if err != nil { + t.Fatal(err) + } + return keysAt(t, raw, path...) +} + +func keysAt(t *testing.T, raw []byte, path ...string) []string { + t.Helper() + var v any + if err := json.Unmarshal(raw, &v); err != nil { + t.Fatal(err) + } + for _, p := range path { + v = v.(map[string]any)[p] + if l, ok := v.([]any); ok { + v = l[0] + } + } + var out []string + for k := range v.(map[string]any) { + out = append(out, k) + } + slices.Sort(out) + return out +} + +// catalogVerbsAnswerFromTheStates: the seat mesh-catalog's four verbs answer from the indexer's modules, manifests and +// graph, with the field names of the old catalog module's answers (ADR 0604 §1, §2). +func catalogVerbsAnswerFromTheStates(t *testing.T) { + withAcme := strings.Replace(renamed(t, "audit-logger", "acme"), `"module": "acme",`, + `"module": "acme", "provides": [{"name": "internal-acme-ca", "scope": "mesh", "answered-by": "helper"}],`, 1) + ix, mods := newFormIndexer(t, mod{"mesh-tools", renamed(t, "avahi", "mesh-tools")}, + mod{"avahi", standingOn(t, catalogueManifest(t, "avahi"), "mesh-tools")}, + mod{"ca-trust", catalogueManifest(t, "ca-trust")}, mod{"acme", withAcme}) + reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1") + c := CatalogRead{ + Modules: func() (map[string]delivery.Module, error) { + out := map[string]delivery.Module{} + for k := range mods.vals { + m, _ := moduleOf(t, mods, k) + out[k] = m + } + return out, nil + }, + Manifest: func(digest string) (string, error) { + raw, _, err := ix.manifestRaw(context.Background(), digest) + return raw, err + }, + Graph: func() (delivery.Graph, error) { + g := delivery.Graph{} + for k := range ix.Graph.(*memStore).vals { + g[k] = graphOf(t, ix, k) + } + return g, nil + }, + } + answer := func(v map[string]any, err error) []byte { + t.Helper() + if err != nil { + t.Fatal(err) + } + raw, _ := json.Marshal(v) + return raw + } + contains := func(got, want []string) bool { + for _, w := range want { + if !slices.Contains(got, w) { + return false + } + } + return true + } + raw := answer(c.CatalogModulesAnswer()) + if got, want := keysAt(t, raw, "modules"), capturedKeys(t, "catalog_modules.json", "modules"); !contains(got, want) { + t.Errorf("modules' fields %v, want the old catalog's %v", got, want) + } + if !strings.Contains(string(raw), `"module":"avahi"`) || !strings.Contains(string(raw), `"fp":"src2:`) { + t.Errorf("modules = %s", raw) + } + raw = answer(c.CatalogModuleAnswer("ca-trust")) + want := slices.DeleteFunc(capturedKeys(t, "catalog_module.json", "module"), func(k string) bool { return k == "ref" }) // the trunk's, no ref + if got := keysAt(t, raw, "module"); !contains(got, want) || !strings.Contains(string(raw), `"internal-acme-ca"`) { + t.Errorf("module ca-trust = %s, want the fields %v and its manifest", raw, want) + } + if _, err := c.CatalogModuleAnswer("gone"); err == nil || !strings.Contains(err.Error(), "no module gone") { + t.Errorf("a module the trunk does not hold: %v", err) + } + raw = answer(c.CatalogProvidesAnswer("internal-acme-ca")) + if got, want := keysAt(t, raw, "provides"), capturedKeys(t, "catalog_provides.json", "provides"); !reflect.DeepEqual(got, want) || + !reflect.DeepEqual(keysAt(t, raw, "requires"), capturedKeys(t, "catalog_provides.json", "requires")) || + !strings.Contains(string(raw), `"provides":[{"module":"acme","commit":"`+merge+`","scope":"mesh"}]`) || + !strings.Contains(string(raw), `"requires":[{"module":"ca-trust"`) { + t.Errorf("provides internal-acme-ca = %s", raw) + } + raw = answer(c.CatalogDependentsAnswer("mesh-tools")) + if got, want := keysAt(t, raw), capturedKeys(t, "catalog_dependents.json"); !reflect.DeepEqual(got, want) || + !strings.Contains(string(raw), `"dependents":[{"module":"avahi"`) { + t.Errorf("dependents of mesh-tools = %s", raw) + } + raw = answer(c.CatalogDependentsAnswer("mesh-build-go")) // the build module reaches every Go module it builds + if !strings.Contains(string(raw), `"avahi"`) || !strings.Contains(string(raw), `"mesh-tools"`) { + t.Errorf("dependents of mesh-build-go = %s", raw) + } +} diff --git a/modules/mesh-delivery-indexer/internal/index/manifests_test.go b/modules/mesh-delivery-indexer/internal/index/manifests_test.go index 835092ca..b9817ec9 100644 --- a/modules/mesh-delivery-indexer/internal/index/manifests_test.go +++ b/modules/mesh-delivery-indexer/internal/index/manifests_test.go @@ -45,6 +45,7 @@ func withManifests(f delivery.Forge, manifests map[string]string, locks map[stri func TestAMergedManifestIsKeptOnceByItsDigestAndNamedByTheModule(t *testing.T) { t.Run("kept by its digest and named by the module", mergedManifestIsKeptByItsDigestAndNamedByTheModule) t.Run("the same manifest applied again is written once", sameManifestAppliedAgainIsWrittenOnce) + t.Run("the seat mesh-catalog's verbs answer from the states", catalogVerbsAnswerFromTheStates) } func mergedManifestIsKeptByItsDigestAndNamedByTheModule(t *testing.T) { // 1449/AC1 diff --git a/modules/mesh-delivery-indexer/internal/index/testdata/catalog/catalog_dependents.json b/modules/mesh-delivery-indexer/internal/index/testdata/catalog/catalog_dependents.json new file mode 100644 index 00000000..8f6b152b --- /dev/null +++ b/modules/mesh-delivery-indexer/internal/index/testdata/catalog/catalog_dependents.json @@ -0,0 +1,4 @@ +{ + "dependents": [], + "module": "mesh-build-go" +} diff --git a/modules/mesh-delivery-indexer/internal/index/testdata/catalog/catalog_module.json b/modules/mesh-delivery-indexer/internal/index/testdata/catalog/catalog_module.json new file mode 100644 index 00000000..eecc2522 --- /dev/null +++ b/modules/mesh-delivery-indexer/internal/index/testdata/catalog/catalog_module.json @@ -0,0 +1,18 @@ +{ + "module": { + "commit": "b5475cfa5c3daffd52b5b25eb7063e745b976026", + "manifest": { + "module": "gitea", + "requires": [ + "postgres-database", + "route", + "secret" + ], + "version": "1" + }, + "module": "gitea", + "path": "modules/gitea", + "ref": "refs/pull/571/head", + "repository": "novox/mesh-catalog" + } +} diff --git a/modules/mesh-delivery-indexer/internal/index/testdata/catalog/catalog_modules.json b/modules/mesh-delivery-indexer/internal/index/testdata/catalog/catalog_modules.json new file mode 100644 index 00000000..e5d979fa --- /dev/null +++ b/modules/mesh-delivery-indexer/internal/index/testdata/catalog/catalog_modules.json @@ -0,0 +1,22 @@ +{ + "modules": [ + { + "commit": "b5475cfa5c3daffd52b5b25eb7063e745b976026", + "module": "gitea", + "path": "modules/gitea", + "repository": "novox/mesh-catalog" + }, + { + "commit": "dc90b3f5bd1386747ac70f2792169babf8eeed73", + "module": "mesh-controller", + "path": ".", + "repository": "novox/mesh-controller" + }, + { + "commit": "432326264f6c34ceec1fb67f5ca9d450b4caaa1d", + "module": "postgres", + "path": "modules/postgres", + "repository": "novox/mesh-catalog" + } + ] +} diff --git a/modules/mesh-delivery-indexer/internal/index/testdata/catalog/catalog_provides.json b/modules/mesh-delivery-indexer/internal/index/testdata/catalog/catalog_provides.json new file mode 100644 index 00000000..b4b1606f --- /dev/null +++ b/modules/mesh-delivery-indexer/internal/index/testdata/catalog/catalog_provides.json @@ -0,0 +1,15 @@ +{ + "provides": [ + { + "commit": "432326264f6c34ceec1fb67f5ca9d450b4caaa1d", + "module": "postgres", + "scope": "mesh" + } + ], + "requires": [ + { + "commit": "b5475cfa5c3daffd52b5b25eb7063e745b976026", + "module": "gitea" + } + ] +} From 6bc16d74db5ab1057bc8a90dbae138e6521cd6e9 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 16:38:02 +0100 Subject: [PATCH 25/49] Write and take in a merge that leaves no module.json, so a module-less repository's reach is not held for ever and a removed last module is dropped (issue 1126) The forge bridge wrote no merged message for such a merge, so taken. never moved and the view held 'the indexer has not yet taken in ' (hq #490-#492). The bridge now writes it with no trees; the indexer drops every module of the repository on it, after the same order check as any merged message. The view needed no change: with the taken key moving it reaches the merge, which its new row proves. Needs mesh-sdk fix/1126-no-module-reach (a merged message may carry no trees). --- .../cmd/mesh-delivery-forge-bridge/main.go | 4 --- .../internal/bridge/bridge.go | 16 +++++----- .../internal/bridge/bridge_test.go | 14 +++++++-- .../internal/index/modules.go | 6 ++-- .../internal/index/modules_test.go | 30 +++++++++++++++++-- .../internal/view/reach_test.go | 14 ++++++++- 6 files changed, 64 insertions(+), 20 deletions(-) diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/main.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/main.go index deb2a428..c8bb3b00 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/main.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/main.go @@ -74,10 +74,6 @@ func handle(ctx context.Context, m delivered, pub publisher, trees bridge.Trees, logger.Printf("%v; heard again in %s", err, retryDelay) _ = m.NakWithDelay(retryDelay) return - case errors.Is(err, bridge.ErrNoModules): - logger.Printf("%v; no merged message", err) - _ = m.Ack() - return case errors.Is(err, bridge.ErrNotHandled): logger.Printf("%v; nothing written", err) _ = m.Ack() diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge.go b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge.go index c8cb1a32..774bc6b1 100644 --- a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge.go +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge.go @@ -29,9 +29,6 @@ var ErrNotHandled = errors.New("event not handled") // heard again later. var ErrRetry = errors.New("the forge could not say") -// ErrNoModules says a merge into the trunk left a repository with no module.json anywhere: no merged message. -var ErrNoModules = errors.New("no module.json in the merge") - // Trunk is the branch a merged message is written for. const Trunk = "main" @@ -100,7 +97,9 @@ func unreadable(subject, why string, args ...any) error { // Convert turns one event of the forge module, by its bus subject and body, into a message. An event it cannot read // is an ErrUnreadable error naming the subject and why; an event it writes nothing for is ErrNotHandled. A merge into // the trunk asks trees once and becomes a merged message; a merge into any other branch is a closed pull message, as the -// pull request left its group. A forge that cannot say is ErrRetry; a merge with no module.json is ErrNoModules. +// pull request left its group. A forge that cannot say is ErrRetry. A merge that leaves no module.json anywhere is a merged message with no +// trees all the same (issue 1126): the indexer takes it in and drops the repository's modules, and the view reads the +// repository as reached at it. func Convert(subject string, data []byte, trees Trees, files Files) (Message, error) { return ConvertKept(subject, data, trees, files, nil) } @@ -294,9 +293,6 @@ func merged(subject string, p pull, trees Trees, files Files, kept Kept) (Messag if err != nil { return Message{}, fmt.Errorf("%w: %s: the trees of %s at the merge commit %s: %v", ErrRetry, subject, repository, p.MergeSHA, err) } - if len(dirs) == 0 { - return Message{}, fmt.Errorf("%w: %s: %s at the merge commit %s", ErrNoModules, subject, repository, p.MergeSHA) - } names := make([]string, 0, len(dirs)) for dir := range dirs { names = append(names, dir) @@ -339,7 +335,11 @@ func merged(subject string, p pull, trees Trees, files Files, kept Kept) (Messag if err != nil { return Message{}, unreadable(subject, "%v", err) } - return Message{Event: event, Data: body}, nil + msg := Message{Event: event, Data: body} + if len(dirs) == 0 { + msg.Said = fmt.Sprintf("%s at the merge commit %s holds no module.json: a merged message with no module directories", repository, p.MergeSHA) + } + return msg, nil } // openPull is an entry of gitea_open_pulls's answer (mesh-catalog modules/gitea/client.ts:369-381 openPulls, with the diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go index 95367380..3bd1116f 100644 --- a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go @@ -269,11 +269,19 @@ func TestTreesRefusedWritesNothingAndSaysWhy(t *testing.T) { // 1236/AC3 (the co } } -func TestNoModuleJSONAnywhereWritesNoMergedMessage(t *testing.T) { // 1236 (notes: a repository with no module.json) +// 1126 (note 94): a merge that leaves no module.json anywhere (hq, or a repository whose last module was removed) is a merged +// message with no trees, so the indexer takes it in and the repository's reach is not held for ever. +func TestNoModuleJSONAnywhereWritesAMergedMessageWithNoTrees(t *testing.T) { var calls []call msg, err := Convert(mergedSubject, mergedBody(nil), treesOf(&calls, "r0", map[string]string{}, nil), anyFiles) - if !errors.Is(err, ErrNoModules) || msg.Data != nil { - t.Errorf("err = %v data = %s", err, msg.Data) + if err != nil { + t.Fatal(err) + } + f, err := delivery.DecodeForge(msg.Data) + want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, + Head: head, Merge: merge, Root: "r0"} + if err != nil || msg.Event != "forge.novox.mesh-delivery.merged" || !delivery.SameForge(f, want) || !strings.Contains(msg.Said, "no module.json") { + t.Errorf("event %q message %+v said %q, %v; want a merged message with no trees", msg.Event, f, msg.Said, err) } } diff --git a/modules/mesh-delivery-indexer/internal/index/modules.go b/modules/mesh-delivery-indexer/internal/index/modules.go index a8cc9842..77dc7568 100644 --- a/modules/mesh-delivery-indexer/internal/index/modules.go +++ b/modules/mesh-delivery-indexer/internal/index/modules.go @@ -117,8 +117,10 @@ func (ix *Indexer) applyMoving(ctx context.Context, f delivery.Forge, drop bool, overlay[key] = next } // A module of this repository the merge commit holds no module.json for is gone from the trunk, and so is its key - // (issue 1900). A message that carries no manifests (an older writer) or one that cannot be read whole says nothing. - if drop && len(f.Manifests) > 0 && every { + // (issue 1900). A message with no trees says the merge commit holds no module.json at all, so every module of the + // repository goes (issue 1126 note 94: a repository that removed its last module). A message with trees but no + // manifests (an older writer) or one that cannot be read whole says nothing. + if drop && (len(f.Manifests) > 0 || len(f.Trees) == 0) && every { if err := ix.dropRemoved(ctx, f.Repository, held, func(key string, _ delivery.Module) bool { return named[key] }); err != nil { return w, err } diff --git a/modules/mesh-delivery-indexer/internal/index/modules_test.go b/modules/mesh-delivery-indexer/internal/index/modules_test.go index b75b5a8f..767f8746 100644 --- a/modules/mesh-delivery-indexer/internal/index/modules_test.go +++ b/modules/mesh-delivery-indexer/internal/index/modules_test.go @@ -219,6 +219,32 @@ func TestAnUnchangedModuleWritesNothing(t *testing.T) { // 1239 (unchanged value } return ix.Apply(context.Background(), mergedMsg(repo, three)) }}, + // Issue 1126 note 94: a merge that leaves no module.json anywhere (hq, or a repository whose last module was removed) is a + // merged message with no trees; it drops the repository's modules and moves its taken key, and an older one, heard late, + // goes through the same order check and drops nothing. + {name: "a merge that leaves no module.json drops every module and is taken in", writes: 3, commit: map[string]string{"a": "", "b": "", "c": ""}, taken: next, + said: "modules written: none", + then: func(ix *Indexer) error { + m := mergedMsg(repo, nil) + m.Merge = next + return ix.Apply(context.Background(), m) + }}, + {name: "an earlier merge with no module.json heard after a later one drops nothing", writes: 0, commit: map[string]string{"a": merge, "b": merge, "c": merge}, taken: next, + said: "the merged message is older than taken.novox/mesh-delivery at cccccccccccc", + then: func(ix *Indexer) error { + const earlier = "dddddddddddddddddddddddddddddddddddddddd" + ix.Contains = func(_ context.Context, _, commit, in string) (bool, error) { + return commit == in || commit == earlier && in == next, nil + } + later := mergedMsg(repo, three) + later.Merge = next + if err := ix.Apply(context.Background(), later); err != nil { + return err + } + gone := mergedMsg(repo, nil) + gone.Merge = earlier + return ix.Apply(context.Background(), gone) + }}, // Review round 2: a 502 whose words carry "404" inside a sha is no not-found; the late message is left for redelivery. {name: "a late message whose order the forge fails with a 502 writes nothing and is left for redelivery", writes: 1, commit: map[string]string{"a": next, "b": merge, "c": merge}, taken: next, err: true, said: "502 Bad Gateway", @@ -305,8 +331,8 @@ func TestAnUnchangedModuleWritesNothing(t *testing.T) { // 1239 (unchanged value if got := mods.writes - writes; got != tc.writes { t.Errorf("%d values written, want %d", got, tc.writes) } - for module, want := range tc.commit { - if m, ok := moduleOf(t, mods, module); !ok || m.Commit != want { + for module, want := range tc.commit { // "" is a module the merge deleted + if m, ok := moduleOf(t, mods, module); ok != (want != "") || m.Commit != want { t.Errorf("%s = %+v (%v), want commit %.8s", module, m, ok, want) } } diff --git a/modules/mesh-delivery-view/internal/view/reach_test.go b/modules/mesh-delivery-view/internal/view/reach_test.go index 236a1697..276049ce 100644 --- a/modules/mesh-delivery-view/internal/view/reach_test.go +++ b/modules/mesh-delivery-view/internal/view/reach_test.go @@ -13,7 +13,7 @@ import ( func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) { const web, sdk = "novox/web", "novox/sdk" commitA, commitB, commitC := strings.Repeat("a", 40), strings.Repeat("b", 40), strings.Repeat("c", 40) - commitS, commitX := strings.Repeat("5", 40), strings.Repeat("e", 40) + commitS, commitX, commitD := strings.Repeat("5", 40), strings.Repeat("e", 40), strings.Repeat("d", 40) const fpMail = "src2:3333333333333333333333333333333333333333333333333333333333333333" mod := func(module, fp, commit string, src uint64, history bool) Entry { v, err := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: strings.Repeat("a", 64), Repository: web, Commit: commit, Src: src, Path: module}) @@ -84,6 +84,18 @@ func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) { then: []Entry{taken(web, commitB)}}, {name: "a change whose source revision is not known yet holds no head the source named", reached: commitC, then: []Entry{mod("web", "src2:"+strings.Repeat("9", 64), commitX, 0, true), mod("web", "src2:"+strings.Repeat("9", 64), commitX, 0, false)}}, + // 1126 note 94: a merge that removes the repository's last module.json moves nothing; once the indexer dropped its + // modules and took the merge in, the repository reaches it, held by nothing, as the library with no module does. + {name: "a merge that removed the last module is reached at the merge the indexer took in", reached: commitD, + then: func() []Entry { + head := src(13, commitD) + current := head + current.History = false + gone := func(module string, history bool) Entry { + return Entry{Bucket: delivery.ModulesBucket, Key: module, Deleted: true, At: t0, History: history} + } + return []Entry{head, current, gone("web", true), gone("mail", true), gone("web", false), gone("mail", false), taken(web, commitD)} + }()}, // 1126: the view started while the bus did not answer its writes; its reach and its event are made at the next change it // takes in, though the derivation is the same, so the state reach never keeps an older have than node_haves says. {name: "a write and an event refused at the start are made at the next change", reached: commitC, refused: true, From e1d0c9fe032ac098f52ed1ee17ddf902eb52ac66 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 16:20:46 +0100 Subject: [PATCH 26/49] A merge that leaves a repository no module never deletes a module another trunk holds, and indexer_take_in takes a repository in (issue 1367) dropRemoved hands the module over from the other trunk, read from the forge at the head the source names, so no wait in memory is needed. indexer_take_in with a repository takes in a head with no module.json as a merge that leaves none, and answers by name for a head with modules. --- .../cmd/mesh-delivery-indexer-tools/main.go | 22 +++++-- .../cmd/mesh-delivery-indexer/takein.go | 6 +- .../internal/index/modules.go | 12 ++++ .../internal/index/modules_test.go | 62 ++++++++++++++++--- .../internal/index/moves.go | 61 ++++++++++++++++++ .../internal/index/registered.go | 62 +++++++++++++++++++ .../internal/index/takein.go | 19 +++++- 7 files changed, 228 insertions(+), 16 deletions(-) diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go index 41bc0f1d..cfc4ff71 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go @@ -48,12 +48,24 @@ func Tools(ask asker, now func() time.Time, read stdio.KeptState, counts func() "indexer) is read again at that head and written when it changed. Answers stored, from which " + "repository, commit and path, or why not (held already, no registration, the forge's error, no manifest there). " + "The indexer reads such a module again by itself on the next source revision and merged message; this asks now. " + - "indexer_module (mesh-delivery-view) reads the result.", - Input: map[string]any{"type": "object", "required": []string{"module"}, "properties": map[string]any{ - "module": map[string]any{"type": "string", "description": "the module's name, as the catalogue registers it"}, - "why": map[string]any{"type": "string", "description": "why it is taken in now, in plain words"}}}, + "indexer_module (mesh-delivery-view) reads the result. With repository instead of module it takes in the head the gate's " + + "source names for that repository, for a merge whose merged message the indexer never heard (the forge bridge has no " + + "tool): a head whose trunk holds no module.json is taken in as a merge that leaves none (every module of the repository " + + "is dropped, one another repository's trunk holds handed over from there first, listed in handed and dropped); a head " + + "that holds modules is answered \"take each module in by name\".", + Input: map[string]any{"type": "object", "properties": map[string]any{ + "module": map[string]any{"type": "string", "description": "the module's name, as the catalogue registers it"}, + "repository": map[string]any{"type": "string", "description": "instead of module: the repository (owner/name) whose head is taken in"}, + "why": map[string]any{"type": "string", "description": "why it is taken in now, in plain words"}}}, Run: func(args map[string]any) (any, error) { - key, value, err := index.NewTakeIn(str(args, "module"), str(args, "why"), stdio.CallerOf(args), now()) + var key string + var value []byte + var err error + if str(args, "repository") != "" && str(args, "module") == "" { + key, value, err = index.NewTakeInRepository(str(args, "repository"), str(args, "why"), stdio.CallerOf(args), now()) + } else { + key, value, err = index.NewTakeIn(str(args, "module"), str(args, "why"), stdio.CallerOf(args), now()) + } if err != nil { return nil, err } diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/takein.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/takein.go index 6ec98671..a32aea21 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/takein.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/takein.go @@ -14,6 +14,7 @@ import ( // takerIn is the part of the indexer a takeins value asks through. type takerIn interface { TakeIn(ctx context.Context, module string) index.TakenIn + TakeInRepository(ctx context.Context, repository string) index.TakenIn JudgeAgain(ctx context.Context, module string) index.TakenIn } @@ -33,7 +34,10 @@ func takenIn(ctx context.Context, ix takerIn, kv jetstream.KeyValue, e jetstream return } asked, r := "take-in", index.TakenIn{} - if a.Judge { + if a.Repository != "" { + asked, r = "take-in of a repository", ix.TakeInRepository(ctx, a.Repository) + a.Module = a.Repository + } else if a.Judge { asked, r = "judging again", ix.JudgeAgain(ctx, a.Module) } else { r = ix.TakeIn(ctx, a.Module) diff --git a/modules/mesh-delivery-indexer/internal/index/modules.go b/modules/mesh-delivery-indexer/internal/index/modules.go index 77dc7568..fece3567 100644 --- a/modules/mesh-delivery-indexer/internal/index/modules.go +++ b/modules/mesh-delivery-indexer/internal/index/modules.go @@ -216,6 +216,7 @@ func (ix *Indexer) namedObjects(ctx context.Context, f delivery.Forge) (map[stri // held(key, module) says whether the trunk holds it. A module of another repository, or of none known, is never touched. func (ix *Indexer) dropRemoved(ctx context.Context, repository string, held map[string]delivery.Module, trunkHolds func(key string, m delivery.Module) bool) error { var handed []string + elsewhere := map[string]delivery.Forge{} for _, key := range slices.Sorted(maps.Keys(held)) { cur := held[key] if cur.Repository == "" || !delivery.SameRepository(cur.Repository, repository) || trunkHolds(key, cur) { @@ -225,6 +226,12 @@ func (ix *Indexer) dropRemoved(ctx context.Context, repository string, held map[ handed = append(handed, key) // written in place by the repository it moves to, never deleted first continue } + // The wait of moves.go is memory, which a restart forgets (issue 1367): another repository's trunk that holds the + // module is asked, so the move survives it. + if f, ok := ix.heldByAnotherTrunk(ctx, key, repository); ok { + elsewhere[key] = f + continue + } err := write(ctx, ix.Modules, key, func(value []byte, found bool) ([]byte, bool, error) { if !found { return nil, false, nil @@ -244,6 +251,11 @@ func (ix *Indexer) dropRemoved(ctx context.Context, repository string, held map[ return err } } + for _, key := range slices.Sorted(maps.Keys(elsewhere)) { + if err := ix.handOverFrom(ctx, key, elsewhere[key]); err != nil { + return err + } + } return nil } diff --git a/modules/mesh-delivery-indexer/internal/index/modules_test.go b/modules/mesh-delivery-indexer/internal/index/modules_test.go index 767f8746..b7c32974 100644 --- a/modules/mesh-delivery-indexer/internal/index/modules_test.go +++ b/modules/mesh-delivery-indexer/internal/index/modules_test.go @@ -445,31 +445,76 @@ func (d *deletesSeen) Delete(ctx context.Context, key string, rev uint64) error // holding repository's merge that drops it hands it over in place, never deleting its key; merged the other way round, // the module is deleted and then written new. func TestAModuleMovesBetweenRepositoriesOnlyWhenItsTrunkDropsIt(t *testing.T) { - const from, to = "novox/mesh-controller", "novox/mesh-networking" + const from, to, toHead = "novox/mesh-controller", "novox/mesh-networking", "bbbbbbbbbbbb" held := mergedMsg(from, map[string]string{"modules/a": "t", "modules/b": "t"}) named := mergedMsg(to, map[string]string{"modules/a": "t2"}) dropped := mergedMsg(from, map[string]string{"modules/b": "t"}) + gone := mergedMsg(from, nil) // issue 1367: a merge that leaves no module.json + gone.Trees, gone.Manifests = nil, nil for _, tc := range []struct { name string merges []delivery.Forge repository string // modules.a's repository at the end, "" for none deletedKeyA bool + // Issue 1367: a fresh indexer, no wait in memory, the source naming heads; trunk is the module directories the forge + // reads at the head of `to`; takeIn is a repository taken in after the merges (indexer_take_in), whose answer must + // hold answer and be stored or not. + heads bool + trunk []string + takeIn string + answer string + stored bool + bGoneAtEnd bool }{ - {"named while its trunk holds it: left", []delivery.Forge{held, named}, from, false}, - {"named, then dropped by its trunk: handed over in place", []delivery.Forge{held, named, dropped}, to, false}, - {"dropped by its trunk, then named: deleted, then written new", []delivery.Forge{held, dropped, named}, to, true}, - {"dropped by its trunk with no merge naming it: deleted", []delivery.Forge{held, dropped}, "", true}, + {name: "named while its trunk holds it: left", merges: []delivery.Forge{held, named}, repository: from}, + {name: "named, then dropped by its trunk: handed over in place", merges: []delivery.Forge{held, named, dropped}, repository: to}, + {name: "dropped by its trunk, then named: deleted, then written new", merges: []delivery.Forge{held, dropped, named}, repository: to, deletedKeyA: true}, + {name: "dropped by its trunk with no merge naming it: deleted", merges: []delivery.Forge{held, dropped}, deletedKeyA: true}, + {name: "no module.json left, another trunk holds it, nothing waits in memory: written from that trunk, never deleted", + merges: []delivery.Forge{held, gone}, heads: true, trunk: []string{"modules/a", "modules/other"}, repository: to, bGoneAtEnd: true}, + {name: "no module.json left, no other trunk holds it: deleted", + merges: []delivery.Forge{held, gone}, heads: true, deletedKeyA: true, bGoneAtEnd: true}, + {name: "taken in by repository, a head with no module.json: handed over from the other trunk", + merges: []delivery.Forge{held}, heads: true, trunk: []string{"modules/a"}, takeIn: from, answer: "handed over", stored: true, repository: to, bGoneAtEnd: true}, + {name: "taken in by repository, a head with modules: answered by name", + merges: []delivery.Forge{held}, heads: true, trunk: []string{"modules/a"}, takeIn: to, answer: "by name", repository: from}, + {name: "taken in by repository, no head named: answered so", + merges: []delivery.Forge{held}, heads: true, takeIn: "novox/nobody", answer: "names no head", repository: from}, } { t.Run(tc.name, func(t *testing.T) { mods := &deletesSeen{memStore: newMem()} ix := &Indexer{Store: newMem(), Modules: mods, Builds: NewBuilds()} var logs []string ix.Logf = func(format string, args ...any) { logs = append(logs, format) } + if tc.heads { + ix.source = sourceNow{rev: 7, heads: map[string]string{from: merge, to: toHead}} + ix.Trunk = func(_ context.Context, repository, sha string) ([]string, error) { + if repository == to && sha == toHead { + return tc.trunk, nil + } + if repository == from && tc.takeIn == from { + return nil, nil + } + return nil, nil + } + ix.Read = func(_ context.Context, repository, sha, dir string) (RepoRead, error) { + if repository != to || sha != toHead || dir != "modules/a" { + t.Errorf("read %s at %s %q, want only %s at %s modules/a", repository, sha, dir, to, toHead) + } + return RepoRead{Root: "r2", Trees: map[string]string{dir: "t2"}, Manifests: map[string]string{dir: `{"module":"a","version":"1"}`}}, nil + } + } for _, f := range tc.merges { if err := ix.Apply(context.Background(), f); err != nil { t.Fatal(err) } } + if tc.takeIn != "" { + got := ix.TakeInRepository(context.Background(), tc.takeIn) + if got.Stored != tc.stored || !strings.Contains(got.Answer, tc.answer) { + t.Errorf("take-in answered %+v, want stored %v and %q", got, tc.stored, tc.answer) + } + } m, ok := moduleOf(t, mods.memStore, "a") if got := map[bool]string{true: m.Repository}[ok]; got != tc.repository { t.Errorf("modules.a of %q, want %q (logs %v)", got, tc.repository, logs) @@ -477,11 +522,14 @@ func TestAModuleMovesBetweenRepositoriesOnlyWhenItsTrunkDropsIt(t *testing.T) { if ok && m.Path != "modules/a" { t.Errorf("modules.a at %q, want modules/a", m.Path) } + if ok && m.Repository == to && tc.heads && m.Commit != toHead { + t.Errorf("modules.a at %s, want the other trunk's head %s", m.Commit, toHead) + } if deleted := slices.Contains(mods.deleted, "a"); deleted != tc.deletedKeyA { t.Errorf("modules.a deleted %v, want %v", deleted, tc.deletedKeyA) } - if _, ok := mods.vals["b"]; !ok { - t.Error("modules.b, which its trunk still holds, is gone") + if _, ok := mods.vals["b"]; ok == tc.bGoneAtEnd { + t.Errorf("modules.b held %v, want %v", ok, !tc.bGoneAtEnd) } if len(logs) == 0 { t.Error("nothing was said") diff --git a/modules/mesh-delivery-indexer/internal/index/moves.go b/modules/mesh-delivery-indexer/internal/index/moves.go index 8567d4d4..e192731a 100644 --- a/modules/mesh-delivery-indexer/internal/index/moves.go +++ b/modules/mesh-delivery-indexer/internal/index/moves.go @@ -3,10 +3,66 @@ package index import ( "context" "fmt" + "maps" + "slices" + "strings" "git.novox.be/novox/mesh-sdk/go/delivery" ) +// heldByAnotherTrunk reads, for a module the trunk of repository no longer holds, whether the trunk of another repository +// holds it at the head the latest source names: the module directories there are asked of the forge (Trunk) and the manifest of +// each directory named like the module is read (Read). It answers the merge to hand the module over with, or none: a forge +// that cannot say leaves the module to be dropped as it was, said (issue 1367). +func (ix *Indexer) heldByAnotherTrunk(ctx context.Context, module, repository string) (delivery.Forge, bool) { + if ix.Trunk == nil || ix.Read == nil { + return delivery.Forge{}, false + } + ix.mu.Lock() + heads := maps.Clone(ix.source.heads) + ix.mu.Unlock() + for _, other := range slices.Sorted(maps.Keys(heads)) { + head := heads[other] + if head == "" || delivery.SameRepository(other, repository) { + continue + } + dirs, err := ix.Trunk(ctx, other, head) + if err != nil { + ix.logf("%s at %s: the modules of the trunk cannot be read, so %s is not looked for there: %v", other, head, module, err) + continue + } + for _, d := range dirs { + dir := strings.Trim(d, "/") + if nameOfDirectory(dir) != module { + continue + } + read, err := ix.Read(ctx, other, head, dir) + if err != nil { + ix.logf("%s at %s: %q cannot be read, so %s is not handed over from it: %v", other, head, dir, module, err) + continue + } + raw := read.Manifests[dir] + if read.Trees[dir] == "" || manifestModule(raw) != module { + continue + } + f := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: other, Merge: head, Root: read.Root, + Trees: map[string]string{dir: read.Trees[dir]}, Manifests: map[string]string{dir: raw}} + if l := read.Locks[dir]; len(l) > 0 { + f.Locks = map[string]map[string]string{dir: l} + } + if p := read.Packages[dir]; len(p) > 0 { + f.Packages = map[string][]string{dir: p} + } + ix.logf("%s at %s: the trunk holds the module %s at %q, so it is handed over from there and not deleted (issue 1367)", other, head, module, dir) + return f, true + } + } + return delivery.Forge{}, false +} + +// nameOfDirectory is the last element of a module directory. +func nameOfDirectory(dir string) string { return dir[strings.LastIndex(dir, "/")+1:] } + // A module moves between repositories by the merges of the delivery group that moves it (novox/hq ADR 0420 clause 7, ADR // 0501 clause 3; issue 1126: route-proxy leaves the controller's repository, which is archived). The merge of the // repository it moves to names it while the indexer holds it of another: that merge leaves it, says so, and waits here. @@ -57,6 +113,11 @@ func (ix *Indexer) handOver(ctx context.Context, key string) error { if !ok { return nil } + return ix.handOverFrom(ctx, key, f) +} + +// handOverFrom writes the module at key from the merge f, which names it, in place. +func (ix *Indexer) handOverFrom(ctx context.Context, key string, f delivery.Forge) error { w, err := ix.applyMoving(ctx, f, false, map[string]bool{key: true}) if err != nil { return fmt.Errorf("modules.%s, moving to %s: %w", key, f.Repository, err) diff --git a/modules/mesh-delivery-indexer/internal/index/registered.go b/modules/mesh-delivery-indexer/internal/index/registered.go index f541990d..cf0940bb 100644 --- a/modules/mesh-delivery-indexer/internal/index/registered.go +++ b/modules/mesh-delivery-indexer/internal/index/registered.go @@ -213,6 +213,10 @@ type TakenIn struct { Commit string `json:"commit,omitempty"` Path string `json:"path,omitempty"` Answer string `json:"answer"` + // Handed and Dropped are the modules a take-in of a repository (TakeInRepository) moved to another repository's trunk + // and deleted. + Handed []string `json:"handed,omitempty"` + Dropped []string `json:"dropped,omitempty"` } // TakeIn reads a registered module's manifest now, whatever was tried before, and writes it as a merge would (the tool @@ -358,3 +362,61 @@ func (ix *Indexer) takeInHeld(ctx context.Context, module string) (TakenIn, bool } return out, true } + +// TakeInRepository takes in the head the gate's source names for a repository whose merged message the indexer never +// heard (the tool indexer_take_in with repository, issue 1367). A head whose trunk holds no module.json is applied as +// the merged message of a merge that leaves none: every module of the repository goes, one another trunk holds handed over +// from there first. A head whose trunk holds modules is answered "by name": each is taken in with its own name. +func (ix *Indexer) TakeInRepository(ctx context.Context, repository string) TakenIn { + out := TakenIn{Repository: repository} + ix.mu.Lock() + head := "" + for repo, h := range ix.source.heads { + if delivery.SameRepository(repo, repository) && h != "" { + head, out.Repository = h, repo + } + } + ix.mu.Unlock() + if head == "" || ix.Modules == nil || ix.Trunk == nil { + out.Answer = "the gate's source names no head of " + repository + ", or the indexer reads no forge or writes no modules" + return out + } + out.Commit = head + dirs, err := ix.Trunk(ctx, out.Repository, head) + if err != nil { + out.Answer = fmt.Sprintf("the modules of %s at %.12s cannot be read, so nothing is taken in: %v", repository, head, err) + return out + } + if len(dirs) > 0 { + out.Answer = fmt.Sprintf("%s at %.12s holds modules (%s): take each module in by name (module)", repository, head, strings.Join(dirs, ", ")) + return out + } + before, err := ix.heldModules(ctx) + if err != nil { + out.Answer = "the modules state cannot be read: " + err.Error() + return out + } + f := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: out.Repository, Merge: head} + if _, err := ix.ApplyMessage(ctx, f); err != nil { + out.Answer = fmt.Sprintf("the merge of %s at %.12s could not be taken in: %v", repository, head, err) + return out + } + after, err := ix.heldModules(ctx) + if err != nil { + out.Answer = "the modules state cannot be read: " + err.Error() + return out + } + for _, key := range slices.Sorted(maps.Keys(before)) { + if !delivery.SameRepository(before[key].Repository, repository) { + continue + } + if now, ok := after[key]; ok { + out.Handed = append(out.Handed, key+" to "+now.Repository+" at "+now.Commit) + } else { + out.Dropped = append(out.Dropped, key) + } + } + out.Stored = true + out.Answer = fmt.Sprintf("%s at %.12s holds no module.json: taken in as a merge that leaves none; handed over %v, dropped %v", repository, head, out.Handed, out.Dropped) + return out +} diff --git a/modules/mesh-delivery-indexer/internal/index/takein.go b/modules/mesh-delivery-indexer/internal/index/takein.go index b59456b6..10866eb0 100644 --- a/modules/mesh-delivery-indexer/internal/index/takein.go +++ b/modules/mesh-delivery-indexer/internal/index/takein.go @@ -27,7 +27,9 @@ const ( type TakeInAsk struct { V int `json:"v"` Module string `json:"module,omitempty"` - Why string `json:"why,omitempty"` + // Repository asks the head the gate's source names for a repository to be taken in (TakeInRepository) instead of a module. + Repository string `json:"repository,omitempty"` + Why string `json:"why,omitempty"` // Judge asks the manifests to be judged again now (indexer_judge_again) instead of the module to be taken in. Judge bool `json:"judge,omitempty"` By string `json:"by,omitempty"` @@ -48,8 +50,8 @@ func DecodeTakeIn(data []byte) (TakeInAsk, error) { if a.V != 1 { return a, fmt.Errorf("a takeins value of version %d, not 1", a.V) } - if a.Module == "" { - return a, errors.New("a takeins value naming no module") + if a.Module == "" && a.Repository == "" { + return a, errors.New("a takeins value naming no module or repository") } return a, nil } @@ -67,3 +69,14 @@ func NewTakeIn(module, why, by string, at time.Time) (string, []byte, error) { data, err := json.Marshal(TakeInAsk{V: 1, Module: module, Why: strings.TrimSpace(why), By: by, At: at.UTC()}) return key, data, err } + +// NewTakeInRepository is the value indexer_take_in writes for a repository: its key and the value. +func NewTakeInRepository(repository, why, by string, at time.Time) (string, []byte, error) { + repository = strings.TrimSpace(repository) + key, err := delivery.RepositoryKey(repository) + if err != nil { + return "", nil, err + } + data, err := json.Marshal(TakeInAsk{V: 1, Repository: repository, Why: strings.TrimSpace(why), By: by, At: at.UTC()}) + return "repository." + key, data, err +} From 39b3ce3c73daeb53b5eef5b847de0b11a0b4e674 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 16:20:46 +0100 Subject: [PATCH 27/49] builder_builds_since names the operator's command nox builds as its reader, not mesh-catalog (ADR 0604) --- .../cmd/mesh-delivery-builder-tools/main.go | 2 +- modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go | 2 +- modules/mesh-delivery-builder/internal/build/run.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go index c8a31c31..56516c70 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go @@ -98,7 +98,7 @@ func Tools(r build.StuckReader, askAgain func(key string, value []byte) error, q Description: "Every build that ended since a time, oldest first, from the builder's log build-log (kept 30 days): per build its module, " + "fingerprint, source revision, repository, directory, commit and ref, the build node, when it ended, its outcome (done, failed or " + "queued), the manifest built, the bases it was built against and each artifact made with its digest and reference. more says the " + - "log held more than one answer carries: ask again from the last build's time. What mesh-catalog registers a build from. Read-only.", + "log held more than one answer carries: ask again from the last build's time. What the operator's command nox builds reads. Read-only.", Input: map[string]any{"type": "object", "required": []string{"since"}, "properties": map[string]any{ "since": map[string]any{"type": "string", "description": "RFC 3339 time; the builds that ended at or after it"}}}, Run: func(args map[string]any) (any, error) { diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go index 2c118885..bdfdb4a3 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go @@ -131,7 +131,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro trunks.start() b.Trunk = trunks.head // The builder's log (ADR 0571 §1): each build's start and end in its module log `build-log`, read by the view, the - // indexer and the assigner through their reads, and by mesh-catalog through the tool builder_builds_since. + // indexer and the assigner through their reads, and by the operator's command nox builds through the tool builder_builds_since. b.Log = func(e delivery.BuildLogEntry) { if err := appendLog(context.WithoutCancel(ctx), js, cred, e); err != nil { logger.Printf("%v", err) diff --git a/modules/mesh-delivery-builder/internal/build/run.go b/modules/mesh-delivery-builder/internal/build/run.go index 6cae6942..106916cf 100644 --- a/modules/mesh-delivery-builder/internal/build/run.go +++ b/modules/mesh-delivery-builder/internal/build/run.go @@ -944,7 +944,7 @@ func LogStarted(module string, m delivery.Module, ref, by string, at time.Time) } // LogEnded is the log's entry of that build ending: the outcome (done, failed or queued), the manifest built, the bases it -// was built against and each artifact made, as the done key has them: what mesh-catalog registers a build from. +// was built against and each artifact made, as the done key has them: what the operator's command nox builds shows of a build. func LogEnded(module string, m delivery.Module, ref, by string, at time.Time, result delivery.BuilderBuild) delivery.BuildLogEntry { e := LogStarted(module, m, ref, by, at) e.Event, e.Outcome, e.Manifest = delivery.BuildEndedEvent, result.State, m.Manifest From aacdba44e34acad3440fb6f589ad1e8e3ded17d6 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 16:25:04 +0100 Subject: [PATCH 28/49] =?UTF-8?q?The=20judge's=20copy=20of=20the=20mesh-ca?= =?UTF-8?q?talog=20seat=20is=20the=20SDK's=20entry:=20four=20optional=20ve?= =?UTF-8?q?rbs,=20so=20the=20old=20module's=20claim=20is=20not=20outgrown?= =?UTF-8?q?=20(ADR=200604=20=C2=A71;=20F3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/mesh-delivery-indexer/judge/rules.go | 23 ++++++++++++++++++- .../mesh-delivery-indexer/judge/rules_test.go | 11 +++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/modules/mesh-delivery-indexer/judge/rules.go b/modules/mesh-delivery-indexer/judge/rules.go index 49fa9f40..734c5730 100644 --- a/modules/mesh-delivery-indexer/judge/rules.go +++ b/modules/mesh-delivery-indexer/judge/rules.go @@ -7462,7 +7462,8 @@ var defaultSeats = append([]catalogue.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: catalogue.ScopeMesh, Delivers: "artifact-store", Decision: "novox/hq ADR 0075"}, - {Name: "mesh-catalog", Scope: catalogue.ScopeMesh, Decision: "novox/hq ADR 0121"}, + // The catalog (novox/hq ADR 0604): the SDK's entry, its four verbs optional while the module that claimed it before stands. + {Name: "mesh-catalog", Scope: catalogue.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. @@ -7881,6 +7882,26 @@ func configInput() map[string]any { return in } +// catalogVerbs are the catalog seat's four verbs, as the SDK's seat serves them (catalogue.CatalogSeat). +func catalogVerbs() []catalogue.Verb { + return []catalogue.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"})}, + } +} + // deliveryVerbs are the delivery seat's tools: what reads the merges and checks, and the acts of a person and healer H2 // on a merge (ADR 0539 clause 17). func deliveryVerbs() []catalogue.Verb { diff --git a/modules/mesh-delivery-indexer/judge/rules_test.go b/modules/mesh-delivery-indexer/judge/rules_test.go index 6e873e02..5e3c4325 100644 --- a/modules/mesh-delivery-indexer/judge/rules_test.go +++ b/modules/mesh-delivery-indexer/judge/rules_test.go @@ -4825,6 +4825,17 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) { []string{"placement", "proposals", "propose", "withdraw", "again"}) { t.Errorf("the placement seat is %v, %v", ok, s) } + // The judge's copy of the catalog seat is the SDK's entry exactly (ADR 0604 §1): four optional verbs, so the claim of the + // module that held the seat before is not outgrown while both stand. + if sdk, ok := catalogue.SeatNamed(catalogue.CatalogSeat); !ok || len(sdk.Serves) != 4 { + t.Errorf("the SDK's catalog seat is %v, %+v", ok, sdk) + } else { + for _, s := range defaultSeats { + if s.Name == catalogue.CatalogSeat && !reflect.DeepEqual(s, sdk) { + t.Errorf("the judge's copy of the catalog seat is %+v, want the SDK's %+v", s, sdk) + } + } + } // The checker's seat serves the try, which replaces the shell's merge check (ADR 0547 §10, ADR 0245). var checkerVerbs []string for _, s := range catalogue.Seats() { From 875d42fe3e301047fe3c1c606b9eb219d87e788c Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 16:46:40 +0100 Subject: [PATCH 29/49] On B1: the ledger test names its tool runner by its manifest's tool-runner, and ShelfWaiting takes B1's old-section counts (rebase onto main c4470f42) --- modules/mesh-delivery-indexer/judge/judge_test.go | 2 +- modules/mesh-grants/internal/buscompose/buscompose_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/mesh-delivery-indexer/judge/judge_test.go b/modules/mesh-delivery-indexer/judge/judge_test.go index 4b6cdc7c..27caf578 100644 --- a/modules/mesh-delivery-indexer/judge/judge_test.go +++ b/modules/mesh-delivery-indexer/judge/judge_test.go @@ -62,7 +62,7 @@ func TestTheJudge(t *testing.T) { "bad": []byte(`{"module":"bad","version":"1","provides":[{"name":"amqp"}]}`), "ok": []byte(`{"module":"ok","version":"1"}`), } - refused, waits, _ := ShelfWaiting(waitRaws, nil, 0) + refused, waits, _ := ShelfWaiting(waitRaws, nil, 0, nil) if !reflect.DeepEqual(waits, map[string][]string{"later": {"later-field", "other"}}) || len(refused) != 2 || refused["renamed"] == nil || refused["bad"] == nil { t.Errorf("ShelfWaiting: waits %v, refused %v", waits, refused) } diff --git a/modules/mesh-grants/internal/buscompose/buscompose_test.go b/modules/mesh-grants/internal/buscompose/buscompose_test.go index f67098bc..16a053d0 100644 --- a/modules/mesh-grants/internal/buscompose/buscompose_test.go +++ b/modules/mesh-grants/internal/buscompose/buscompose_test.go @@ -978,7 +978,7 @@ func aLedgerIsWrittenByItsLedgerAccountAlone(t *testing.T) { r := Records{Nodes: nodes, Assigned: map[string][]Declared{}} for _, n := range nodes { r.Assigned[n] = []Declared{ - {Module: RuntimeModule}, + {Module: "node-tools", ToolRunner: true}, {Module: "m", Ledgers: []string{"records", "audit"}, Processes: []ProcessPrincipal{{Process: "w"}}}, {Module: "plain"}, } From 0ce16ad773587fe9e79d1a9ad83f7157a458ed61 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 16:46:40 +0100 Subject: [PATCH 30/49] Pin mesh-sdk 18b3eac47c8e, the group's head of feat/l3b-bus rebased onto main 2341e8e9 --- modules/mesh-delivery-assigner/go.mod | 2 +- modules/mesh-delivery-assigner/go.sum | 4 ++-- modules/mesh-delivery-builder/buildrun/go.mod | 2 +- modules/mesh-delivery-builder/buildrun/go.sum | 4 ++-- modules/mesh-delivery-builder/go.mod | 2 +- modules/mesh-delivery-builder/go.sum | 4 ++-- modules/mesh-delivery-checker/go.mod | 2 +- modules/mesh-delivery-checker/go.sum | 4 ++-- modules/mesh-delivery-forge-bridge/go.mod | 2 +- modules/mesh-delivery-forge-bridge/go.sum | 4 ++-- modules/mesh-delivery-indexer/go.mod | 2 +- modules/mesh-delivery-indexer/go.sum | 6 ++---- modules/mesh-delivery-teller/go.mod | 2 +- modules/mesh-delivery-teller/go.sum | 4 ++-- modules/mesh-delivery-view/go.mod | 2 +- modules/mesh-delivery-view/go.sum | 4 ++-- modules/mesh-delivery-watchman/go.mod | 2 +- modules/mesh-delivery-watchman/go.sum | 4 ++-- modules/mesh-enrolment/go.mod | 2 +- modules/mesh-enrolment/go.sum | 4 ++-- modules/mesh-grants/go.mod | 2 +- modules/mesh-grants/go.sum | 6 ++---- 22 files changed, 33 insertions(+), 37 deletions(-) diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index 63867cb5..649a1013 100644 --- a/modules/mesh-delivery-assigner/go.mod +++ b/modules/mesh-delivery-assigner/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-assigner go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-assigner/go.sum b/modules/mesh-delivery-assigner/go.sum index 51006e69..10cdc176 100644 --- a/modules/mesh-delivery-assigner/go.sum +++ b/modules/mesh-delivery-assigner/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 h1:BdYMFwpcDLBj7xkNYvmJESgJqxG0P/ozPs1eQHZE43k= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/buildrun/go.mod b/modules/mesh-delivery-builder/buildrun/go.mod index 67030533..5a43d648 100644 --- a/modules/mesh-delivery-builder/buildrun/go.mod +++ b/modules/mesh-delivery-builder/buildrun/go.mod @@ -3,7 +3,7 @@ module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/buildrun/go.sum b/modules/mesh-delivery-builder/buildrun/go.sum index 51006e69..10cdc176 100644 --- a/modules/mesh-delivery-builder/buildrun/go.sum +++ b/modules/mesh-delivery-builder/buildrun/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 h1:BdYMFwpcDLBj7xkNYvmJESgJqxG0P/ozPs1eQHZE43k= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/go.mod b/modules/mesh-delivery-builder/go.mod index 7dd28acb..d42def97 100644 --- a/modules/mesh-delivery-builder/go.mod +++ b/modules/mesh-delivery-builder/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-builder go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/go.sum b/modules/mesh-delivery-builder/go.sum index 7c582d73..99140c9e 100644 --- a/modules/mesh-delivery-builder/go.sum +++ b/modules/mesh-delivery-builder/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 h1:BdYMFwpcDLBj7xkNYvmJESgJqxG0P/ozPs1eQHZE43k= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index c1d5f529..c484bf3d 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-checker go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-checker/go.sum b/modules/mesh-delivery-checker/go.sum index 7c582d73..99140c9e 100644 --- a/modules/mesh-delivery-checker/go.sum +++ b/modules/mesh-delivery-checker/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 h1:BdYMFwpcDLBj7xkNYvmJESgJqxG0P/ozPs1eQHZE43k= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-forge-bridge/go.mod b/modules/mesh-delivery-forge-bridge/go.mod index 4188e76d..1b302d9e 100644 --- a/modules/mesh-delivery-forge-bridge/go.mod +++ b/modules/mesh-delivery-forge-bridge/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-forge-bridge go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-forge-bridge/go.sum b/modules/mesh-delivery-forge-bridge/go.sum index 51006e69..10cdc176 100644 --- a/modules/mesh-delivery-forge-bridge/go.sum +++ b/modules/mesh-delivery-forge-bridge/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 h1:BdYMFwpcDLBj7xkNYvmJESgJqxG0P/ozPs1eQHZE43k= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-indexer/go.mod b/modules/mesh-delivery-indexer/go.mod index eba3ec65..29297f4b 100644 --- a/modules/mesh-delivery-indexer/go.mod +++ b/modules/mesh-delivery-indexer/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-indexer go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103142843-f8bd12e4b7b1 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-indexer/go.sum b/modules/mesh-delivery-indexer/go.sum index d2717419..10cdc176 100644 --- a/modules/mesh-delivery-indexer/go.sum +++ b/modules/mesh-delivery-indexer/go.sum @@ -1,7 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103142843-f8bd12e4b7b1 h1:T66tg9T/1hHg6KN/bDScHPT41IHkxLAGGO8VJiAOlb8= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103142843-f8bd12e4b7b1/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-teller/go.mod b/modules/mesh-delivery-teller/go.mod index c8fbba19..98711c70 100644 --- a/modules/mesh-delivery-teller/go.mod +++ b/modules/mesh-delivery-teller/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-teller go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-teller/go.sum b/modules/mesh-delivery-teller/go.sum index 51006e69..10cdc176 100644 --- a/modules/mesh-delivery-teller/go.sum +++ b/modules/mesh-delivery-teller/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 h1:BdYMFwpcDLBj7xkNYvmJESgJqxG0P/ozPs1eQHZE43k= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index d4a65d31..c75d1278 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-view go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index 51006e69..10cdc176 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 h1:BdYMFwpcDLBj7xkNYvmJESgJqxG0P/ozPs1eQHZE43k= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-watchman/go.mod b/modules/mesh-delivery-watchman/go.mod index a3f976e9..6fc2a937 100644 --- a/modules/mesh-delivery-watchman/go.mod +++ b/modules/mesh-delivery-watchman/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-watchman go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-watchman/go.sum b/modules/mesh-delivery-watchman/go.sum index 5d2a70e4..9bf59c43 100644 --- a/modules/mesh-delivery-watchman/go.sum +++ b/modules/mesh-delivery-watchman/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 h1:BdYMFwpcDLBj7xkNYvmJESgJqxG0P/ozPs1eQHZE43k= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-enrolment/go.mod b/modules/mesh-enrolment/go.mod index ba7f8092..5f3b3e9f 100644 --- a/modules/mesh-enrolment/go.mod +++ b/modules/mesh-enrolment/go.mod @@ -3,7 +3,7 @@ module mesh-enrolment go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-enrolment/go.sum b/modules/mesh-enrolment/go.sum index 51006e69..10cdc176 100644 --- a/modules/mesh-enrolment/go.sum +++ b/modules/mesh-enrolment/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9 h1:BdYMFwpcDLBj7xkNYvmJESgJqxG0P/ozPs1eQHZE43k= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103153231-182d8757dba9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-grants/go.mod b/modules/mesh-grants/go.mod index baf4cd76..04540ec2 100644 --- a/modules/mesh-grants/go.mod +++ b/modules/mesh-grants/go.mod @@ -3,7 +3,7 @@ module mesh-grants go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103142843-f8bd12e4b7b1 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-grants/go.sum b/modules/mesh-grants/go.sum index d2717419..10cdc176 100644 --- a/modules/mesh-grants/go.sum +++ b/modules/mesh-grants/go.sum @@ -1,7 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103142843-f8bd12e4b7b1 h1:T66tg9T/1hHg6KN/bDScHPT41IHkxLAGGO8VJiAOlb8= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103142843-f8bd12e4b7b1/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= From 3411b9157120506a1a1febe546882d219d7b85b8 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 16:56:37 +0100 Subject: [PATCH 31/49] The module graph is mesh-delivery's own: a Go module sdk/ with package graph, used by path by the indexer, the view and the checker, and the seat mesh-catalog's answers built from mesh-sdk's contract types (the operator, 2026-11-03: no graph-related code in mesh-sdk, only contracts; research 081 D11, D13) --- .../internal/check/graph_cycle.go | 8 +- .../mesh-delivery-indexer-tools/catalog.go | 16 +- .../cmd/mesh-delivery-indexer/main.go | 4 +- .../internal/index/candidates.go | 2 +- .../internal/index/catalog.go | 86 ++-- .../internal/index/catalog_test.go | 8 +- .../internal/index/graph.go | 18 +- .../internal/index/toolchains_test.go | 8 +- .../mesh-delivery-indexer/manifest_test.go | 4 +- .../cmd/mesh-delivery-view-tools/main.go | 4 +- .../tiers_rows_test.go | 12 +- .../mesh-delivery-view/internal/view/read.go | 2 +- .../mesh-delivery-view/internal/view/tiers.go | 18 +- sdk/go.mod | 5 + sdk/go.sum | 14 + sdk/graph/graph.go | 398 ++++++++++++++++++ sdk/graph/graph_test.go | 174 ++++++++ 17 files changed, 685 insertions(+), 96 deletions(-) create mode 100644 sdk/go.mod create mode 100644 sdk/go.sum create mode 100644 sdk/graph/graph.go create mode 100644 sdk/graph/graph_test.go diff --git a/modules/mesh-delivery-checker/internal/check/graph_cycle.go b/modules/mesh-delivery-checker/internal/check/graph_cycle.go index 0490f9a9..673ff548 100644 --- a/modules/mesh-delivery-checker/internal/check/graph_cycle.go +++ b/modules/mesh-delivery-checker/internal/check/graph_cycle.go @@ -10,6 +10,8 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/composer" "git.novox.be/novox/mesh-sdk/go/delivery" + + "git.novox.be/novox/mesh-delivery/sdk/graph" ) // The check refuses a change whose module graph has a cycle and names it (novox/hq ADR 0603 §2): modules that would each @@ -49,15 +51,15 @@ func (st *state) refuseCycle(ctx context.Context) *delivery.Verdict { if err != nil { return fail(goModule, "the change's module graph cannot be worked out, so no cycle can be ruled out: "+err.Error()) } - cycle := delivery.ManifestGraph(change).Cycle() + cycle := graph.OfManifests(change).Cycle() if cycle == nil { st.c.note("the change's module graph has no cycle") return nil } words := strings.Join(cycle, " -> ") if before, err := read(req.Cases[1].Manifests); err == nil { - among := delivery.Graph{} - for name, n := range delivery.ManifestGraph(before) { + among := graph.Graph{} + for name, n := range graph.OfManifests(before) { if slices.Contains(cycle, name) { among[name] = n } diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/catalog.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/catalog.go index 24411bac..62df17fb 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/catalog.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/catalog.go @@ -7,6 +7,8 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/graph" + "mesh-delivery-indexer/internal/index" ) @@ -35,7 +37,7 @@ func CatalogTools(c index.CatalogRead) []stdio.Tool { } // catalogRead reads the indexer's modules, manifests and graph states for the seat's verbs. -func catalogRead(modules, manifests, graph stdio.KeptState) index.CatalogRead { +func catalogRead(modules, manifests, nodes stdio.KeptState) index.CatalogRead { return index.CatalogRead{ Modules: func() (map[string]delivery.Module, error) { keys, err := modules.Keys() @@ -72,21 +74,21 @@ func catalogRead(modules, manifests, graph stdio.KeptState) index.CatalogRead { kept, err := delivery.DecodeManifest(e.Value) return kept.JSON, err }, - Graph: func() (delivery.Graph, error) { - keys, err := graph.Keys() + Graph: func() (graph.Graph, error) { + keys, err := nodes.Keys() if err != nil { return nil, err } - g := delivery.Graph{} + g := graph.Graph{} for _, k := range keys { - e, err := graph.Get(k) + e, err := nodes.Get(k) if err != nil { return nil, fmt.Errorf("graph.%s: %w", k, err) } if e == nil { continue } - if n, err := delivery.DecodeGraphNode(e.Value); err == nil { + if n, err := graph.Decode(e.Value); err == nil { g[k] = n } } @@ -97,5 +99,5 @@ func catalogRead(modules, manifests, graph stdio.KeptState) index.CatalogRead { // catalogTools are the seat's verbs over the indexer's own states, as main serves them once the indexer claims the seat. func catalogTools() []stdio.Tool { - return CatalogTools(catalogRead(stdio.State("modules"), stdio.State("manifests"), stdio.State(delivery.GraphState))) + return CatalogTools(catalogRead(stdio.State("modules"), stdio.State("manifests"), stdio.State(graph.State))) } diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go index eb111122..0d896889 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go @@ -26,6 +26,8 @@ import ( "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/delivery" + + "git.novox.be/novox/mesh-delivery/sdk/graph" "git.novox.be/novox/mesh-sdk/go/process" "mesh-delivery-indexer/internal/index" @@ -300,7 +302,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // the judge refuses no use of an old section. oldSections := process.AwaitBucket(ctx, js, conn, index.OldSectionsBucket, "the count of the old sections is not kept, so the judge refuses no use of one", logger.Printf) // The module graph (ADR 0603 §2): a state this delivery brings, bound when it appears; until then no graph is written. - graph := process.AwaitBucket(ctx, js, conn, delivery.GraphBucket, "no module graph is written", logger.Printf) + graph := process.AwaitBucket(ctx, js, conn, graph.Bucket, "no module graph is written", logger.Printf) ix := &index.Indexer{OldSections: awaitedStore{oldSections}, Store: kvStore{candidates}, Modules: kvStore{modules}, Manifests: kvStore{manifests}, Taken: awaitedStore{taken}, Builds: known, Verdicts: awaitedStore{verdicts}, SayRefused: sayRefused(js, cred, logger.Printf), SayAccepted: sayAccepted(js, cred, logger.Printf), Predefined: awaitedStore{predefined}, ConfigCandidates: awaitedStore{configCandidates}, Logf: logger.Printf, diff --git a/modules/mesh-delivery-indexer/internal/index/candidates.go b/modules/mesh-delivery-indexer/internal/index/candidates.go index e0cba4fe..b1620ac0 100644 --- a/modules/mesh-delivery-indexer/internal/index/candidates.go +++ b/modules/mesh-delivery-indexer/internal/index/candidates.go @@ -67,7 +67,7 @@ type Indexer struct { // Packages is the packages state (delivery.PackagesBucket, packages.go; novox/hq ADR 0593 §2); nil publishes none, // and every fingerprint takes the version line. Packages Store - // Graph is the module graph's state (delivery.GraphBucket, graph.go; novox/hq ADR 0603 §2); nil writes no graph. + // Graph is the module graph's state (graph.Bucket, graph.go; novox/hq ADR 0603 §2); nil writes no graph. Graph Store // Files asks the forge for every blob under each directory at a commit, by directory then path relative to it, each // with its git object id (gitea.gitea_commit_trees with files): the listing a package's digest is computed from. diff --git a/modules/mesh-delivery-indexer/internal/index/catalog.go b/modules/mesh-delivery-indexer/internal/index/catalog.go index 0d5c30ce..33807d48 100644 --- a/modules/mesh-delivery-indexer/internal/index/catalog.go +++ b/modules/mesh-delivery-indexer/internal/index/catalog.go @@ -9,13 +9,15 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" + + "git.novox.be/novox/mesh-delivery/sdk/graph" ) // The seat mesh-catalog's verbs, answered from the indexer's own states (novox/hq ADR 0604 §1, §2): modules, module, // provides and dependents read the modules state, the manifests state and the module graph, and write nothing. The // indexer serves no request subject (ADR 0437 §4): its tools bundle serves them as .. Each answer keeps the // field names of the old catalog module's tool it replaces (catalog_modules, catalog_module, catalog_provides, -// catalog_dependents), so a caller that moves to the seat reads the same fields. +// catalog_dependents): the answers are the seat's contract (catalogue.CatalogModules and the rest), built here. // CatalogRead reads the states the seat's verbs answer from. type CatalogRead struct { @@ -24,99 +26,75 @@ type CatalogRead struct { // Manifest is the manifest kept under a digest in the manifests state, as its JSON. Manifest func(digest string) (string, error) // Graph is the module graph, by module. - Graph func() (delivery.Graph, error) + Graph func() (graph.Graph, error) } -// CatalogEntry is one module of the catalog: its repository, its directory there (delivery.RootPath for the root), the -// merge commit it was last changed at, and its 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. -type CatalogEnd struct { - Module string `json:"module"` - Commit string `json:"commit"` - Scope string `json:"scope,omitempty"` -} - -func entryOf(name string, m delivery.Module) CatalogEntry { - return CatalogEntry{Module: name, Repository: m.Repository, Path: m.Path, Commit: m.Commit, FP: m.FP2, None: m.None2} +func entryOf(name string, m delivery.Module) catalogue.CatalogEntry { + return catalogue.CatalogEntry{Module: name, Repository: m.Repository, Path: m.Path, Commit: m.Commit, FP: m.FP2, None: m.None2} } // CatalogModulesAnswer is the verb modules: every module the modules state holds, by name. -func (c CatalogRead) CatalogModulesAnswer() (map[string]any, error) { +func (c CatalogRead) CatalogModulesAnswer() (catalogue.CatalogModules, error) { mods, err := c.Modules() if err != nil { - return nil, fmt.Errorf("the indexer's modules cannot be read: %w", err) + return catalogue.CatalogModules{}, fmt.Errorf("the indexer's modules cannot be read: %w", err) } - out := []CatalogEntry{} + out := []catalogue.CatalogEntry{} for _, name := range slices.Sorted(maps.Keys(mods)) { out = append(out, entryOf(name, mods[name])) } - return map[string]any{"modules": out}, nil + return catalogue.CatalogModules{Modules: out}, nil } // CatalogModuleAnswer is the verb module: one module whole, its manifest included. -func (c CatalogRead) CatalogModuleAnswer(name string) (map[string]any, error) { +func (c CatalogRead) CatalogModuleAnswer(name string) (catalogue.CatalogOne, error) { name = strings.TrimSpace(name) if name == "" { - return nil, fmt.Errorf("name the module") + return catalogue.CatalogOne{}, fmt.Errorf("name the module") } mods, err := c.Modules() if err != nil { - return nil, fmt.Errorf("the indexer's modules cannot be read: %w", err) + return catalogue.CatalogOne{}, fmt.Errorf("the indexer's modules cannot be read: %w", err) } m, ok := mods[name] if !ok { - return nil, fmt.Errorf("the trunk holds no module %s", name) + return catalogue.CatalogOne{}, fmt.Errorf("the trunk holds no module %s", name) } - one := CatalogModule{CatalogEntry: entryOf(name, m)} + one := catalogue.CatalogModule{CatalogEntry: entryOf(name, m)} if m.Manifest == "" { - return nil, fmt.Errorf("the indexer keeps no manifest of %s", name) + return catalogue.CatalogOne{}, fmt.Errorf("the indexer keeps no manifest of %s", name) } raw, err := c.Manifest(m.Manifest) if err != nil { - return nil, fmt.Errorf("the manifest %s of %s cannot be read: %w", m.Manifest, name, err) + return catalogue.CatalogOne{}, fmt.Errorf("the manifest %s of %s cannot be read: %w", m.Manifest, name, err) } one.Manifest = json.RawMessage(raw) - return map[string]any{"module": one}, nil + return catalogue.CatalogOne{Module: one}, nil } // CatalogProvidesAnswer is the verb provides: the modules that provide a provision, each with its scope (a module provides // its own name, at node scope), and the modules that require it, from the module graph's provision edges. -func (c CatalogRead) CatalogProvidesAnswer(provision string) (map[string]any, error) { +func (c CatalogRead) CatalogProvidesAnswer(provision string) (catalogue.CatalogProvides, error) { provision = strings.TrimSpace(provision) if provision == "" { - return nil, fmt.Errorf("name the provision") + return catalogue.CatalogProvides{}, fmt.Errorf("name the provision") } g, mods, err := c.graphAndModules() if err != nil { - return nil, err + return catalogue.CatalogProvides{}, err } - provides, requires := []CatalogEnd{}, []CatalogEnd{} + provides, requires := []catalogue.CatalogEnd{}, []catalogue.CatalogEnd{} for _, name := range g.Providers(provision) { - end := CatalogEnd{Module: name, Commit: mods[name].Commit, Scope: catalogue.ScopeNode} + end := catalogue.CatalogEnd{Module: name, Commit: mods[name].Commit, Scope: catalogue.ScopeNode} if name != provision { end.Scope = c.scopeOf(mods[name], provision) } provides = append(provides, end) } for _, name := range g.Requirers(provision) { - requires = append(requires, CatalogEnd{Module: name, Commit: mods[name].Commit}) + requires = append(requires, catalogue.CatalogEnd{Module: name, Commit: mods[name].Commit}) } - return map[string]any{"provision": provision, "provides": provides, "requires": requires}, nil + return catalogue.CatalogProvides{Provision: provision, Provides: provides, Requires: requires}, nil } // scopeOf is the scope the module's manifest offers provision at, "" when its manifest cannot be read. @@ -142,23 +120,23 @@ func (c CatalogRead) scopeOf(m delivery.Module, provision string) string { // CatalogDependentsAnswer is the verb dependents: the modules whose build edges reach the module, directly or through // others, from the module graph. -func (c CatalogRead) CatalogDependentsAnswer(module string) (map[string]any, error) { +func (c CatalogRead) CatalogDependentsAnswer(module string) (catalogue.CatalogDependents, error) { module = strings.TrimSpace(module) if module == "" { - return nil, fmt.Errorf("name the module") + return catalogue.CatalogDependents{}, fmt.Errorf("name the module") } g, mods, err := c.graphAndModules() if err != nil { - return nil, err + return catalogue.CatalogDependents{}, err } - out := []CatalogEnd{} + out := []catalogue.CatalogEnd{} for _, name := range g.Dependents(module) { - out = append(out, CatalogEnd{Module: name, Commit: mods[name].Commit}) + out = append(out, catalogue.CatalogEnd{Module: name, Commit: mods[name].Commit}) } - return map[string]any{"module": module, "dependents": out}, nil + return catalogue.CatalogDependents{Module: module, Dependents: out}, nil } -func (c CatalogRead) graphAndModules() (delivery.Graph, map[string]delivery.Module, error) { +func (c CatalogRead) graphAndModules() (graph.Graph, map[string]delivery.Module, error) { g, err := c.Graph() if err != nil { return nil, nil, fmt.Errorf("the module graph cannot be read: %w", err) diff --git a/modules/mesh-delivery-indexer/internal/index/catalog_test.go b/modules/mesh-delivery-indexer/internal/index/catalog_test.go index 6cec8c2a..03c0b68c 100644 --- a/modules/mesh-delivery-indexer/internal/index/catalog_test.go +++ b/modules/mesh-delivery-indexer/internal/index/catalog_test.go @@ -11,6 +11,8 @@ import ( "testing" "git.novox.be/novox/mesh-sdk/go/delivery" + + "git.novox.be/novox/mesh-delivery/sdk/graph" ) // capturedKeys are the field names of the first object at path (a list's first element when the value is a list) in the @@ -67,15 +69,15 @@ func catalogVerbsAnswerFromTheStates(t *testing.T) { raw, _, err := ix.manifestRaw(context.Background(), digest) return raw, err }, - Graph: func() (delivery.Graph, error) { - g := delivery.Graph{} + Graph: func() (graph.Graph, error) { + g := graph.Graph{} for k := range ix.Graph.(*memStore).vals { g[k] = graphOf(t, ix, k) } return g, nil }, } - answer := func(v map[string]any, err error) []byte { + answer := func(v any, err error) []byte { t.Helper() if err != nil { t.Fatal(err) diff --git a/modules/mesh-delivery-indexer/internal/index/graph.go b/modules/mesh-delivery-indexer/internal/index/graph.go index deaac724..f9195d3b 100644 --- a/modules/mesh-delivery-indexer/internal/index/graph.go +++ b/modules/mesh-delivery-indexer/internal/index/graph.go @@ -8,10 +8,12 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" + + "git.novox.be/novox/mesh-delivery/sdk/graph" ) // The module graph (novox/hq ADR 0603 §2): at every recompute the indexer writes, in its state graph -// (delivery.GraphBucket), one key per module it holds the inputs of, its build and provision edges as that recompute +// (graph.Bucket), one key per module it holds the inputs of, its build and provision edges as that recompute // computed them, and deletes the key of a module the modules state no longer holds. A key is written only when its value // changes, so its history of 5 is five changes of the module's edges. The delivery view, the checker and the seat // mesh-catalog's verbs read it; nothing stores an order (ADR 0603 §1). @@ -33,11 +35,11 @@ type graphEdges struct { // graphNode is the graph node of e: its edges, the awaited provisions judged against providers (catalogue.Awaited, as // the assigner judges them). -func graphNode(e graphEdges, providers func(string) []catalogue.Manifest) delivery.GraphNode { +func graphNode(e graphEdges, providers func(string) []catalogue.Manifest) graph.Node { if e.fp == "" && e.none == "" { e.none = "its fingerprint was not computed" } - n := delivery.GraphEdgesOf(e.manifest, providers) + n := graph.EdgesOf(e.manifest, providers) n.FP, n.None, n.Repository, n.Path = e.fp, e.none, e.cur.Repository, e.cur.Path n.Packages = slices.Sorted(maps.Keys(e.pkgs)) tcs := e.tcs @@ -50,9 +52,9 @@ func graphNode(e graphEdges, providers func(string) []catalogue.Manifest) delive continue } if n.Toolchains == nil { - n.Toolchains = map[string]delivery.GraphToolchain{} + n.Toolchains = map[string]graph.Toolchain{} } - n.Toolchains[language] = delivery.GraphToolchain{Module: builder, Version: t.Version, Recipe: t.Recipe} + n.Toolchains[language] = graph.Toolchain{Module: builder, Version: t.Version, Recipe: t.Recipe} } return n } @@ -79,7 +81,7 @@ func (ix *Indexer) writeGraph(ctx context.Context, edges map[string]graphEdges) return out } for _, name := range names { - key, err := delivery.GraphKey(name) + key, err := graph.Key(name) if err != nil { ix.logf("graph.%s is not written: %v", name, err) continue @@ -87,11 +89,11 @@ func (ix *Indexer) writeGraph(ctx context.Context, edges map[string]graphEdges) next := graphNode(edges[name], providers) err = write(ctx, ix.Graph, key, func(value []byte, found bool) ([]byte, bool, error) { if found { - if cur, err := delivery.DecodeGraphNode(value); err == nil && delivery.SameGraphNode(cur, next) { + if cur, err := graph.Decode(value); err == nil && graph.Same(cur, next) { return nil, false, nil } } - data, err := delivery.EncodeGraphNode(next) + data, err := graph.Encode(next) if err != nil { return nil, false, fmt.Errorf("graph.%s: %w", key, err) } diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go index b9bd4efc..0fbe30d6 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go @@ -9,6 +9,8 @@ import ( "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/graph" + "mesh-delivery-indexer/internal/fingerprint" ) @@ -61,13 +63,13 @@ type mod struct { // merged applies one merge of the catalogue carrying these modules (every one a Go bundle's go.sum, a TypeScript bundle's // package-lock.json) and returns the indexer. // graphOf is the graph node the indexer holds of module. -func graphOf(t *testing.T, ix *Indexer, module string) delivery.GraphNode { +func graphOf(t *testing.T, ix *Indexer, module string) graph.Node { t.Helper() v, ok := ix.Graph.(*memStore).vals[module] if !ok { t.Fatalf("graph.%s is not held", module) } - g, err := delivery.DecodeGraphNode(v) + g, err := graph.Decode(v) if err != nil { t.Fatal(err) } @@ -232,7 +234,7 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { // t.Errorf("other: fp2 = %q none2 = %q, want none2 naming the base ts-base", m.FP2, m.None2) } // L3 (ADR 0603 §2): the module graph holds each module's edges as this recompute computed them. - goTool := map[string]delivery.GraphToolchain{"go": {Module: "mesh-build-go", Version: "go1.27.1", Recipe: "1"}} + goTool := map[string]graph.Toolchain{"go": {Module: "mesh-build-go", Version: "go1.27.1", Recipe: "1"}} if g := graphOf(t, ix, "avahi"); g.FP != want["avahi"] || !reflect.DeepEqual(g.Bases, []string{"mesh-tools"}) || !reflect.DeepEqual(g.Toolchains, goTool) || g.Repository != catalogueRepo || g.Path != "modules/avahi" { t.Errorf("graph.avahi = %+v", g) diff --git a/modules/mesh-delivery-indexer/manifest_test.go b/modules/mesh-delivery-indexer/manifest_test.go index c77c7b80..7569b44c 100644 --- a/modules/mesh-delivery-indexer/manifest_test.go +++ b/modules/mesh-delivery-indexer/manifest_test.go @@ -13,6 +13,8 @@ import ( "testing" "git.novox.be/novox/mesh-sdk/go/delivery" + + "git.novox.be/novox/mesh-delivery/sdk/graph" ) // The controller's manifest reader is mesh-controller internal/catalogue (Manifest, manifest.go: Consumes :521, @@ -91,7 +93,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build } // 1126 note 76: heard keeps the last 64 FORGE messages the indexer handled, for indexer_heard. if want := []state{{Name: "candidates", History: 10}, {Name: "heard", History: 64}, {Name: "manifests", History: 1}, {Name: "modules", History: 20}, {Name: "predefined", History: 5}, {Name: "config-candidates", History: 5}, {Name: "taken", History: 1}, {Name: "takeins", History: 1}, {Name: "verdicts", History: 5}, {Name: "oldsections", History: 1}, {Name: delivery.PackagesState, History: delivery.PackagesHistory}, - {Name: delivery.GraphState, History: delivery.GraphHistory}}; !reflect.DeepEqual(m.State, want) { + {Name: graph.State, History: graph.History}}; !reflect.DeepEqual(m.State, want) { t.Errorf("state = %+v, want %+v", m.State, want) } if want := []string{"mesh-build-go.toolchains", "mesh-build-node.toolchains", "mesh-delivery-assigner.placement", "mesh-delivery-builder.build-log", "mesh-delivery-checker.previews", "mesh-delivery-gate.source"}; !reflect.DeepEqual(sorted(m.Reads), want) { diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go index 69e67c73..d147a802 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go @@ -13,6 +13,8 @@ import ( "mesh-delivery-view/internal/view" "git.novox.be/novox/mesh-sdk/go/delivery" + + "git.novox.be/novox/mesh-delivery/sdk/graph" "git.novox.be/novox/mesh-sdk/go/provisions" stdio "git.novox.be/novox/mesh-sdk/go" @@ -318,7 +320,7 @@ func main() { PredefinedState: read(stdio.State(delivery.IndexerModule + "." + delivery.IndexerPredefinedState)), ConfigCandidatesState: read(stdio.State(delivery.IndexerModule + "." + delivery.IndexerConfigCandidatesState)), PackagesState: read(stdio.State(delivery.IndexerModule + "." + delivery.PackagesState)), - GraphState: read(stdio.State(delivery.GraphRead)), SourceState: read(stdio.State(delivery.GateModule + ".source")), + GraphState: read(stdio.State(graph.Read)), SourceState: read(stdio.State(delivery.GateModule + ".source")), ProvisionsState: read(stdio.State(provisions.ProvisionsRead)), BuildLog: stdio.Log(delivery.BuildLogRead).Since} u := view.UsageReader{Haves: r.HavesState, Resolved: read(stdio.State("mesh-config.resolved")), Tries: read(stdio.State("mesh-delivery-checker.tries")), diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go index 815e1407..6236315b 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go @@ -10,6 +10,8 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/delivery" + + "git.novox.be/novox/mesh-delivery/sdk/graph" "git.novox.be/novox/mesh-sdk/go/provisions" "mesh-delivery-view/internal/view" @@ -34,8 +36,8 @@ func tiersToolSaysEachModulesWait(t *testing.T) { b, err := delivery.EncodeModule(delivery.Module{FP2: f, Manifest: manifest, Src: src, FPSrc: src, Repository: "novox/mesh-catalog", Commit: "c"}) return encoded(t, b, err) } - node := func(g delivery.GraphNode) string { - b, err := delivery.EncodeGraphNode(g) + node := func(g graph.Node) string { + b, err := graph.Encode(g) return encoded(t, b, err) } build := func(b delivery.BuilderBuild) string { @@ -59,9 +61,9 @@ func tiersToolSaysEachModulesWait(t *testing.T) { since := time.Date(2026, 11, 3, 16, 40, 0, 0, time.UTC) r := fake{"db": mod(fp("a"), 9), "web": mod(fp("b"), 9), "lib": mod(fp("c"), 9), "old": mod(fp("e"), 5)}.reader() r.GraphState = fake{ - "db": node(delivery.GraphNode{FP: fp("a"), Repository: "novox/mesh-catalog", Provides: []string{"database"}}), - "web": node(delivery.GraphNode{FP: fp("b"), Repository: "novox/mesh-catalog", Requires: []string{"database"}, Awaits: []string{"database"}}), - "lib": node(delivery.GraphNode{FP: fp("c"), Repository: "novox/mesh-catalog"}), + "db": node(graph.Node{FP: fp("a"), Repository: "novox/mesh-catalog", Provides: []string{"database"}}), + "web": node(graph.Node{FP: fp("b"), Repository: "novox/mesh-catalog", Requires: []string{"database"}, Awaits: []string{"database"}}), + "lib": node(graph.Node{FP: fp("c"), Repository: "novox/mesh-catalog"}), }.reader().State r.Builds = fake{ key(delivery.BuilderBuildKey("db", fp("a"))): build(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: manifest}), diff --git a/modules/mesh-delivery-view/internal/view/read.go b/modules/mesh-delivery-view/internal/view/read.go index 89d80210..5da2b0bb 100644 --- a/modules/mesh-delivery-view/internal/view/read.go +++ b/modules/mesh-delivery-view/internal/view/read.go @@ -55,7 +55,7 @@ type Reader struct { PredefinedState, ConfigCandidatesState StateRead // PackagesState is the indexer's published packages (delivery.PackagesBucket, ADR 0593 §2); unset reads none. PackagesState StateRead - // GraphState is the indexer's module graph (delivery.GraphBucket, ADR 0603 §2), SourceState the gate's source and + // GraphState is the indexer's module graph (graph.Bucket, ADR 0603 §2), SourceState the gate's source and // ProvisionsState the provisioner's keys: what view_tiers reads (tiers.go). Unset reads none, said in the answer. GraphState, SourceState, ProvisionsState StateRead // BuildLog reads the builder's log from a time (stdio.Log(delivery.BuildLogRead).Since); unset reads none. diff --git a/modules/mesh-delivery-view/internal/view/tiers.go b/modules/mesh-delivery-view/internal/view/tiers.go index 6289d96f..8055ad38 100644 --- a/modules/mesh-delivery-view/internal/view/tiers.go +++ b/modules/mesh-delivery-view/internal/view/tiers.go @@ -10,6 +10,8 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/delivery" + + "git.novox.be/novox/mesh-delivery/sdk/graph" "git.novox.be/novox/mesh-sdk/go/provisions" ) @@ -93,13 +95,13 @@ func (r Reader) Tiers(revision uint64) (Tiers, error) { len(names), TiersMost, names[TiersMost], len(names)-TiersMost-1) names = names[:TiersMost] } - graph := delivery.Graph{} + gr := graph.Graph{} if raws, err := values(r.GraphState); err != nil { out.Graph = "the module graph cannot be read, so no module is ordered after another: " + err.Error() } else { for key, raw := range raws { - if n, err := delivery.DecodeGraphNode(raw); err == nil { - graph[key] = n + if n, err := graph.Decode(raw); err == nil { + gr[key] = n } } } @@ -115,15 +117,15 @@ func (r Reader) Tiers(revision uint64) (Tiers, error) { } after := map[string][]string{} for _, n := range names { - for _, b := range graph.Before(n) { + for _, b := range gr.Before(n) { if in[b] { after[n] = append(after[n], b) } } } - sub := delivery.Graph{} + sub := graph.Graph{} for _, n := range names { - if g, ok := graph[n]; ok { + if g, ok := gr[n]; ok { sub[n] = g } } @@ -168,7 +170,7 @@ func (r Reader) Tiers(revision uint64) (Tiers, error) { } m := moved[n] tm := TierModule{Module: n, FP: m.FP2, None: m.None2, After: after[n]} - tm.Waits = r.waitsOf(n, m, graph[n], wants, provided, perr, away) + tm.Waits = r.waitsOf(n, m, gr[n], wants, provided, perr, away) out.Tiers[t] = append(out.Tiers[t], tm) } compact := out.Tiers[:0] @@ -202,7 +204,7 @@ func (r Reader) away() map[string]bool { // waitsOf is what module m waits on now: its build at its fingerprint, then on each node present that wants it, its want, // its have, and each provision its graph node awaits. -func (r Reader) waitsOf(module string, m delivery.Module, g delivery.GraphNode, wants, provided map[string]json.RawMessage, +func (r Reader) waitsOf(module string, m delivery.Module, g graph.Node, wants, provided map[string]json.RawMessage, perr error, away map[string]bool) []TierWait { var out []TierWait if m.FP2 == "" { diff --git a/sdk/go.mod b/sdk/go.mod new file mode 100644 index 00000000..31746712 --- /dev/null +++ b/sdk/go.mod @@ -0,0 +1,5 @@ +module git.novox.be/novox/mesh-delivery/sdk + +go 1.26.0 + +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b diff --git a/sdk/go.sum b/sdk/go.sum new file mode 100644 index 00000000..cea3187b --- /dev/null +++ b/sdk/go.sum @@ -0,0 +1,14 @@ +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= +github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/sdk/graph/graph.go b/sdk/graph/graph.go new file mode 100644 index 00000000..6648ced8 --- /dev/null +++ b/sdk/graph/graph.go @@ -0,0 +1,398 @@ +package graph + +import ( + "encoding/json" + "errors" + "fmt" + "maps" + "reflect" + "slices" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// Version is the version of a node's value this package writes and the only one it reads. +const Version = 1 + +// ErrUnknownVersion is what Decode returns for a value whose version is missing or not Version. +var ErrUnknownVersion = errors.New("graph: unknown value version") + +func decode(data []byte, into *Node) error { + var probe struct { + V *int `json:"v"` + } + if err := json.Unmarshal(data, &probe); err != nil { + return fmt.Errorf("graph: value does not fit its shape: %w", err) + } + if probe.V == nil || *probe.V != Version { + return fmt.Errorf("%w: this reader reads %d", ErrUnknownVersion, Version) + } + if err := json.Unmarshal(data, into); err != nil { + return fmt.Errorf("graph: value does not fit its shape: %w", err) + } + return nil +} + +// checkName refuses a module name a key cannot hold (delivery.ModulesKey's rule). +func checkName(kind, name string) error { + if _, err := delivery.ModulesKey(name); err != nil { + return fmt.Errorf("graph: the %s name %q: %w", kind, name, err) + } + return nil +} + +// checkRepository refuses a repository that is not /. +func checkRepository(repository string) error { + owner, name, ok := strings.Cut(repository, "/") + if !ok || owner == "" || name == "" || strings.Contains(name, "/") { + return fmt.Errorf("graph: the repository %q is not /", repository) + } + return nil +} + +// The module graph (novox/hq ADR 0603 §2, ADR 0604 §1): the indexer keeps, as its state `graph`, one key per module it +// holds the inputs of, written at every recompute and deleted with the module. A key holds the module's build edges +// (its bases, the mesh packages its fingerprint takes, and per language the build module, toolchain version and recipe +// version its fingerprint names, ADR 0530 §1) and its provision edges (what its manifest provides, requires, and of what +// it requires what a want of it waits for). Nothing stores an order: a reader works it out from the edges (ADR 0603 +// §1, §6). The indexer writes it and works the edges out (EdgesOf), the delivery view, the checker and the indexer's +// answers of the seat mesh-catalog read it; all three are in this repository, which uses this package by path (the +// operator, 2026-11-03: no graph-related code in mesh-sdk, only contracts). It names no module. + +const ( + // State is the name of the indexer's state of the module graph, as its manifest declares it, and Bucket its + // bucket: one key per module (Key). + State = "graph" + Bucket = delivery.IndexerModule + "_" + State + // Read is the line a reader's manifest names in `reads` to read the graph. + Read = delivery.IndexerModule + "." + State + // History is the revisions the graph state keeps per key (ADR 0603 §2). + History = 5 +) + +// Key is the key, in Bucket, of a module: ``, as ModulesKey. +func Key(module string) (string, error) { return delivery.ModulesKey(module) } + +// ParseKey is the inverse of Key. +func ParseKey(key string) (string, error) { + module, err := Key(key) + if err != nil { + return "", fmt.Errorf("graph: %q is not a graph key: %w", key, err) + } + return module, nil +} + +// Node is the value of a graph key: one module's edges as its last recompute computed them. FP is its new-form +// fingerprint then, or None why it has none (exactly one is set); Repository and Path say where it is (as Module), so a +// package edge is resolved to the module that publishes it (GraphPublisher). Bases are the modules it is built on; +// Packages the mesh packages (Go module paths) its fingerprint took (Module.Packages); Toolchains, by language, the +// build module and the toolchain it was computed with. Provides are the provisions its manifest offers (its own name is +// provided too and is not listed), Requires what it requires and does not require optionally, and Awaits those of +// Requires a want of it waits for (catalogue.Awaited, ADR 0570 §4): the provision edges that order a source revision. +// Every list is sorted and each entry once; an encode sorts them. +type Node struct { + V int `json:"v"` + FP string `json:"fp,omitempty"` + None string `json:"none,omitempty"` + Repository string `json:"repository"` + Path string `json:"path,omitempty"` + Bases []string `json:"bases,omitempty"` + Packages []string `json:"packages,omitempty"` + Toolchains map[string]Toolchain `json:"toolchains,omitempty"` + Provides []string `json:"provides,omitempty"` + Requires []string `json:"requires,omitempty"` + Awaits []string `json:"awaits,omitempty"` +} + +// Toolchain is a build edge of one language: the build module that builds it (Module) and the toolchain version +// and recipe version its fingerprint names. +type Toolchain struct { + Module string `json:"module"` + Version string `json:"version"` + Recipe string `json:"recipe"` +} + +func (g Node) check() error { + switch { + case (g.FP == "") == (g.None == ""): + return fmt.Errorf("graph: a graph node has either a fingerprint or a reason for none, not both or neither") + case g.FP != "" && !strings.HasPrefix(g.FP, delivery.SourcePrefix2): + return fmt.Errorf("graph: the graph node's fingerprint %q does not start with %s", g.FP, delivery.SourcePrefix2) + } + if err := checkRepository(g.Repository); err != nil { + return err + } + for _, b := range g.Bases { + if err := checkName("base", b); err != nil { + return err + } + } + for _, p := range g.Packages { + if _, _, ok := delivery.PackageRepository(p); !ok { + return fmt.Errorf("graph: the graph node's package %q is no mesh package's path", p) + } + } + for language, t := range g.Toolchains { + if err := checkName("build module", t.Module); err != nil { + return fmt.Errorf("graph: the graph node's toolchain of %q: %w", language, err) + } + if language == "" || t.Version == "" || t.Recipe == "" { + return fmt.Errorf("graph: the graph node's toolchain of %q needs its language, version and recipe version", language) + } + } + for _, list := range [][]string{g.Provides, g.Requires, g.Awaits} { + if slices.Contains(list, "") { + return fmt.Errorf("graph: a graph node names an empty provision") + } + } + for _, a := range g.Awaits { + if !slices.Contains(g.Requires, a) { + return fmt.Errorf("graph: the graph node awaits %q, which it does not require", a) + } + } + return nil +} + +func sortedOnce(in []string) []string { + if len(in) == 0 { + return nil + } + out := slices.Clone(in) + slices.Sort(out) + return slices.Compact(out) +} + +func (g Node) normal() Node { + g.Bases, g.Packages = sortedOnce(g.Bases), sortedOnce(g.Packages) + g.Provides, g.Requires, g.Awaits = sortedOnce(g.Provides), sortedOnce(g.Requires), sortedOnce(g.Awaits) + if len(g.Toolchains) == 0 { + g.Toolchains = nil + } + return g +} + +// Encode is g as JSON at Version, its lists sorted; it refuses what Node's contract does not hold. +func Encode(g Node) ([]byte, error) { + g = g.normal() + if err := g.check(); err != nil { + return nil, err + } + g.V = Version + return json.Marshal(g) +} + +// Decode reads a graph node; it refuses an unknown version (ErrUnknownVersion) and what Encode refuses. +func Decode(data []byte) (Node, error) { + var g Node + if err := decode(data, &g); err != nil { + return Node{}, err + } + if err := g.check(); err != nil { + return Node{}, err + } + return g.normal(), nil +} + +// Same says whether two graph nodes are the same value once sorted, so a writer writes only when they differ. +func Same(a, b Node) bool { + a, b = a.normal(), b.normal() + a.V, b.V = 0, 0 + return reflect.DeepEqual(a, b) +} + +// Graph is the module graph as a reader holds it: every graph node, by module. +type Graph map[string]Node + +// Providers are the modules that provide provision: each whose Provides names it, and the module of that name (a module +// provides its own name), sorted. +func (g Graph) Providers(provision string) []string { + var out []string + for name, n := range g { + if name == provision || slices.Contains(n.Provides, provision) { + out = append(out, name) + } + } + slices.Sort(out) + return out +} + +// Requirers are the modules that require provision, not optionally, sorted. +func (g Graph) Requirers(provision string) []string { + var out []string + for name, n := range g { + if slices.Contains(n.Requires, provision) { + out = append(out, name) + } + } + slices.Sort(out) + return out +} + +// Publisher is the module of the graph whose repository and directory publish the mesh package pkg (PackageRepository), +// or "" when the graph holds none. +func (g Graph) Publisher(pkg string) string { + repository, dir, ok := delivery.PackageRepository(pkg) + if !ok { + return "" + } + if dir == "" { + dir = delivery.RootPath + } + for _, name := range slices.Sorted(maps.Keys(g)) { + if n := g[name]; n.Repository == repository && n.Path == dir { + return name + } + } + return "" +} + +// buildEdges are the modules n's build edges reach: its bases, the publisher of each of its packages, and the build +// module of each language. +func (g Graph) buildEdges(n Node) []string { + out := slices.Clone(n.Bases) + for _, p := range n.Packages { + if m := g.Publisher(p); m != "" { + out = append(out, m) + } + } + for _, t := range n.Toolchains { + out = append(out, t.Module) + } + return sortedOnce(out) +} + +// Dependents are the modules whose build edges reach module, directly or through other modules' (a base of a base, a +// module built by a build module that stands on module), sorted and without module itself (ADR 0604 §1): what a +// change of module builds again. +func (g Graph) Dependents(module string) []string { + reverse := map[string][]string{} + for _, name := range slices.Sorted(maps.Keys(g)) { + for _, to := range g.buildEdges(g[name]) { + reverse[to] = append(reverse[to], name) + } + } + seen := map[string]bool{module: true} + queue := []string{module} + var out []string + for len(queue) > 0 { + at := queue[0] + queue = queue[1:] + for _, from := range reverse[at] { + if !seen[from] { + seen[from] = true + out = append(out, from) + queue = append(queue, from) + } + } + } + slices.Sort(out) + return out +} + +// Before are the modules name waits for, by the graph's ordering edges, sorted: its bases and, for each provision it +// awaits, every other module that provides it (ADR 0603 §1). A toolchain or package edge orders nothing (§3). +func (g Graph) Before(name string) []string { + n := g[name] + out := slices.Clone(n.Bases) + for _, a := range n.Awaits { + for _, p := range g.Providers(a) { + if p != name { + out = append(out, p) + } + } + } + return sortedOnce(out) +} + +// Cycle is the first cycle of the graph's ordering edges, as the modules it passes through with the first named again +// at its end (a -> b -> a), or nil for none (ADR 0603 §2: the check refuses it and names it). The ordering edges are a +// module's bases and, for each provision it awaits, every other module that provides it: modules ordered on each other +// would each wait for the other for good. A toolchain edge is no ordering edge (ADR 0603 §3: nothing waits on a build +// module), and neither is a package edge. The modules are visited in sorted order, so the same graph names the same +// cycle. +func (g Graph) Cycle() []string { + edges := g.Before + const ( + unseen = iota + onPath + done + ) + state := map[string]int{} + var path []string + var visit func(string) []string + visit = func(name string) []string { + state[name] = onPath + path = append(path, name) + for _, to := range edges(name) { + if _, ours := g[to]; !ours { + continue + } + switch state[to] { + case onPath: + i := slices.Index(path, to) + return append(slices.Clone(path[i:]), to) + case unseen: + if c := visit(to); c != nil { + return c + } + } + } + path = path[:len(path)-1] + state[name] = done + return nil + } + for _, name := range slices.Sorted(maps.Keys(g)) { + if state[name] == unseen { + if c := visit(name); c != nil { + return c + } + } + } + return nil +} + +// EdgesOf is the graph node of manifest's own edges: its bases (each module its build stands on), what it provides, +// what it requires and does not require optionally, and of those what a want of it waits for (catalogue.Awaited, as the +// assigner judges it), providers answering the manifests that provide a provision. The fingerprint, place, packages and +// toolchains are the indexer's to add; this is the part a reader of manifests alone (the checker) works out the same way. +func EdgesOf(manifest catalogue.Manifest, providers func(provision string) []catalogue.Manifest) Node { + var n Node + if manifest.Build != nil { + for _, on := range manifest.Build.On { + if on.Module != "" { + n.Bases = append(n.Bases, on.Module) + } + } + } + for _, o := range manifest.Provides { + n.Provides = append(n.Provides, o.Name) + } + for _, r := range manifest.Requires { + if !manifest.OptionalRequires[r] { + n.Requires = append(n.Requires, r) + } + } + n.Awaits = catalogue.Awaited(manifest, providers) + return n.normal() +} + +// OfManifests is the graph of these manifests' own edges (EdgesOf), by module, each provision's providers being +// the manifests among them that offer it: what the check works out for a change to find a cycle (ADR 0603 §2). +func OfManifests(manifests map[string]catalogue.Manifest) Graph { + providers := func(provision string) []catalogue.Manifest { + var out []catalogue.Manifest + for _, name := range slices.Sorted(maps.Keys(manifests)) { + if slices.Contains(manifests[name].Offers(), provision) { + out = append(out, manifests[name]) + } + } + return out + } + g := Graph{} + for name, m := range manifests { + g[name] = EdgesOf(m, providers) + } + return g +} diff --git a/sdk/graph/graph_test.go b/sdk/graph/graph_test.go new file mode 100644 index 00000000..9c34b9db --- /dev/null +++ b/sdk/graph/graph_test.go @@ -0,0 +1,174 @@ +package graph + +import ( + "errors" + "reflect" + "slices" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// TestGraphEdges: the module graph's cycle, dependents and providers, each from the edges alone (ADR 0603 §2, ADR 0604 §1). +func TestGraphEdges(t *testing.T) { + t.Run("a node's value", nodeValues) + fp := "src2:" + strings.Repeat("b", 64) + node := func(repository, path string, edit func(*Node)) Node { + n := Node{FP: fp, Repository: repository, Path: path} + if edit != nil { + edit(&n) + } + return n + } + gotool := map[string]Toolchain{"go": {Module: "build-go", Version: "go1", Recipe: "1"}} + // A base chain, a package and a build module: app stands on base, imports lib's package, is built by build-go. + g := Graph{ + "build-go": node("novox/mesh-catalog", "modules/build-go", func(n *Node) { n.Toolchains = gotool }), + "lib": node("novox/lib", delivery.RootPath, nil), + "base": node("novox/mesh-catalog", "modules/base", nil), + "app": node("novox/mesh-catalog", "modules/app", func(n *Node) { + n.Bases, n.Packages, n.Toolchains = []string{"base"}, []string{"git.novox.be/novox/lib"}, gotool + }), + "top": node("novox/mesh-catalog", "modules/top", func(n *Node) { n.Bases = []string{"app"} }), + "db": node("novox/mesh-catalog", "modules/db", func(n *Node) { n.Provides = []string{"database"} }), + "web": node("novox/mesh-catalog", "modules/web", func(n *Node) { + n.Requires, n.Awaits = []string{"database", "db"}, []string{"database"} + }), + } + for module, want := range map[string][]string{ + "base": {"app", "top"}, + "lib": {"app", "top"}, + "build-go": {"app", "top"}, // build-go's own toolchain edge reaches itself, which is left out + "top": nil, + "db": nil, // a provision edge is no build edge + } { + if got := g.Dependents(module); !reflect.DeepEqual(got, want) { + t.Errorf("dependents of %s = %v, want %v", module, got, want) + } + } + if got := g.Providers("database"); !reflect.DeepEqual(got, []string{"db"}) { + t.Errorf("providers of database = %v", got) + } + if got := g.Providers("db"); !reflect.DeepEqual(got, []string{"db"}) { // a module provides its own name + t.Errorf("providers of db = %v", got) + } + if got := g.Before("web"); !reflect.DeepEqual(got, []string{"db"}) { // web awaits database, which db provides + t.Errorf("web waits for %v", got) + } + if got := g.Before("top"); !reflect.DeepEqual(got, []string{"app"}) { + t.Errorf("top waits for %v", got) + } + if got := g.Requirers("db"); !reflect.DeepEqual(got, []string{"web"}) { + t.Errorf("requirers of db = %v", got) + } + if g.Publisher("git.novox.be/novox/lib") != "lib" || g.Publisher("git.novox.be/novox/mesh-sdk/go") != "" { + t.Error("a package's publisher is not the module at its repository and directory") + } + // From manifests alone (the check's graph): a requirement answered by a helper at mesh scope is awaited, an optional + // one is not, and two modules awaiting each other's provision are a cycle. + offer := func(name string) []catalogue.Offer { + return []catalogue.Offer{{Name: name, Scope: catalogue.ScopeMesh, AnsweredBy: catalogue.AnsweredByHelper}} + } + ms := map[string]catalogue.Manifest{ + "db": {Module: "db", Provides: offer("database"), Requires: []string{"site", "logs"}, OptionalRequires: map[string]bool{"logs": true}}, + "web": {Module: "web", Provides: offer("site"), Requires: []string{"database"}, Build: &catalogue.Build{On: []catalogue.BuildsOn{{Module: "base"}}}}, + } + mg := OfManifests(ms) + if db := mg["db"]; !reflect.DeepEqual(db.Requires, []string{"site"}) || !reflect.DeepEqual(db.Awaits, []string{"site"}) || + !reflect.DeepEqual(mg["web"].Bases, []string{"base"}) || !reflect.DeepEqual(db.Provides, []string{"database"}) { + t.Errorf("the graph of manifests = %+v", mg) + } + if got := mg.Cycle(); !reflect.DeepEqual(got, []string{"db", "web", "db"}) { + t.Errorf("the cycle of manifests = %v", got) + } + for name, c := range map[string]struct { + edit func(Graph) + want []string + }{ + "none: a toolchain on itself and a requirement not awaited order nothing": {func(Graph) {}, nil}, + "bases": {func(g Graph) { + b := g["base"] + b.Bases = []string{"top"} + g["base"] = b + }, []string{"app", "base", "top", "app"}}, + "provisions a -> b -> a": {func(g Graph) { + d := g["db"] + d.Requires, d.Awaits = []string{"site"}, []string{"site"} + g["db"] = d + w := g["web"] + w.Provides = []string{"site"} + g["web"] = w + }, []string{"db", "web", "db"}}, + "a requirement not awaited is no edge": {func(g Graph) { + d := g["db"] + d.Requires = []string{"site"} + g["db"] = d + w := g["web"] + w.Provides = []string{"site"} + g["web"] = w + }, nil}, + } { + h := Graph{} + for k, v := range g { + h[k] = v + } + c.edit(h) + if got := h.Cycle(); !reflect.DeepEqual(got, c.want) { + t.Errorf("%s: cycle = %v, want %v", name, got, c.want) + } + } +} + +// sampleNode is a graph node with every kind of edge, its lists unsorted. +func sampleNode() Node { + return Node{FP: "src2:" + strings.Repeat("a", 64), Repository: "novox/mesh-catalog", Path: "modules/gitea", + Bases: []string{"z-base", "a-base"}, Packages: []string{"git.novox.be/novox/mesh-sdk/go"}, + Toolchains: map[string]Toolchain{"go": {Module: "build-go", Version: "go1.27.1", Recipe: "3"}}, + Provides: []string{"forge"}, Requires: []string{"postgres-database", "dns"}, Awaits: []string{"postgres-database"}} +} + +// nodeValues: a node round-trips with its lists sorted, an unknown version is refused, a node that does not hold is +// refused naming why, and the state is the indexer's graph (ADR 0603 §2). +func nodeValues(t *testing.T) { + if Bucket != "mesh-delivery-indexer_graph" || Read != "mesh-delivery-indexer.graph" || History != 5 || State != "graph" { + t.Error("the graph's state differs from the decided layout") + } + g := sampleNode() + data, err := Encode(g) + if err != nil || !strings.Contains(string(data), `"v":1`) { + t.Fatalf("encode: %s %v", data, err) + } + if got, err := Decode(data); err != nil || !Same(got, g) || !slices.IsSorted(got.Bases) { + t.Errorf("round trip: %+v %v", got, err) + } + if Same(g, Node{None: "x", Repository: "a/b"}) { + t.Error("different graph nodes are the same") + } + for _, in := range []string{`{"none":"x","repository":"a/b"}`, `{"v":2,"none":"x","repository":"a/b"}`} { + if _, err := Decode([]byte(in)); !errors.Is(err, ErrUnknownVersion) { + t.Errorf("decode of %s = %v, want ErrUnknownVersion", in, err) + } + } + for name, c := range map[string]struct { + change func(*Node) + word string + }{ + "both": {func(g *Node) { g.None = "why" }, "both"}, + "neither": {func(g *Node) { g.FP = "" }, "neither"}, + "an old form": {func(g *Node) { g.FP = "src1:x" }, "src2:"}, + "a dotted base": {func(g *Node) { g.Bases = []string{"a.b"} }, "a.b"}, + "a foreign package": {func(g *Node) { g.Packages = []string{"example.com/x"} }, "example.com/x"}, + "a toolchain of no module": {func(g *Node) { g.Toolchains = map[string]Toolchain{"go": {Version: "go1", Recipe: "1"}} }, "build module"}, + "a toolchain of no recipe": {func(g *Node) { g.Toolchains = map[string]Toolchain{"go": {Module: "b", Version: "go1"}} }, "recipe"}, + "awaiting what it does not require": {func(g *Node) { g.Awaits = []string{"other"} }, "other"}, + "no repository": {func(g *Node) { g.Repository = "" }, "repositor"}, + } { + g := sampleNode() + c.change(&g) + if _, err := Encode(g); err == nil || !strings.Contains(err.Error(), c.word) { + t.Errorf("a node with %s: %v, want it refused naming %q", name, err, c.word) + } + } +} From 4a736858b6a681777df1c1bd70501ecae45ace1e Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 16:56:37 +0100 Subject: [PATCH 32/49] Pin mesh-sdk cf5812101b0b, the group's head of feat/l3b-bus with the module graph taken out --- modules/mesh-delivery-assigner/go.mod | 2 +- modules/mesh-delivery-assigner/go.sum | 4 ++-- modules/mesh-delivery-builder/buildrun/go.mod | 2 +- modules/mesh-delivery-builder/buildrun/go.sum | 4 ++-- modules/mesh-delivery-builder/go.mod | 2 +- modules/mesh-delivery-builder/go.sum | 4 ++-- modules/mesh-delivery-checker/go.mod | 6 +++++- modules/mesh-delivery-checker/go.sum | 4 ++-- modules/mesh-delivery-forge-bridge/go.mod | 2 +- modules/mesh-delivery-forge-bridge/go.sum | 4 ++-- modules/mesh-delivery-indexer/go.mod | 6 +++++- modules/mesh-delivery-indexer/go.sum | 4 ++-- modules/mesh-delivery-teller/go.mod | 2 +- modules/mesh-delivery-teller/go.sum | 4 ++-- modules/mesh-delivery-view/go.mod | 6 +++++- modules/mesh-delivery-view/go.sum | 4 ++-- modules/mesh-delivery-watchman/go.mod | 2 +- modules/mesh-delivery-watchman/go.sum | 4 ++-- modules/mesh-enrolment/go.mod | 2 +- modules/mesh-enrolment/go.sum | 4 ++-- modules/mesh-grants/go.mod | 2 +- modules/mesh-grants/go.sum | 4 ++-- 22 files changed, 45 insertions(+), 33 deletions(-) diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index 649a1013..9ea2c53d 100644 --- a/modules/mesh-delivery-assigner/go.mod +++ b/modules/mesh-delivery-assigner/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-assigner go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-assigner/go.sum b/modules/mesh-delivery-assigner/go.sum index 10cdc176..764e281d 100644 --- a/modules/mesh-delivery-assigner/go.sum +++ b/modules/mesh-delivery-assigner/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/buildrun/go.mod b/modules/mesh-delivery-builder/buildrun/go.mod index 5a43d648..ee2e0bdc 100644 --- a/modules/mesh-delivery-builder/buildrun/go.mod +++ b/modules/mesh-delivery-builder/buildrun/go.mod @@ -3,7 +3,7 @@ module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/buildrun/go.sum b/modules/mesh-delivery-builder/buildrun/go.sum index 10cdc176..764e281d 100644 --- a/modules/mesh-delivery-builder/buildrun/go.sum +++ b/modules/mesh-delivery-builder/buildrun/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/go.mod b/modules/mesh-delivery-builder/go.mod index d42def97..188e0da9 100644 --- a/modules/mesh-delivery-builder/go.mod +++ b/modules/mesh-delivery-builder/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-builder go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/go.sum b/modules/mesh-delivery-builder/go.sum index 99140c9e..bf8fed0c 100644 --- a/modules/mesh-delivery-builder/go.sum +++ b/modules/mesh-delivery-builder/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index c484bf3d..332e93f6 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-checker go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) @@ -25,3 +25,7 @@ require ( ) replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun => ../mesh-delivery-builder/buildrun + +require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 + +replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk diff --git a/modules/mesh-delivery-checker/go.sum b/modules/mesh-delivery-checker/go.sum index 99140c9e..bf8fed0c 100644 --- a/modules/mesh-delivery-checker/go.sum +++ b/modules/mesh-delivery-checker/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-forge-bridge/go.mod b/modules/mesh-delivery-forge-bridge/go.mod index 1b302d9e..c26ab21a 100644 --- a/modules/mesh-delivery-forge-bridge/go.mod +++ b/modules/mesh-delivery-forge-bridge/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-forge-bridge go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-forge-bridge/go.sum b/modules/mesh-delivery-forge-bridge/go.sum index 10cdc176..764e281d 100644 --- a/modules/mesh-delivery-forge-bridge/go.sum +++ b/modules/mesh-delivery-forge-bridge/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-indexer/go.mod b/modules/mesh-delivery-indexer/go.mod index 29297f4b..24118bf4 100644 --- a/modules/mesh-delivery-indexer/go.mod +++ b/modules/mesh-delivery-indexer/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-indexer go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) @@ -23,3 +23,7 @@ require ( ) replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun => ../mesh-delivery-builder/buildrun + +require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 + +replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk diff --git a/modules/mesh-delivery-indexer/go.sum b/modules/mesh-delivery-indexer/go.sum index 10cdc176..764e281d 100644 --- a/modules/mesh-delivery-indexer/go.sum +++ b/modules/mesh-delivery-indexer/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-teller/go.mod b/modules/mesh-delivery-teller/go.mod index 98711c70..e0b216ec 100644 --- a/modules/mesh-delivery-teller/go.mod +++ b/modules/mesh-delivery-teller/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-teller go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-teller/go.sum b/modules/mesh-delivery-teller/go.sum index 10cdc176..764e281d 100644 --- a/modules/mesh-delivery-teller/go.sum +++ b/modules/mesh-delivery-teller/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index c75d1278..e9f4bcec 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-view go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) @@ -20,3 +20,7 @@ require ( golang.org/x/sys v0.48.0 // indirect golang.org/x/time v0.15.0 // indirect ) + +require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 + +replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index 10cdc176..764e281d 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-watchman/go.mod b/modules/mesh-delivery-watchman/go.mod index 6fc2a937..575686f9 100644 --- a/modules/mesh-delivery-watchman/go.mod +++ b/modules/mesh-delivery-watchman/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-watchman go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-watchman/go.sum b/modules/mesh-delivery-watchman/go.sum index 9bf59c43..0790a735 100644 --- a/modules/mesh-delivery-watchman/go.sum +++ b/modules/mesh-delivery-watchman/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-enrolment/go.mod b/modules/mesh-enrolment/go.mod index 5f3b3e9f..d7d484d9 100644 --- a/modules/mesh-enrolment/go.mod +++ b/modules/mesh-enrolment/go.mod @@ -3,7 +3,7 @@ module mesh-enrolment go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-enrolment/go.sum b/modules/mesh-enrolment/go.sum index 10cdc176..764e281d 100644 --- a/modules/mesh-enrolment/go.sum +++ b/modules/mesh-enrolment/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-grants/go.mod b/modules/mesh-grants/go.mod index 04540ec2..90f1e501 100644 --- a/modules/mesh-grants/go.mod +++ b/modules/mesh-grants/go.mod @@ -3,7 +3,7 @@ module mesh-grants go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-grants/go.sum b/modules/mesh-grants/go.sum index 10cdc176..764e281d 100644 --- a/modules/mesh-grants/go.sum +++ b/modules/mesh-grants/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e h1:+0CK6QOYhLeZlvKEK/SVD5Mwnsw4VsoGEunOnBptKxU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103154141-18b3eac47c8e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= From 2c37f48f9db0a4066cede1b63d2ba822927cc488 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 17:16:30 +0100 Subject: [PATCH 33/49] =?UTF-8?q?A=20source=20revision=20is=20delivered=20?= =?UTF-8?q?over=20the=20nodes=20present:=20the=20view=20leaves=20out=20a?= =?UTF-8?q?=20node=20the=20watchman=20says=20is=20away=20and=20names=20it,?= =?UTF-8?q?=20and=20the=20node=20counts=20once=20it=20runs=20its=20wants?= =?UTF-8?q?=20(ADR=200603=20=C2=A77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mesh-delivery-view/internal/view/reach.go | 6 ++++ .../internal/view/reach_test.go | 5 ++++ .../mesh-delivery-view/internal/view/view.go | 29 +++++++++++++++++-- .../internal/view/wants_test.go | 10 +++++++ 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/modules/mesh-delivery-view/internal/view/reach.go b/modules/mesh-delivery-view/internal/view/reach.go index 303fb5d8..1f68ce6e 100644 --- a/modules/mesh-delivery-view/internal/view/reach.go +++ b/modules/mesh-delivery-view/internal/view/reach.go @@ -202,6 +202,7 @@ func (m *Model) moduleReach(name string, mod delivery.Module, heads []headAt, wa mr.Changes = append(mr.Changes, c) } var waiting []string + away := m.awayNodes() floor, floorNode, unset := -1, "", "" for _, key := range wantKeys { node, _ := splitKey(key) @@ -209,6 +210,11 @@ func (m *Model) moduleReach(name string, mod delivery.Module, heads []headAt, wa if w.err != "" || w.value.Removed || w.value.FP == "" { continue } + if away[node] { + // A node away holds no head back (ADR 0603 §7); it catches up by its wants and counts once it runs them. + waiting = append(waiting, fmt.Sprintf("%s on %s is away", name, node)) + continue + } mr.Nodes++ h, ok := m.haves[key] switch { diff --git a/modules/mesh-delivery-view/internal/view/reach_test.go b/modules/mesh-delivery-view/internal/view/reach_test.go index 276049ce..69284aef 100644 --- a/modules/mesh-delivery-view/internal/view/reach_test.go +++ b/modules/mesh-delivery-view/internal/view/reach_test.go @@ -68,6 +68,11 @@ func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) { {name: "every node runs the newest build", reached: commitC}, {name: "a node lagging stops the head before the change it lacks", reached: commitB, holds: "web on g14", then: []Entry{haveFP(t, "g14", "web", fpOld, delivery.HealthOK)}, waiting: "not yet the"}, + // ADR 0603 §7: a node away holds no head back, is named, and holds it again once it returns still behind. + {name: "a node away lagging holds no head back and is named", reached: commitC, waiting: "g14 is away", + then: []Entry{haveFP(t, "g14", "web", fpOld, delivery.HealthOK), presenceE(t, "g14", false, t0)}}, + {name: "a node returned lagging holds the head again", reached: commitB, holds: "web on g14", waiting: "not yet the", + then: []Entry{haveFP(t, "g14", "web", fpOld, delivery.HealthOK), presenceE(t, "g14", false, t0), presenceE(t, "g14", true, t0)}}, {name: "a want held back at an older build holds the head as a lag does", reached: commitB, holds: "web on g14 runs the build of", then: []Entry{wantE(t, "g14", "web", delivery.Want{FP: fpOld, Newest: fpNew, Rule: 2}), haveFP(t, "g14", "web", fpOld, delivery.HealthOK)}}, {name: "a failing health check is said and holds nothing", reached: commitC, waiting: "health check failed", diff --git a/modules/mesh-delivery-view/internal/view/view.go b/modules/mesh-delivery-view/internal/view/view.go index a447fe76..1b3cd75b 100644 --- a/modules/mesh-delivery-view/internal/view/view.go +++ b/modules/mesh-delivery-view/internal/view/view.go @@ -124,6 +124,9 @@ type SourceRow struct { Waiting []string `json:"waiting,omitempty"` // Wants is how many wants of modules at the indexer were looked at; a revision with none is delivered of nothing. Wants int `json:"wants"` + // Away are the nodes the watchman says are offline, left out of the comparison (ADR 0603 §7): the revision counts for them + // once they return and run their wants. + Away []string `json:"away,omitempty"` } // AskRow is an open ask (novox/hq ADR 0437 §10): the gate's ask with no answer under its key in the teller's answers. @@ -1061,7 +1064,8 @@ func (m *Model) wantRows() []WantRow { // sourceRow derives the state of the source revision the gate holds (ADR 0429 §1; recorded by no part): delivered when every want // equals its module's fingerprint at the indexer and every have equals its want, running and healthy, and every node a module is -// assigned to has a want. It is nil when the gate holds no readable source. +// assigned to has a want, compared only on the nodes present: a node the watchman says is offline is named away and left out +// (ADR 0603 §7, changing ADR 0429 §1). It is nil when the gate holds no readable source. func (m *Model) sourceRow() *SourceRow { if m.source == nil || m.source.err != "" { return nil @@ -1075,8 +1079,12 @@ func (m *Model) sourceRow() *SourceRow { return fp } looked := 0 + away := m.awayNodes() for key, w := range m.wants { node, module := splitKey(key) + if away[node] { + continue + } mod, ok := m.modules[module] if w.err != "" || w.value.Removed || !ok || mod.err != "" || mod.value.FP2 == "" { continue @@ -1096,7 +1104,7 @@ func (m *Model) sourceRow() *SourceRow { } for _, a := range m.placedPairs() { wk, werr := delivery.HaveKey(a.Node, a.Module) - if werr != nil { + if werr != nil || away[a.Node] { continue } if _, ok := m.wants[wk]; ok { @@ -1107,13 +1115,28 @@ func (m *Model) sourceRow() *SourceRow { } } sort.Strings(waiting) - row := &SourceRow{Revision: m.source.revision, State: SourceDelivered, Wants: looked} + row := &SourceRow{Revision: m.source.revision, State: SourceDelivered, Wants: looked, Away: slices.Sorted(maps.Keys(away))} if len(waiting) > 0 { row.State, row.Waiting = SourceNotDelivered, waiting } return row } +// awayNodes are the nodes whose presence the watchman says is offline (a node's own key, not a part instance's): a source +// revision is delivered over the nodes present (ADR 0603 §7). Called with the lock held. +func (m *Model) awayNodes() map[string]bool { + out := map[string]bool{} + for key, p := range m.presence { + if p.err != "" || p.value.Online { + continue + } + if node, part, _, err := delivery.ParsePresenceKey(key); err == nil && part == "" { + out[node] = true + } + } + return out +} + // splitKey is the node and the module (unescaped, issue 1150) of a model key, which is the have bucket's key form. func splitKey(key string) (node, module string) { node, module, isNode, err := delivery.ParseHaveKey(key) diff --git a/modules/mesh-delivery-view/internal/view/wants_test.go b/modules/mesh-delivery-view/internal/view/wants_test.go index 07ad6dcd..e545bfe2 100644 --- a/modules/mesh-delivery-view/internal/view/wants_test.go +++ b/modules/mesh-delivery-view/internal/view/wants_test.go @@ -124,6 +124,16 @@ func TestASourceRevisionIsDerivedFromWantsAndHaves(t *testing.T) { if s.State != SourceNotDelivered || !strings.Contains(strings.Join(s.Waiting, ";"), "novox") { t.Errorf("a node assigned the module with no want: %+v", s) } + // ADR 0603 §7: a node away holds nothing back and is named; it counts once it returns and runs its wants. + fill(m, presenceE(t, "novox", false, t0)) + s = m.Snapshot().Source + if s.State != SourceDelivered || len(s.Away) != 1 || s.Away[0] != "novox" { + t.Errorf("a node away: %+v", s) + } + fill(m, presenceE(t, "novox", true, t0)) + if s = m.Snapshot().Source; s.State != SourceNotDelivered || len(s.Away) != 0 { + t.Errorf("the node returned and runs nothing yet: %+v", s) + } } func TestNoSourceRevisionHeldMeansNoStateIsDerived(t *testing.T) { From 92b430792210f6c310c212db3af9808f1be2ddee Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 17:18:36 +0100 Subject: [PATCH 34/49] =?UTF-8?q?The=20watchman=20mirrors=20a=20want=20hel?= =?UTF-8?q?d=20by=20a=20rule,=20a=20provision=20asked=20and=20a=20build=20?= =?UTF-8?q?queued=20behind=20a=20package=20as=20waits=20with=20a=20bound,?= =?UTF-8?q?=20and=20raises=20a=20finding=20past=20it=20(ADR=200603=20?= =?UTF-8?q?=C2=A78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cmd/mesh-delivery-watchman/main.go | 22 +++++++- .../internal/watch/findings_test.go | 32 +++++++++++ .../internal/watch/mirror.go | 56 +++++++++++++++++++ .../internal/watch/signals.go | 31 ++++++++++ .../internal/watch/watch.go | 3 +- .../mesh-delivery-watchman/manifest_test.go | 2 +- modules/mesh-delivery-watchman/module.json | 1 + 7 files changed, 144 insertions(+), 3 deletions(-) diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go index 1ded4bd7..8ec5142d 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go @@ -246,6 +246,17 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro w.ReadWhole(delivery.AssignerWantsBucket) } } + // The builder's builds (ADR 0603 §8): a key queued behind a package is a wait, mirrored from the time it was written. + var buildSeen jetstream.KeyWatcher + if kv := process.OptionalBucket(ctx, js, delivery.BuilderBuildsBucket, "a build waiting for a package is not watched until the watchman starts again", logger.Printf); kv != nil { + buildEach := func(e jetstream.KeyValueEntry) { w.Build(e.Key(), e.Value(), opOf(e), e.Created()) } + if buildSeen, err = settle(ctx, kv, buildEach); err != nil { + process.NotWatched(logger.Printf, delivery.BuilderBuildsBucket, err, "a build waiting for a package is not watched until the watchman starts again") + buildSeen = nil + } else { + w.ReadWhole(delivery.BuilderBuildsBucket) + } + } // The build seats' steps: a step key `.` written is the ask taken (issue 1887 A8b). A seat whose build module // keeps no steps bucket (yet) is said and left; the asks of its stream are mirrored all the same. taken := map[string]jetstream.KeyWatcher{} @@ -304,7 +315,11 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // The seat mesh-provisioner's provisions (ADR 0570 §7): a provision in waits is needs-operator. Read before the findings are // reconciled when the bucket is there; bound once it appears when it is not (mesh-provisioner comes in its own delivery). provisionsWatch := func(kv jetstream.KeyValue) (jetstream.KeyWatcher, error) { - return settle(ctx, kv, func(e jetstream.KeyValueEntry) { w.Provision(e.Key(), e.Value(), opOf(e)) }) + pw, err := settle(ctx, kv, func(e jetstream.KeyValueEntry) { w.Provision(e.Key(), e.Value(), opOf(e)) }) + if err == nil { + w.ReadWhole(provisions.ProvisionsBucket) + } + return pw, err } var provWatch jetstream.KeyWatcher if kv, err := js.KeyValue(ctx, provisions.ProvisionsBucket); err == nil { @@ -395,6 +410,11 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro errs <- follow(ctx, wantSeen, func(e jetstream.KeyValueEntry) { w.Want(e.Key(), e.Value(), opOf(e), e.Created()) }) }() } + if buildSeen != nil { + go func() { + errs <- follow(ctx, buildSeen, func(e jetstream.KeyValueEntry) { w.Build(e.Key(), e.Value(), opOf(e), e.Created()) }) + }() + } for lang, sw := range taken { lang, sw := lang, sw go func() { errs <- follow(ctx, sw, func(e jetstream.KeyValueEntry) { stepTaken(w, lang, e) }) }() diff --git a/modules/mesh-delivery-watchman/internal/watch/findings_test.go b/modules/mesh-delivery-watchman/internal/watch/findings_test.go index 57f26142..b0a04e78 100644 --- a/modules/mesh-delivery-watchman/internal/watch/findings_test.go +++ b/modules/mesh-delivery-watchman/internal/watch/findings_test.go @@ -457,6 +457,38 @@ func TestTheWatchmansFindings(t *testing.T) { w.DeadLettersHeld(map[string]int{"SEAT_X.y_worker": 1}, who) w.NoticesWaiting(true) }, map[string]string{"bus.SEAT_X.y_worker.max-deliveries": Warning, "bus.DEAD_LETTER_NOTICES.controller.notices-waiting": Warning}}, + {"ADR 0603 §8: a want held by a rule, a provision asked and a build queued behind a package each raise a finding naming the wait past their bound, and clear when it ends", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { + w.SetStepBound(30 * time.Minute) + held, _ := delivery.EncodeWant(delivery.Want{FP: fpOld, Self: true, Newest: fpNew, Rule: delivery.RuleCanary, Words: "the canary ace does not run it yet"}) + released, _ := delivery.EncodeWant(delivery.Want{FP: fpNew, Self: true}) + asked, _ := provisions.Encode(provisions.Provision{Provision: "oidc", Consumer: provisions.End{Module: "photos", Node: "ace"}, + Provider: provisions.End{Module: "keycloak", Node: "novox"}, State: provisions.Asked, At: t0}) + provided, _ := provisions.Encode(provisions.Provision{Provision: "oidc", Consumer: provisions.End{Module: "photos", Node: "ace"}, + Provider: provisions.End{Module: "keycloak", Node: "novox"}, State: provisions.Provided, At: t0}) + bkey, _ := delivery.BuilderBuildKey("web", fpNew) + queued, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildQueued, Waits: "mesh-sdk@v0.1.14"}) + building, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildBuilding, By: "n1"}) + w.Have("ace.web", haveValue(t, fpOld), Put) // the node runs what the held want names: the hold is still the wait + w.Want("want.ace.web", held, Put, t0) + w.Provision("ace.photos.oidc", asked, Put) + w.Build(bkey, queued, Put, t0) + waits := map[string]string{delivery.AssignerWantsBucket: "want.ace.web", provisions.ProvisionsBucket: "ace.photos.oidc", delivery.BuilderBuildsBucket: bkey} + if len(o.pulses) != 3 || len(o.findings) != 0 { + t.Fatalf("pulses %v findings %v", o.pulses, o.findings) + } + for bucket, key := range waits { + expire(w, c, pulseKeyOf(t, bucket, key), 31*time.Minute) + } + for bucket, key := range waits { + f := o.findings[FindingKey(ScopeDelivery, stuckKeyOf(t, bucket, key), KindStuck)] + if f.Kind != KindStuck || !f.Since.Equal(t0) || !strings.Contains(f.Summary, "bound 30m0s") { + t.Fatalf("%s: findings = %v", bucket, o.findings) + } + } + w.Want("want.ace.web", released, Put, c.t) + w.Provision("ace.photos.oidc", provided, Put) + w.Build(bkey, building, Put, c.t) + }, map[string]string{}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { diff --git a/modules/mesh-delivery-watchman/internal/watch/mirror.go b/modules/mesh-delivery-watchman/internal/watch/mirror.go index 5bf05e7f..b2f0e180 100644 --- a/modules/mesh-delivery-watchman/internal/watch/mirror.go +++ b/modules/mesh-delivery-watchman/internal/watch/mirror.go @@ -78,6 +78,7 @@ func (w *Watchman) Want(key string, value []byte, op Op, at time.Time) { return case Delete: delete(w.wants, key) + delete(w.held, key) w.moved(delivery.AssignerWantsBucket, key) return } @@ -89,10 +90,27 @@ func (w *Watchman) Want(key string, value []byte, op Op, at time.Time) { } if want.Removed { delete(w.wants, key) + delete(w.held, key) w.moved(delivery.AssignerWantsBucket, key) return } w.wants[key] = want.FP + if want.Newest != "" || want.Rule != 0 { + // A want held by a rule (ADR 0603 §8) waits on the rule, not on the node: its wait is the hold's, from the time the + // assigner wrote it, and the node running what the want names does not end it. + if at.IsZero() { + at = w.now() + } + w.held[key] = true + w.mirrorWithin(w.stepBound, delivery.AssignerWantsBucket, key, + describe("the hold on %s on %s at %s (rule %d) to end, bound %s: %s", module, node, shortFP(want.FP), want.Rule, w.stepBound, want.Words), + "held|"+want.FP+"|"+want.Newest, at, 0) + return + } + if w.held[key] { + delete(w.held, key) + w.stuckMoved(delivery.AssignerWantsBucket, key) // the hold ended: its finding is cleared + } if w.have(node, module) == want.FP { w.moved(delivery.AssignerWantsBucket, key) return @@ -104,6 +122,44 @@ func (w *Watchman) Want(key string, value []byte, op Op, at time.Time) { describe("the node agent of %s to run %s at %s", node, module, shortFP(want.FP)), want.FP, at, 0) } +// A build key queued behind a package is a wait of the builder's (ADR 0603 §8, ADR 0500 clause 6): its Waits names the package +// version the registry does not yet hold, and the key is not claimed until it is published. Its pulse is the pulse of the +// builder's builds bucket under the key, from the time the key was written, bounded as a step is (StepBound); the key +// claimed, done, failed or deleted ends it. + +// Build is a change of a key of the builder's builds bucket, read from the watch (at the start the keys that stand, then +// each change), at the time the entry was written. +func (w *Watchman) Build(key string, value []byte, op Op, at time.Time) { + module, fp, err := delivery.ParseBuilderBuildKey(key) + if err != nil { + return + } + w.mu.Lock() + defer w.mu.Unlock() + if op == Expired { + return + } + if op == Delete { + w.moved(delivery.BuilderBuildsBucket, key) + return + } + w.stands(delivery.BuilderBuildsBucket, key) + b, err := delivery.DecodeBuilderBuild(value) + if err != nil { + w.say("the build %s cannot be read, and is left: %v", key, err) + return + } + if b.State != delivery.BuildQueued || b.Waits == "" { + w.moved(delivery.BuilderBuildsBucket, key) + return + } + if at.IsZero() { + at = w.now() + } + w.mirrorWithin(w.stepBound, delivery.BuilderBuildsBucket, key, + describe("the package %s to be published before the build of %s at %s, bound %s", b.Waits, module, shortFP(fp), w.stepBound), b.Waits, at, 0) +} + // Have is a change of a key of the node-engine's have bucket: the fingerprint a node runs of a module. One that names the // fingerprint of the node's want for the module ends the want's pulse. A node's own key and a key deleted are left. func (w *Watchman) Have(key string, value []byte, op Op) { diff --git a/modules/mesh-delivery-watchman/internal/watch/signals.go b/modules/mesh-delivery-watchman/internal/watch/signals.go index ee44118a..9c2e8501 100644 --- a/modules/mesh-delivery-watchman/internal/watch/signals.go +++ b/modules/mesh-delivery-watchman/internal/watch/signals.go @@ -589,6 +589,7 @@ func (w *Watchman) Provision(key string, value []byte, op Op) { w.waiting[id][key] = fmt.Sprintf("%s from %s on %s: %s", p.Provision, p.Provider.Module, p.Provider.Node, p.Why) } } + w.askedWait(key, value, op) fkey := FindingKey(ScopeModule, id, KindNeedsOperator) if len(w.waiting[id]) == 0 { delete(w.waiting, id) @@ -604,6 +605,36 @@ func (w *Watchman) Provision(key string, value []byte, op Op) { Said: strings.Join(said, "; ")}) } +// askedWait is the wait of a provision its provider has not provided (ADR 0603 §8): a key asked is mirrored from the time the +// holder wrote it, bounded as a step is, and provided, failed, waiting for the operator (its own finding), retiring or deleted +// ends it. Called with the lock held. +func (w *Watchman) askedWait(key string, value []byte, op Op) { + bucket := provisions.ProvisionsBucket + if op == Expired { + return + } + w.stands(bucket, key) + if op == Delete { + w.moved(bucket, key) + return + } + p, err := provisions.Decode(value) + if err != nil || p.State != provisions.Asked { + w.moved(bucket, key) + return + } + since := p.At + if since.IsZero() { + since = w.now() + } + words := fmt.Sprintf("%s from %s on %s to be provided for %s on %s, bound %s", p.Provision, p.Provider.Module, p.Provider.Node, + p.Consumer.Module, p.Consumer.Node, w.stepBound) + if p.Why != "" { + words += ": " + p.Why + } + w.mirrorWithin(w.stepBound, bucket, key, words, "asked", since, 0) +} + func sortedKeys(m map[string]string) []string { out := make([]string, 0, len(m)) for k := range m { diff --git a/modules/mesh-delivery-watchman/internal/watch/watch.go b/modules/mesh-delivery-watchman/internal/watch/watch.go index 3c769493..ef16ebbd 100644 --- a/modules/mesh-delivery-watchman/internal/watch/watch.go +++ b/modules/mesh-delivery-watchman/internal/watch/watch.go @@ -64,6 +64,7 @@ type Watchman struct { keptSince map[string]time.Time wants map[string]string // the fingerprint of each want that stands, by want key haves map[string]string // the fingerprint each node runs of a module, by have key + held map[string]bool // each want key held by a rule (ADR 0603 §8), mirrored as the hold's wait bound time.Duration // findings is every finding written, by key, as written; seededFindings those read at the start and not yet observed // again. @@ -103,7 +104,7 @@ type Watchman struct { // New is a watchman writing to out, reading the time from now. func New(out Out, now func() time.Time) *Watchman { return &Watchman{out: out, now: now, counts: map[string]int{}, presence: map[string]presence{}, - pulses: map[string]*pulse{}, stuck: map[string]delivery.Stuck{}, wants: map[string]string{}, haves: map[string]string{}, bound: delivery.PulseMoveBound, stepBound: StepBound, + pulses: map[string]*pulse{}, stuck: map[string]delivery.Stuck{}, wants: map[string]string{}, haves: map[string]string{}, held: map[string]bool{}, bound: delivery.PulseMoveBound, stepBound: StepBound, findings: map[string][]byte{}, seededFindings: map[string]bool{}, signals: map[string]signalDue{}, away: map[string]bool{}, haveState: map[string]delivery.Have{}, unhealthy: map[string]int{}, advised: map[string]int{}, advisedSince: map[string]time.Time{}, control: map[string]bool{}, failedSince: map[string]time.Time{}, provFailed: map[string]map[string]ProvReport{}, closures: map[string]map[string]time.Time{}, waiting: map[string]map[string]string{}, diff --git a/modules/mesh-delivery-watchman/manifest_test.go b/modules/mesh-delivery-watchman/manifest_test.go index 7f146ca1..a577361e 100644 --- a/modules/mesh-delivery-watchman/manifest_test.go +++ b/modules/mesh-delivery-watchman/manifest_test.go @@ -91,7 +91,7 @@ func TestManifestStateAndReads(t *testing.T) { // 1135/AC6 reads := slices.Clone(m.Reads) slices.Sort(reads) if !reflect.DeepEqual(reads, []string{"mesh-build-go.steps", "mesh-build-image.steps", "mesh-build-node.steps", - "mesh-delivery-assigner.wants", "mesh-delivery-indexer.modules", "mesh-host.health", "mesh-provisioner.provisions", "node-engine.have"}) { + "mesh-delivery-assigner.wants", "mesh-delivery-builder.builds", "mesh-delivery-indexer.modules", "mesh-host.health", "mesh-provisioner.provisions", "node-engine.have"}) { t.Errorf("reads = %v", reads) } // 1952/WM1: the watchman holds the seat mesh-watchdogs at mesh scope, its first seat (novox/hq ADR 0564 §1, §4). diff --git a/modules/mesh-delivery-watchman/module.json b/modules/mesh-delivery-watchman/module.json index f405cf75..4c85f5d7 100644 --- a/modules/mesh-delivery-watchman/module.json +++ b/modules/mesh-delivery-watchman/module.json @@ -20,6 +20,7 @@ "mesh-build-image.steps", "mesh-build-node.steps", "mesh-delivery-assigner.wants", + "mesh-delivery-builder.builds", "mesh-delivery-indexer.modules", "mesh-host.health", "mesh-provisioner.provisions", From a1814c6571dac39f0458e8d7203b7b47a3aa7ed0 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 17:19:05 +0100 Subject: [PATCH 35/49] =?UTF-8?q?Assigner=20row:=20the=20newest=20source?= =?UTF-8?q?=20revision's=20provider=20edge=20replaces=20the=20older=20one,?= =?UTF-8?q?=20and=20only=20it=20is=20waited=20on=20(ADR=200603=20=C2=A79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/assign/wants_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/modules/mesh-delivery-assigner/internal/assign/wants_test.go b/modules/mesh-delivery-assigner/internal/assign/wants_test.go index 95d4eee3..ba188a94 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants_test.go @@ -104,6 +104,24 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { t.Errorf("provided provisions held g14's want: %+v (%s)", out.Wants["g14"], out.Rules["g14"].Words) } } + // ADR 0603 §9: the newest source revision governs. The module now at fNew no longer requires the database an older revision + // asked for and requires oidc instead; the seat's keys follow the newest manifest, so the older edge is no wait: a provided + // oidc moves the want, an asked one holds it and names oidc and never database. + for _, c := range []struct { + name string + state string + moves bool + }{{"the replaced edge provided", provisions.Provided, true}, {"the replaced edge asked", provisions.Asked, false}} { + m, n := ready() + m.Manifest.Requires = []string{"oidc"} + m.Provisions = func(node string) map[string]provisions.Provision { + return map[string]provisions.Provision{node + ".mail.oidc": {Provision: "oidc", State: c.state}} + } + out := Wants(m, n, wantedOld(), nil) + if got := out.Wants["g14"].FP == fNew; got != c.moves || (!c.moves && (!strings.Contains(out.Rules["g14"].Words, "oidc") || strings.Contains(out.Rules["g14"].Words, "database"))) { + t.Errorf("%s: want %+v, held by %q", c.name, out.Wants["g14"], out.Rules["g14"].Words) + } + } // 1954/D1 (the lead, 2026-10-29): a required provision with no key yet holds the want, named; and only one the // composition keys (catalogue.Awaited, issue 774), replaying 2026-10-30: grafana (not handed) waited on ace for // oidc-client, which the controller still makes, and mesh-build-go (handed) for go-toolchain on its own machine. From 1387b10e78930bdf1348356d2b7822362a7250e5 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 17:21:01 +0100 Subject: [PATCH 36/49] Take a missing graph state as no graph yet and a Publisher's directory as the nearest module above it The indexer's recompute and merge failed on listing a graph bucket not yet on the bus, and a package published from inside a module's directory had no publisher, so the dependents of a build module missed what imports its packages (review findings 3 and 4 of L3). Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- .../internal/index/graph.go | 27 +++++++++++++------ .../internal/index/index_test.go | 4 +++ sdk/graph/graph.go | 21 ++++++++++++--- sdk/graph/graph_test.go | 18 +++++++++++-- 4 files changed, 56 insertions(+), 14 deletions(-) diff --git a/modules/mesh-delivery-indexer/internal/index/graph.go b/modules/mesh-delivery-indexer/internal/index/graph.go index f9195d3b..d50caa64 100644 --- a/modules/mesh-delivery-indexer/internal/index/graph.go +++ b/modules/mesh-delivery-indexer/internal/index/graph.go @@ -2,12 +2,14 @@ package index import ( "context" + "errors" "fmt" "maps" "slices" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-delivery/sdk/graph" ) @@ -60,11 +62,18 @@ func graphNode(e graphEdges, providers func(string) []catalogue.Manifest) graph. } // writeGraph writes the graph node of every module of edges whose value changed, at most GraphMost of them, then -// deletes each graph key whose module the modules state no longer holds. Nil Graph writes nothing. +// deletes each graph key whose module the modules state no longer holds. Nil Graph writes nothing, and neither does a +// graph state that is not on the bus yet (process.ErrNotYet; main.go: until then no graph is written), said in the log; +// a listing that fails otherwise skips the deletion pass, said, and never fails the merge or the recompute that wrote. func (ix *Indexer) writeGraph(ctx context.Context, edges map[string]graphEdges) error { if ix.Graph == nil { return nil } + keys, listErr := ix.Graph.Keys(ctx) + if errors.Is(listErr, process.ErrNotYet) { + ix.logf("the module graph is not written: its state is not on the bus yet") + return nil + } names := slices.Sorted(maps.Keys(edges)) if len(names) > GraphMost { ix.logf("the module graph holds at most %d modules: %d are known, so %s and the %d after it are not written", @@ -104,19 +113,21 @@ func (ix *Indexer) writeGraph(ctx context.Context, edges map[string]graphEdges) } } held, err := ix.Modules.Keys(ctx) - if err != nil { - return fmt.Errorf("listing the modules for the graph: %w", err) - } - keys, err := ix.Graph.Keys(ctx) - if err != nil { - return fmt.Errorf("listing the graph: %w", err) + switch { + case listErr != nil: + ix.logf("the graph keys of modules the modules state no longer holds are not deleted: listing the graph: %v", listErr) + return nil + case err != nil: + ix.logf("the graph keys of modules the modules state no longer holds are not deleted: listing the modules: %v", err) + return nil } for _, key := range keys { if slices.Contains(held, key) { continue } if err := write(ctx, ix.Graph, key, func(_ []byte, found bool) ([]byte, bool, error) { return nil, found, nil }); err != nil { - return fmt.Errorf("graph.%s: %w", key, err) + ix.logf("graph.%s is not deleted: %v", key, err) + continue } ix.logf("graph.%s deleted: the modules state holds no module %s", key, key) } diff --git a/modules/mesh-delivery-indexer/internal/index/index_test.go b/modules/mesh-delivery-indexer/internal/index/index_test.go index e6f9da4d..b584aa67 100644 --- a/modules/mesh-delivery-indexer/internal/index/index_test.go +++ b/modules/mesh-delivery-indexer/internal/index/index_test.go @@ -19,6 +19,7 @@ const ( // key under the indexer; unbound, when set, is every read's error (a state not yet on the bus). type memStore struct { unbound error + keysErr error // the listing's error vals map[string][]byte revs map[string]uint64 next uint64 @@ -36,6 +37,9 @@ func (m *memStore) Get(_ context.Context, key string) ([]byte, uint64, bool, err return v, m.revs[key], ok, nil } func (m *memStore) Keys(context.Context) ([]string, error) { + if m.keysErr != nil { + return nil, m.keysErr + } var keys []string for k := range m.vals { keys = append(keys, k) diff --git a/sdk/graph/graph.go b/sdk/graph/graph.go index 6648ced8..b0c7951c 100644 --- a/sdk/graph/graph.go +++ b/sdk/graph/graph.go @@ -230,8 +230,9 @@ func (g Graph) Requirers(provision string) []string { return out } -// Publisher is the module of the graph whose repository and directory publish the mesh package pkg (PackageRepository), -// or "" when the graph holds none. +// Publisher is the module of the graph whose repository and directory publish the mesh package pkg (PackageRepository): +// the module whose directory is the package's directory or the nearest one above it (the longest match, a package +// published from inside a module's directory belonging to that module), or "" when the graph holds none. func (g Graph) Publisher(pkg string) string { repository, dir, ok := delivery.PackageRepository(pkg) if !ok { @@ -240,12 +241,24 @@ func (g Graph) Publisher(pkg string) string { if dir == "" { dir = delivery.RootPath } + best, bestLen := "", -1 for _, name := range slices.Sorted(maps.Keys(g)) { - if n := g[name]; n.Repository == repository && n.Path == dir { + n := g[name] + if n.Repository != repository { + continue + } + switch { + case n.Path == dir: return name + case n.Path == delivery.RootPath: + if bestLen < 0 { + best, bestLen = name, 0 + } + case strings.HasPrefix(dir, n.Path+"/") && len(n.Path) > bestLen: + best, bestLen = name, len(n.Path) } } - return "" + return best } // buildEdges are the modules n's build edges reach: its bases, the publisher of each of its packages, and the build diff --git a/sdk/graph/graph_test.go b/sdk/graph/graph_test.go index 9c34b9db..2848f19c 100644 --- a/sdk/graph/graph_test.go +++ b/sdk/graph/graph_test.go @@ -32,7 +32,11 @@ func TestGraphEdges(t *testing.T) { n.Bases, n.Packages, n.Toolchains = []string{"base"}, []string{"git.novox.be/novox/lib"}, gotool }), "top": node("novox/mesh-catalog", "modules/top", func(n *Node) { n.Bases = []string{"app"} }), - "db": node("novox/mesh-catalog", "modules/db", func(n *Node) { n.Provides = []string{"database"} }), + // A package published from inside a module's directory belongs to that module (ADR 0604 §1): users imports build-go's. + "users": node("novox/other", "modules/users", func(n *Node) { + n.Packages = []string{"git.novox.be/novox/mesh-catalog/modules/build-go/buildrun"} + }), + "db": node("novox/mesh-catalog", "modules/db", func(n *Node) { n.Provides = []string{"database"} }), "web": node("novox/mesh-catalog", "modules/web", func(n *Node) { n.Requires, n.Awaits = []string{"database", "db"}, []string{"database"} }), @@ -40,7 +44,7 @@ func TestGraphEdges(t *testing.T) { for module, want := range map[string][]string{ "base": {"app", "top"}, "lib": {"app", "top"}, - "build-go": {"app", "top"}, // build-go's own toolchain edge reaches itself, which is left out + "build-go": {"app", "top", "users"}, // build-go's own toolchain edge reaches itself, which is left out "top": nil, "db": nil, // a provision edge is no build edge } { @@ -63,6 +67,16 @@ func TestGraphEdges(t *testing.T) { if got := g.Requirers("db"); !reflect.DeepEqual(got, []string{"web"}) { t.Errorf("requirers of db = %v", got) } + for pkg, want := range map[string]string{ + "git.novox.be/novox/mesh-catalog/modules/build-go/buildrun": "build-go", // inside its directory + "git.novox.be/novox/mesh-catalog/modules/build-go": "build-go", // its directory + "git.novox.be/novox/mesh-catalog/modules/build-gone": "", // beside it, not inside + "git.novox.be/novox/lib/sub": "lib", // a root module publishes what is below it + } { + if got := g.Publisher(pkg); got != want { + t.Errorf("publisher of %s = %q, want %q", pkg, got, want) + } + } if g.Publisher("git.novox.be/novox/lib") != "lib" || g.Publisher("git.novox.be/novox/mesh-sdk/go") != "" { t.Error("a package's publisher is not the module at its repository and directory") } From b4855f21c32ccb17729147b95a16b3c352dea187 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 17:21:01 +0100 Subject: [PATCH 37/49] Test rows: a missing graph state fails no recompute (review finding 3); a recipe change builds a Go module again (D9) Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- .../internal/index/toolchains_test.go | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go index 0fbe30d6..ffac6f5f 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go @@ -2,12 +2,15 @@ package index import ( "context" + "errors" + "fmt" "reflect" "strings" "testing" "time" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-delivery/sdk/graph" @@ -114,12 +117,17 @@ func toolchainValue(t *testing.T, language, version, recipe string) []byte { } func reportToolchain(t *testing.T, ix *Indexer, module, node, language, version string) { + t.Helper() + reportToolchainRecipe(t, ix, module, node, language, version, "1") +} + +func reportToolchainRecipe(t *testing.T, ix *Indexer, module, node, language, version, recipe string) { t.Helper() key, err := delivery.ToolchainKey(node, language) if err != nil { t.Fatal(err) } - if err := ix.ApplyToolchain(context.Background(), module, key, toolchainValue(t, language, version, "1"), false); err != nil { + if err := ix.ApplyToolchain(context.Background(), module, key, toolchainValue(t, language, version, recipe), false); err != nil { t.Fatal(err) } } @@ -172,6 +180,31 @@ func TestAgreedToolchainChangesAGoModulesReasonWithNoMerge(t *testing.T) { // 14 } } +// ADR 0603 §3 (D9): a source revision that changes the Go build module's recipe and a Go module builds the module at once, with +// the recipe the nodes run, and again once the agreement moves to the new one: the module never waits and never loses its +// fingerprint between the two, and its toolchain edge names the recipe each was computed with. +func TestARecipeChangeBuildsAGoModuleAtOnceAndAgainOnceTheAgreementMoves(t *testing.T) { + ix, mods := newFormIndexer(t, mod{"avahi", catalogueManifest(t, "avahi")}) + for _, node := range []string{"novox", "ace"} { + reportToolchain(t, ix, "mesh-build-go", node, "go", "go1.27.1") + } + first, _ := moduleOf(t, mods, "avahi") + if first.FP2 == "" || graphOf(t, ix, "avahi").Toolchains["go"].Recipe != "1" { + t.Fatalf("with the running recipe: fp2 %q, graph %+v", first.FP2, graphOf(t, ix, "avahi")) + } + // The release carrying the new recipe is merged, but the nodes still run the old one: one node reporting the new recipe is + // no strict majority, so the fingerprint stays, nothing waits. + reportToolchainRecipe(t, ix, "mesh-build-go", "novox", "go", "go1.27.1", "2") + if held, _ := moduleOf(t, mods, "avahi"); held.FP2 != first.FP2 || held.None2 != "" { + t.Errorf("one node on the new recipe: fp2 %q none2 %q, want the fingerprint of the running recipe, %q", held.FP2, held.None2, first.FP2) + } + reportToolchainRecipe(t, ix, "mesh-build-go", "ace", "go", "go1.27.1", "2") + again, _ := moduleOf(t, mods, "avahi") + if again.FP2 == "" || again.FP2 == first.FP2 || again.None2 != "" || graphOf(t, ix, "avahi").Toolchains["go"].Recipe != "2" { + t.Errorf("the agreement moved: fp2 %q (was %q) none2 %q graph %+v, want a second fingerprint at recipe 2", again.FP2, first.FP2, again.None2, graphOf(t, ix, "avahi")) + } +} + func TestNodesReportingDifferentVersionsLeaveTheReasonNamingBoth(t *testing.T) { // 1444/AC6 // Both reports are read before the module's first fingerprint is computed (a fingerprint computed before they // disagree is kept: TestAModuleKeepsTheToolchainsItsFingerprintWasComputedWith). @@ -258,6 +291,34 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { // if _, held := graph.vals["avahi"]; !held { t.Error("graph.avahi went with another module") } + // Review finding 3 (ADR 0603 §2): a graph state not on the bus yet (process.ErrNotYet), or one whose listing fails, never + // fails a recompute; the first writes nothing and says so, the second skips the deletion pass and says so. + graph.vals["ghost"], graph.revs["ghost"] = []byte("x"), 1 + var said []string + ix.Logf = func(format string, args ...any) { said = append(said, fmt.Sprintf(format, args...)) } + for _, row := range []struct { + name string + err error + say string + }{ + {"not on the bus yet", process.ErrNotYet, "not on the bus yet"}, + {"a listing that fails", errors.New("the bus is slow"), "are not deleted"}, + } { + said, graph.keysErr = nil, row.err + if err := ix.Recompute(context.Background()); err != nil { + t.Errorf("%s: the recompute failed: %v", row.name, err) + } + if _, held := graph.vals["ghost"]; !held || !strings.Contains(strings.Join(said, "\n"), row.say) { + t.Errorf("%s: ghost held %v, log %q, want it kept and %q said", row.name, held, said, row.say) + } + } + graph.keysErr = nil + if err := ix.Recompute(context.Background()); err != nil { + t.Fatal(err) + } + if _, held := graph.vals["ghost"]; held { + t.Error("graph.ghost is still held once the graph can be listed") + } a := agentBuild("build-1792587792086548572", agentSrcY, "2026-10-21T15:03:00Z") _ = ix.Builds.Put(a[1].([]byte)) if err := ix.Recompute(context.Background()); err != nil { From 5c4d770aa11c18d01fd01cb6595405b252d1f1a8 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 17:21:07 +0100 Subject: [PATCH 38/49] =?UTF-8?q?The=20check=20names=20the=20modules=20bui?= =?UTF-8?q?lt=20with=20a=20changed=20build=20module=20or=20base;=20rows=20?= =?UTF-8?q?for=20D9=20and=20the=20assigner's=20integration=20test=20(ADR?= =?UTF-8?q?=200603=20=C2=A73,=20=C2=A74,=20=C2=A710)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D13b: the composition's warnings, and so the check's line, name the changed modules built with the Go build module the change alters and those standing on a changed base. D9: builder and verdict rows. D15: the assigner's integration test gets a source revision changing a provider, its new consumer and a build module. Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- .../integration_test.go | 55 ++++++++++++++++ .../internal/build/run_test.go | 39 +++++++++++ .../internal/check/build_change.go | 66 +++++++++++++++++++ .../internal/check/composition_test.go | 51 ++++++++++++++ .../internal/check/judge.go | 1 + .../internal/index/verdicts_test.go | 5 ++ 6 files changed, 217 insertions(+) create mode 100644 modules/mesh-delivery-checker/internal/check/build_change.go diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go index 554595ce..cadba393 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go @@ -27,6 +27,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/health" "git.novox.be/novox/mesh-sdk/go/process" + "git.novox.be/novox/mesh-sdk/go/provisions" "mesh-delivery-assigner/internal/judge" "mesh-delivery-assigner/internal/assign" @@ -434,6 +435,60 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) { if !self { t.Error("ace's own want does not say it composes its own once placed") } + + // ADR 0603 (L3, D15): one source revision changes a provider (db), its new consumer (app) and a build module (tool, whose + // recipe moved: ace runs its first build and the second, under the agreed toolchain, is the one the source revision names). + // The keys are written as their writers write them: the indexer's modules and manifests, the builder's builds, the + // controller's grants, the placement seat's placement and the provisioner's provisions. The build module's want moves at + // once and the provider's with it; the consumer's waits for its provision, with the words naming it, and moves once the + // provisioner provides it (rule 8, ADR 0570 §4): nothing waits for a turn or an order, only for a key. + offer := `"provides":[{"name":"database","scope":"mesh","answered-by":"helper"}]` + dbDigest := b.manifest(t, "db", `{"module":"db","version":"1",`+offer+`}`) + appDigest := b.manifest(t, "app", `{"module":"app","version":"1","requires":["database"]}`) + toolDigest := b.manifest(t, "tool", `{"module":"tool","version":"1"}`) + fpOf := func(c byte) string { return delivery.SourcePrefix2 + strings.Repeat(string(c), 64) } + toolOld := fpOf('1') + for module, row := range map[string]struct { + fp, digest string + }{"db": {fpOf('2'), dbDigest}, "app": {fpOf('3'), appDigest}, "tool": {fpOf('4'), toolDigest}} { + mod, _ := delivery.EncodeModule(delivery.Module{FP2: row.fp, Manifest: row.digest, Repository: "novox/" + module, Commit: strings.Repeat("2", 40), Path: delivery.RootPath}) + b.putJSON(t, delivery.ModulesBucket, module, mod) + fps := []string{row.fp} + if module == "tool" { + fps = append(fps, toolOld) + } + for _, fp := range fps { + built, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, By: "builder.novox.1", Step: "pushed", Manifest: row.digest}) + bkey, _ := delivery.BuilderBuildKey(module, fp) + b.putJSON(t, delivery.BuilderBuildsBucket, bkey, built) + gkey, _ := delivery.PartGrantKey(module, fp) + grant, _ := delivery.EncodeGrant(delivery.Grant{}) + b.putJSON(t, delivery.PartGrantsBucket, gkey, grant) + } + b.place(t, "ace", module) + } + runsOld, _ := delivery.EncodeHave(delivery.Have{FP: toolOld, Phase: delivery.PhaseRunning, Health: delivery.HealthOK}) + b.putJSON(t, delivery.HaveBucket, "ace.tool", runsOld) + asked := func(state, why string) { + raw, err := provisions.Encode(provisions.Provision{V: provisions.Version, Provision: "database", Consumer: provisions.End{Module: "app", Node: "ace"}, + Provider: provisions.End{Module: "db", Node: "ace"}, State: state, Why: why, At: time.Now()}) + if err != nil { + t.Fatal(err) + } + b.putJSON(t, provisions.ProvisionsBucket, "ace.app.database", raw) + } + asked(provisions.Asked, "the provider's build is not done") + if w := b.wantNow(t, "ace", "tool", "the build module's want at the second build", func(w delivery.Want) bool { return w.FP == fpOf('4') }); w.FP != fpOf('4') { + t.Errorf("tool's want = %+v", w) + } + b.wantNow(t, "ace", "db", "the provider's want at its fingerprint", func(w delivery.Want) bool { return w.FP == fpOf('2') }) + if e, err := b.buckets[delivery.AssignerWantsBucket].Get(b.ctx, "want.ace.app"); err == nil { + t.Errorf("app was wanted while its provision was asked: %s", e.Value()) + } + asked(provisions.Provided, "") + if w := b.wantNow(t, "ace", "app", "the consumer's want once its provision is provided", func(w delivery.Want) bool { return w.FP == fpOf('3') }); w.FP != fpOf('3') { + t.Errorf("app's want = %+v", w) + } } // 1297/B9 (the assigner's half, ADR 0528 clause 6): started whole, the assigner holds a fingerprint whose health check diff --git a/modules/mesh-delivery-builder/internal/build/run_test.go b/modules/mesh-delivery-builder/internal/build/run_test.go index 03d64ce4..55ed66ff 100644 --- a/modules/mesh-delivery-builder/internal/build/run_test.go +++ b/modules/mesh-delivery-builder/internal/build/run_test.go @@ -589,6 +589,45 @@ func TestAKeyIsBuiltOnlyWithTheToolchainItsFingerprintNamesAndNothingIsWrittenOt } } +// ADR 0603 §3 (D9): a source revision changing the Go build module's recipe and a Go module builds the module at once with +// the recipe the nodes run and again, a second key under the new fingerprint, once the agreement moves to the new recipe; +// the first key is neither rebuilt nor waited on. +func TestARecipeThatMovesBuildsTheModuleAgainUnderItsNewFingerprint(t *testing.T) { + r := newRig(t, goBundle) + r.consider(t, "modules/m") + calls := len(r.bm.calls) + fp2 := "src2:" + hex64('2') + for _, n := range []string{"ace", "novox"} { + b, err := delivery.EncodeToolchain(delivery.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "2", At: time.Now()}) + if err != nil { + t.Fatal(err) + } + r.tools.Put(buildrun.GoBuildModule, n+".go", b, false) + } + r.b.Reconsider(context.Background()) + r.b.Wait() + if len(r.bm.calls) != calls { + t.Errorf("%d calls, was %d: the first fingerprint was built again for the new recipe", len(r.bm.calls), calls) + } + data, err := delivery.EncodeModule(delivery.Module{Repository: "novox/mesh-catalog", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", FP: "src1:" + hex64('f'), FP2: fp2, + Manifest: mdigest('a'), Path: "modules/m", Toolchains: map[string]delivery.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "2"}}}) + if err != nil { + t.Fatal(err) + } + r.b.Module(context.Background(), "m", data) + r.b.Wait() + second, err := delivery.BuilderBuildKey("m", fp2) + if err != nil { + t.Fatal(err) + } + if v := valueOf(t, r.st, second); v.State != delivery.BuildDone || v.Toolchains["go"].Recipe != "2" { + t.Errorf("second build = %+v, want done with recipe 2", v) + } + if v := valueOf(t, r.st, r.key); v.State != delivery.BuildDone { + t.Errorf("first build = %+v, want it left done", v) + } +} + func TestNodesTheyDisagreeOnLeaveTheKeyUnwritten(t *testing.T) { // 1615/AC4 r := newRig(t, goBundle) r.tools.Put(buildrun.GoBuildModule, "ace.go", tcValue(t, "go", "go1.26.0"), false) diff --git a/modules/mesh-delivery-checker/internal/check/build_change.go b/modules/mesh-delivery-checker/internal/check/build_change.go new file mode 100644 index 00000000..bc57791a --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/build_change.go @@ -0,0 +1,66 @@ +package check + +import ( + "context" + "fmt" + "maps" + "slices" + "strings" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" + "git.novox.be/novox/mesh-sdk/go/catalogue" + + "git.novox.be/novox/mesh-delivery/sdk/graph" +) + +// A change to a build module or a base is built first, and what stands on it is composed from the manifests (novox/hq ADR +// 0603 §10): the check builds the build module (a candidate's mesh-build-go judges its own change, ADR 0535) and the bases, +// and composes the rest from their manifests, because a module built with the new recipe is built once the agreement +// moves, a second time (ADR 0603 §3). The verdict names the changed modules whose fingerprint names the build module or +// the base the change alters, as warnings of its composition: words for the operator, no part decides on them. + +// buildChangeWarnings are the composition warnings of a change that alters the Go build module, or a module other changed +// modules are built on: one line each, naming those modules, sorted. None for a change that alters neither, or when the +// manifests cannot be read (nothing is said that was not read). +func (st *state) buildChangeWarnings(ctx context.Context) []string { + changed := map[string]catalogue.Manifest{} + for _, p := range st.c.Pulls { + for module, cm := range p.Fps { + if cm.FP2 == "" || cm.Manifest == "" || st.j.Read == nil { + continue + } + m, err := st.j.Read.Manifest(ctx, cm.Manifest) + if err != nil { + st.j.logf("%s: the manifest of %s cannot be read, so the modules built with a changed build module or base are not named: %v", st.c.Key, module, err) + return nil + } + changed[module] = m + } + } + var out []string + for _, t := range st.targets { + if t.module != buildrun.GoBuildModule || !changesBuildGo(t.pull.Repository, t.changed) { + continue + } + var built []string + for module, m := range changed { + if m.Build != nil && slices.ContainsFunc(m.Build.Artifacts, func(a catalogue.Artifact) bool { return a.Language == "go" }) { + built = append(built, module) + } + } + slices.Sort(built) + if len(built) > 0 { + out = append(out, fmt.Sprintf("the change alters the build module %s: %s, built with it, are composed from their manifests and built again once the agreed toolchain moves (ADR 0603 §10)", + buildrun.GoBuildModule, strings.Join(built, ", "))) + } + break + } + g := graph.OfManifests(changed) + for _, base := range slices.Sorted(maps.Keys(changed)) { + if on := g.Dependents(base); len(on) > 0 { + out = append(out, fmt.Sprintf("the change alters %s, a base of %s: they are composed from their manifests and built after it (ADR 0603 §10)", + base, strings.Join(on, ", "))) + } + } + return out +} diff --git a/modules/mesh-delivery-checker/internal/check/composition_test.go b/modules/mesh-delivery-checker/internal/check/composition_test.go index 5e3e6d4e..eceeb202 100644 --- a/modules/mesh-delivery-checker/internal/check/composition_test.go +++ b/modules/mesh-delivery-checker/internal/check/composition_test.go @@ -3,9 +3,12 @@ package check import ( "context" "errors" + "maps" + "slices" "strings" "testing" + "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -99,3 +102,51 @@ func TestACompositionNotHadIsNotRun(t *testing.T) { type fixedTrunks struct{ heads map[string]string } func (f fixedTrunks) Heads() map[string]string { return f.heads } + +// ADR 0603 §10: a change that alters the Go build module, or a module others stand on, names in the composition's warnings +// (and so the check's line) the changed modules built with it, from their manifests; a change that alters neither says nothing. +func TestAChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) { + dig := func(c string) string { return strings.Repeat(c, 64) } + goBuild := &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "bin", Language: "go"}}} + for _, row := range []struct { + name string + changed []string + manifest map[string]catalogue.Manifest + want []string + }{ + {name: "the build module changes", changed: []string{buildGoFile}, + manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}, "mail": {Module: "mail", Build: goBuild}, "docs": {Module: "docs"}}, + want: []string{"mesh-build-go: mail, web, built with it"}}, + {name: "a base changes", changed: []string{"README.md"}, + manifest: map[string]catalogue.Manifest{"image": {Module: "image"}, + "web": {Module: "web", Build: &catalogue.Build{On: []catalogue.BuildsOn{{Arg: "BASE", Module: "image", Artifact: "img"}}}}}, + want: []string{"alters image, a base of web"}}, + {name: "neither changes", changed: []string{"README.md"}, + manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}}}, + } { + b := candidateBench(t, true) + b.fetch.Changed = row.changed + b.ex.Compositions = &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionPass, SDK: "x", Machines: []string{"g14"}}} + read, fps := fixedManifests{}, map[string]delivery.CandidateModule{} + for module, m := range row.manifest { + d := dig(string(rune('a' + len(read)))) + read[d], fps[module] = m, delivery.CandidateModule{FP2: "src2:" + dig("f"), Manifest: d} + } + b.ex.Read = read + p := pullOf(catalogueRepository, 9, head1) + p.Fps, p.Modules = fps, slices.Sorted(maps.Keys(fps)) + v := b.judge(t, caseOf(t, p)).Verdict + if v.State != delivery.VerdictPass || v.Composition == nil { + t.Fatalf("%s: %+v", row.name, v) + } + words := compositionWords(v.Composition, v.Took.Composition) + for _, w := range row.want { + if !strings.Contains(words, w) { + t.Errorf("%s: the line says %q, want it to name %q", row.name, words, w) + } + } + if row.want == nil && v.Composition.Words != "" { + t.Errorf("%s: warned %q though neither a build module nor a base changed", row.name, v.Composition.Words) + } + } +} diff --git a/modules/mesh-delivery-checker/internal/check/judge.go b/modules/mesh-delivery-checker/internal/check/judge.go index 9d7a2283..f15c9429 100644 --- a/modules/mesh-delivery-checker/internal/check/judge.go +++ b/modules/mesh-delivery-checker/internal/check/judge.go @@ -287,6 +287,7 @@ func (st *state) composed(ctx context.Context, v *delivery.Verdict) { v.Composition = &delivery.Composition{State: delivery.CompositionNotRun, Words: "not made: " + err.Error()} return } + comp.Warnings = append(comp.Warnings, st.buildChangeWarnings(ctx)...) // ADR 0603 §10 withComposition(v, comp) if v.Composition != nil && v.Composition.State != delivery.CompositionNotRun { v.Composition.FPSet = st.c.FPSet // a carry keeps it while the fingerprints stay the same (ADR 0548 §5) diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go index 28acdca8..051fe168 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go @@ -76,6 +76,11 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { if j, err := ix.JudgeAll(ctx, nil); err != nil || j.Written != 0 || len(said) != 1 { t.Fatalf("a judge that judges alike: %+v %v, said %d", j, err, len(said)) } + // ADR 0603 §4 (D9): a source revision that changes the indexer builds the new indexer, which at its start judges every + // manifest it holds again, none of them fresh: all of them judged, nothing written for a verdict that stands. + if j, err := ix.JudgeAtStart(ctx); err != nil || j.Judged != 2 || j.Written != 0 || len(said) != 1 { + t.Fatalf("the new indexer at its start: %+v %v, said %d, want both manifests judged and nothing written", j, err, len(said)) + } // The manifest mended: its new digest is accepted and the module's refusal goes. mended := hold("bad", `{"module":"bad","version":"2"}`) if j, err := ix.JudgeAll(ctx, map[string]bool{mended: true}); err != nil || j.Written != 1 || len(j.Refused) != 0 { From 9da14440e8ce770aa9e303ced44e257425ace06d Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 17:26:30 +0100 Subject: [PATCH 39/49] =?UTF-8?q?The=20assigner=20waits=20only=20on=20the?= =?UTF-8?q?=20keys=20of=20provisions=20the=20newest=20manifest=20still=20r?= =?UTF-8?q?equires:=20an=20older=20edge's=20asked=20or=20retiring=20key=20?= =?UTF-8?q?holds=20nothing=20(ADR=200603=20=C2=A79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mesh-delivery-assigner/internal/assign/wants.go | 8 +++++++- .../internal/assign/wants_test.go | 13 ++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/modules/mesh-delivery-assigner/internal/assign/wants.go b/modules/mesh-delivery-assigner/internal/assign/wants.go index ab020fca..a7f19d62 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants.go @@ -153,8 +153,14 @@ func (m Module) provisionWaiting(node string) (string, bool) { return fmt.Sprintf("provision %s has no key yet for %s on %s", missing[0], m.Name, node), true } keyed := m.Provisions(node) + // Only a provision the newest manifest still requires is waited on (ADR 0603 §9): a key of an older revision's edge, + // asked or retiring for the operator's answer (ADR 0570 §5), holds nothing. + needed := m.Manifest.Requires + if m.Answering != nil { + needed = catalogue.Awaited(m.Manifest, m.Answering) + } for _, key := range slices.Sorted(maps.Keys(keyed)) { - if p := keyed[key]; p.State != provisions.Provided { + if p := keyed[key]; p.State != provisions.Provided && slices.Contains(needed, p.Provision) { words := fmt.Sprintf("the provision %s of %s on %s is %s, not provided", key, m.Name, node, p.State) if p.Why != "" { words += ": " + p.Why diff --git a/modules/mesh-delivery-assigner/internal/assign/wants_test.go b/modules/mesh-delivery-assigner/internal/assign/wants_test.go index ba188a94..00c91352 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants_test.go @@ -108,14 +108,17 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { // asked for and requires oidc instead; the seat's keys follow the newest manifest, so the older edge is no wait: a provided // oidc moves the want, an asked one holds it and names oidc and never database. for _, c := range []struct { - name string - state string - moves bool - }{{"the replaced edge provided", provisions.Provided, true}, {"the replaced edge asked", provisions.Asked, false}} { + name string + state, older string + moves bool + }{{"the replaced edge provided, the older edge retiring", provisions.Provided, provisions.Retiring, true}, + {"the replaced edge provided, the older edge asked", provisions.Provided, provisions.Asked, true}, + {"the replaced edge asked, the older edge retiring", provisions.Asked, provisions.Retiring, false}} { m, n := ready() m.Manifest.Requires = []string{"oidc"} m.Provisions = func(node string) map[string]provisions.Provision { - return map[string]provisions.Provision{node + ".mail.oidc": {Provision: "oidc", State: c.state}} + return map[string]provisions.Provision{node + ".mail.oidc": {Provision: "oidc", State: c.state}, + node + ".mail.database": {Provision: "database", State: c.older, Why: "the operator's answer"}} } out := Wants(m, n, wantedOld(), nil) if got := out.Wants["g14"].FP == fNew; got != c.moves || (!c.moves && (!strings.Contains(out.Rules["g14"].Words, "oidc") || strings.Contains(out.Rules["g14"].Words, "database"))) { From effa12c4b409270dfe832e2298221f2ef151cad3 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 17:26:31 +0100 Subject: [PATCH 40/49] Fold the three new tests into existing ones (test budget); the base note says what the check does The check builds no artifacts and no changed base before its dependents; build_change.go now says only that they are composed from their manifests. Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- .../internal/build/run_test.go | 3 ++- .../internal/check/build_change.go | 16 +++++++++------- .../internal/check/composition_test.go | 3 ++- .../internal/index/toolchains_test.go | 3 ++- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/modules/mesh-delivery-builder/internal/build/run_test.go b/modules/mesh-delivery-builder/internal/build/run_test.go index 55ed66ff..3a042d25 100644 --- a/modules/mesh-delivery-builder/internal/build/run_test.go +++ b/modules/mesh-delivery-builder/internal/build/run_test.go @@ -565,6 +565,7 @@ func TestAKeyIsBuiltOnlyWithTheToolchainItsFingerprintNamesAndNothingIsWrittenOt r.b.Module(context.Background(), "m", moduleValueWith(t, "modules/m", old)) r.b.Wait() if len(r.st.vals) != 0 || len(r.bm.calls) != 0 { + t.Run("a recipe that moves builds the module again under its new fingerprint", aRecipeThatMovesBuildsTheModuleAgainUnderItsNewFingerprint) t.Errorf("keys %v and %d calls for a fingerprint of go1.26.0 while the nodes agree on go1.27.1, want neither", r.st.vals, len(r.bm.calls)) } // A recipe that differs is a different toolchain too, and a value that names none (written before) is not built. @@ -592,7 +593,7 @@ func TestAKeyIsBuiltOnlyWithTheToolchainItsFingerprintNamesAndNothingIsWrittenOt // ADR 0603 §3 (D9): a source revision changing the Go build module's recipe and a Go module builds the module at once with // the recipe the nodes run and again, a second key under the new fingerprint, once the agreement moves to the new recipe; // the first key is neither rebuilt nor waited on. -func TestARecipeThatMovesBuildsTheModuleAgainUnderItsNewFingerprint(t *testing.T) { +func aRecipeThatMovesBuildsTheModuleAgainUnderItsNewFingerprint(t *testing.T) { r := newRig(t, goBundle) r.consider(t, "modules/m") calls := len(r.bm.calls) diff --git a/modules/mesh-delivery-checker/internal/check/build_change.go b/modules/mesh-delivery-checker/internal/check/build_change.go index bc57791a..7065c080 100644 --- a/modules/mesh-delivery-checker/internal/check/build_change.go +++ b/modules/mesh-delivery-checker/internal/check/build_change.go @@ -13,11 +13,13 @@ import ( "git.novox.be/novox/mesh-delivery/sdk/graph" ) -// A change to a build module or a base is built first, and what stands on it is composed from the manifests (novox/hq ADR -// 0603 §10): the check builds the build module (a candidate's mesh-build-go judges its own change, ADR 0535) and the bases, -// and composes the rest from their manifests, because a module built with the new recipe is built once the agreement -// moves, a second time (ADR 0603 §3). The verdict names the changed modules whose fingerprint names the build module or -// the base the change alters, as warnings of its composition: words for the operator, no part decides on them. +// A change to a build module or a base (novox/hq ADR 0603 §10), as the check can say it. What the check runs is its own +// tests: a candidate Go build module judges its own change (runner workspace, ADR 0535); the check builds no module's +// artifacts, nor a changed base before what stands on it. A base's fingerprint enters its dependents' fingerprints (the +// indexer), so they are other builds once it changes, and the build module's recipe enters the fingerprint of every module +// it builds, so each is built again once the agreed toolchain moves (ADR 0603 §3). The composition composes them from their +// manifests, and the verdict names the changed modules this concerns, as warnings of its composition: words for the +// operator, no part decides on them. // buildChangeWarnings are the composition warnings of a change that alters the Go build module, or a module other changed // modules are built on: one line each, naming those modules, sorted. None for a change that alters neither, or when the @@ -50,7 +52,7 @@ func (st *state) buildChangeWarnings(ctx context.Context) []string { } slices.Sort(built) if len(built) > 0 { - out = append(out, fmt.Sprintf("the change alters the build module %s: %s, built with it, are composed from their manifests and built again once the agreed toolchain moves (ADR 0603 §10)", + out = append(out, fmt.Sprintf("the change alters the build module %s: %s, built with it, are composed from their manifests, not built by the check, and built again once the agreed toolchain moves (ADR 0603 §3, §10)", buildrun.GoBuildModule, strings.Join(built, ", "))) } break @@ -58,7 +60,7 @@ func (st *state) buildChangeWarnings(ctx context.Context) []string { g := graph.OfManifests(changed) for _, base := range slices.Sorted(maps.Keys(changed)) { if on := g.Dependents(base); len(on) > 0 { - out = append(out, fmt.Sprintf("the change alters %s, a base of %s: they are composed from their manifests and built after it (ADR 0603 §10)", + out = append(out, fmt.Sprintf("the change alters %s, a base of %s: their fingerprints name its fingerprint, so they are composed from their manifests, not built by the check (ADR 0603 §10)", base, strings.Join(on, ", "))) } } diff --git a/modules/mesh-delivery-checker/internal/check/composition_test.go b/modules/mesh-delivery-checker/internal/check/composition_test.go index eceeb202..c8dc90c3 100644 --- a/modules/mesh-delivery-checker/internal/check/composition_test.go +++ b/modules/mesh-delivery-checker/internal/check/composition_test.go @@ -33,6 +33,7 @@ func (f *fakeCompositions) Compose(ctx context.Context, c *Case) (delivery.Compo // 1297/AC3 (its unit half): a check whose tests pass takes the assigner's composition of it, by the check's own key, // into its verdict: a pass says it passed with the mesh-sdk and machines it composed, and is no longer "not run". func TestAPassingCheckTakesTheAssignersComposition(t *testing.T) { + t.Run("a change to a build module or base names the modules built on it", aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt) b := newBench(t) f := &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionPass, SDK: "v0.1.14-x", Machines: []string{"g14"}, Took: 40}} b.ex.Compositions = f @@ -105,7 +106,7 @@ func (f fixedTrunks) Heads() map[string]string { return f.heads } // ADR 0603 §10: a change that alters the Go build module, or a module others stand on, names in the composition's warnings // (and so the check's line) the changed modules built with it, from their manifests; a change that alters neither says nothing. -func TestAChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) { +func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) { dig := func(c string) string { return strings.Repeat(c, 64) } goBuild := &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "bin", Language: "go"}}} for _, row := range []struct { diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go index ffac6f5f..2510e054 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go @@ -135,6 +135,7 @@ func reportToolchainRecipe(t *testing.T, ix *Indexer, module, node, language, ve func TestAgreedToolchainChangesAGoModulesReasonWithNoMerge(t *testing.T) { // 1444/AC5 ix, mods := newFormIndexer(t, mod{"avahi", catalogueManifest(t, "avahi")}) if m, _ := moduleOf(t, mods, "avahi"); m.None2 != delivery.ToolchainNoReport { + t.Run("a recipe change builds a Go module at once and again once the agreement moves", aRecipeChangeBuildsAGoModuleAtOnceAndAgainOnceTheAgreementMoves) t.Fatalf("before: none2 = %q", m.None2) } // The release that last merged avahi: its source revision names avahi's commit, so Src (and FPSrc) are 7. @@ -183,7 +184,7 @@ func TestAgreedToolchainChangesAGoModulesReasonWithNoMerge(t *testing.T) { // 14 // ADR 0603 §3 (D9): a source revision that changes the Go build module's recipe and a Go module builds the module at once, with // the recipe the nodes run, and again once the agreement moves to the new one: the module never waits and never loses its // fingerprint between the two, and its toolchain edge names the recipe each was computed with. -func TestARecipeChangeBuildsAGoModuleAtOnceAndAgainOnceTheAgreementMoves(t *testing.T) { +func aRecipeChangeBuildsAGoModuleAtOnceAndAgainOnceTheAgreementMoves(t *testing.T) { ix, mods := newFormIndexer(t, mod{"avahi", catalogueManifest(t, "avahi")}) for _, node := range []string{"novox", "ace"} { reportToolchain(t, ix, "mesh-build-go", node, "go", "go1.27.1") From 45bd63427fadf635a7a8ce5f7086735fee9f66a9 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 17:26:45 +0100 Subject: [PATCH 41/49] =?UTF-8?q?A=20wait=20the=20watchman=20mirrors=20kee?= =?UTF-8?q?ps=20its=20earliest=20start=20while=20the=20same=20mark=20stand?= =?UTF-8?q?s,=20so=20a=20key=20written=20again=20with=20new=20words=20does?= =?UTF-8?q?=20not=20start=20the=20bound=20again=20(ADR=200603=20=C2=A78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/watch/findings_test.go | 8 ++++++ .../internal/watch/mirror.go | 11 +++++--- .../internal/watch/signals.go | 2 +- .../internal/watch/watch.go | 28 +++++++++++++++++-- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/modules/mesh-delivery-watchman/internal/watch/findings_test.go b/modules/mesh-delivery-watchman/internal/watch/findings_test.go index b0a04e78..40e308b2 100644 --- a/modules/mesh-delivery-watchman/internal/watch/findings_test.go +++ b/modules/mesh-delivery-watchman/internal/watch/findings_test.go @@ -476,6 +476,14 @@ func TestTheWatchmansFindings(t *testing.T) { if len(o.pulses) != 3 || len(o.findings) != 0 { t.Fatalf("pulses %v findings %v", o.pulses, o.findings) } + // The keys are written again as their parts move on, with new words and a new time: the wait still began at t0. + c.t = c.t.Add(20 * time.Minute) + held2, _ := delivery.EncodeWant(delivery.Want{FP: fpOld, Self: true, Newest: fpNew, Rule: delivery.RuleCanary, Words: "the canary ace runs it, not healthy yet"}) + asked2, _ := provisions.Encode(provisions.Provision{Provision: "oidc", Consumer: provisions.End{Module: "photos", Node: "ace"}, + Provider: provisions.End{Module: "keycloak", Node: "novox"}, State: provisions.Asked, Why: "the provider is applying", At: c.t}) + w.Want("want.ace.web", held2, Put, c.t) + w.Provision("ace.photos.oidc", asked2, Put) + w.Build(bkey, queued, Put, c.t) for bucket, key := range waits { expire(w, c, pulseKeyOf(t, bucket, key), 31*time.Minute) } diff --git a/modules/mesh-delivery-watchman/internal/watch/mirror.go b/modules/mesh-delivery-watchman/internal/watch/mirror.go index b2f0e180..76fd2cf5 100644 --- a/modules/mesh-delivery-watchman/internal/watch/mirror.go +++ b/modules/mesh-delivery-watchman/internal/watch/mirror.go @@ -102,13 +102,16 @@ func (w *Watchman) Want(key string, value []byte, op Op, at time.Time) { at = w.now() } w.held[key] = true - w.mirrorWithin(w.stepBound, delivery.AssignerWantsBucket, key, + w.mirrorKept(delivery.AssignerWantsBucket, key, describe("the hold on %s on %s at %s (rule %d) to end, bound %s: %s", module, node, shortFP(want.FP), want.Rule, w.stepBound, want.Words), - "held|"+want.FP+"|"+want.Newest, at, 0) + "held|"+want.FP+"|"+want.Newest, at) return } if w.held[key] { delete(w.held, key) + if pk, err := delivery.PulseKey(delivery.AssignerWantsBucket, key); err == nil { + delete(w.starts, pk) + } w.stuckMoved(delivery.AssignerWantsBucket, key) // the hold ended: its finding is cleared } if w.have(node, module) == want.FP { @@ -156,8 +159,8 @@ func (w *Watchman) Build(key string, value []byte, op Op, at time.Time) { if at.IsZero() { at = w.now() } - w.mirrorWithin(w.stepBound, delivery.BuilderBuildsBucket, key, - describe("the package %s to be published before the build of %s at %s, bound %s", b.Waits, module, shortFP(fp), w.stepBound), b.Waits, at, 0) + w.mirrorKept(delivery.BuilderBuildsBucket, key, + describe("the package %s to be published before the build of %s at %s, bound %s", b.Waits, module, shortFP(fp), w.stepBound), b.Waits, at) } // Have is a change of a key of the node-engine's have bucket: the fingerprint a node runs of a module. One that names the diff --git a/modules/mesh-delivery-watchman/internal/watch/signals.go b/modules/mesh-delivery-watchman/internal/watch/signals.go index 9c2e8501..042a5321 100644 --- a/modules/mesh-delivery-watchman/internal/watch/signals.go +++ b/modules/mesh-delivery-watchman/internal/watch/signals.go @@ -632,7 +632,7 @@ func (w *Watchman) askedWait(key string, value []byte, op Op) { if p.Why != "" { words += ": " + p.Why } - w.mirrorWithin(w.stepBound, bucket, key, words, "asked", since, 0) + w.mirrorKept(bucket, key, words, "asked", since) } func sortedKeys(m map[string]string) []string { diff --git a/modules/mesh-delivery-watchman/internal/watch/watch.go b/modules/mesh-delivery-watchman/internal/watch/watch.go index ef16ebbd..0e8b7925 100644 --- a/modules/mesh-delivery-watchman/internal/watch/watch.go +++ b/modules/mesh-delivery-watchman/internal/watch/watch.go @@ -64,8 +64,10 @@ type Watchman struct { keptSince map[string]time.Time wants map[string]string // the fingerprint of each want that stands, by want key haves map[string]string // the fingerprint each node runs of a module, by have key - held map[string]bool // each want key held by a rule (ADR 0603 §8), mirrored as the hold's wait - bound time.Duration + // starts is, by pulse key, the first start of a wait whose mark still stands (mirrorKept). + starts map[string]startMark + held map[string]bool // each want key held by a rule (ADR 0603 §8), mirrored as the hold's wait + bound time.Duration // findings is every finding written, by key, as written; seededFindings those read at the start and not yet observed // again. findings map[string][]byte @@ -104,7 +106,7 @@ type Watchman struct { // New is a watchman writing to out, reading the time from now. func New(out Out, now func() time.Time) *Watchman { return &Watchman{out: out, now: now, counts: map[string]int{}, presence: map[string]presence{}, - pulses: map[string]*pulse{}, stuck: map[string]delivery.Stuck{}, wants: map[string]string{}, haves: map[string]string{}, held: map[string]bool{}, bound: delivery.PulseMoveBound, stepBound: StepBound, + pulses: map[string]*pulse{}, stuck: map[string]delivery.Stuck{}, wants: map[string]string{}, haves: map[string]string{}, held: map[string]bool{}, starts: map[string]startMark{}, bound: delivery.PulseMoveBound, stepBound: StepBound, findings: map[string][]byte{}, seededFindings: map[string]bool{}, signals: map[string]signalDue{}, away: map[string]bool{}, haveState: map[string]delivery.Have{}, unhealthy: map[string]int{}, advised: map[string]int{}, advisedSince: map[string]time.Time{}, control: map[string]bool{}, failedSince: map[string]time.Time{}, provFailed: map[string]map[string]ProvReport{}, closures: map[string]map[string]time.Time{}, waiting: map[string]map[string]string{}, @@ -162,6 +164,25 @@ func (w *Watchman) mirrorWithin(bound time.Duration, bucket, key, waited, head s w.pulses[pk] = &pulse{bucket: bucket, key: key, waited: waited, since: since, head: head, token: token} } +// startMark is when a wait began, for the mark that names it. +type startMark struct { + mark string + since time.Time +} + +// mirrorKept is mirrorWithin for a wait that is the key's value standing as it is (ADR 0603 §8): while the same mark stands +// (a provision asked, a want held at the same fingerprints, a build waiting for the same package), a rewrite of the key (new +// words, a new time) does not start the bound again; the earliest start is kept. A different mark is a new wait. +func (w *Watchman) mirrorKept(bucket, key, waited, mark string, since time.Time) { + if pk, err := delivery.PulseKey(bucket, key); err == nil { + if cur, ok := w.starts[pk]; ok && cur.mark == mark && cur.since.Before(since) { + since = cur.since + } + w.starts[pk] = startMark{mark: mark, since: since} + } + w.mirrorWithin(w.stepBound, bucket, key, waited, mark, since, 0) +} + func (w *Watchman) deletePulse(pk string) { delete(w.pulses, pk) if err := w.out.DeletePulse(pk); err != nil { @@ -176,6 +197,7 @@ func (w *Watchman) moved(bucket, key string) { if err != nil { return } + delete(w.starts, pk) if _, ok := w.pulses[pk]; ok { w.deletePulse(pk) } From 453cdfa6070cb54977bccff4567a5c91a52597a1 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 17:51:09 +0100 Subject: [PATCH 42/49] =?UTF-8?q?The=20check=20waits=20on=20the=20builder'?= =?UTF-8?q?s=20builds=20of=20its=20candidate=20before=20it=20composes,=20s?= =?UTF-8?q?o=20it=20composes=20with=20them=20(ADR=200603=20=C2=A710)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The builder already builds every candidate fingerprint into its own key. The check now watches exactly those keys on the bus and composes once each is done or failed, within the new setting build-wait-bound (1800 s). A failed build fails the check, naming it; a build not ended within the bound is named and the check composes without it. A module built by a build module the change alters (mesh-build-go or mesh-build-node), one standing on a base the change alters, and one the trunk holds no record of are not waited on, and the composition's warnings name each with why. Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- .../mesh-delivery-checker/integration_test.go | 47 ++++++- .../cmd/mesh-delivery-checker/judge.go | 61 +++++++++- .../cmd/mesh-delivery-checker/main.go | 10 +- .../cmd/mesh-delivery-checker/settings.go | 49 +++++--- .../mesh-delivery-checker/settings_test.go | 27 ++-- .../mesh-delivery-checker/files/settings.json | 3 +- .../internal/check/build_change.go | 91 ++++++++++---- .../internal/check/builds.go | 69 +++++++++++ .../internal/check/composition_test.go | 115 +++++++++++++++--- .../internal/check/judge.go | 8 +- .../mesh-delivery-checker/manifest_test.go | 11 +- modules/mesh-delivery-checker/module.json | 11 +- 12 files changed, 421 insertions(+), 81 deletions(-) diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go index 4a03d309..d2dc3250 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go @@ -398,7 +398,7 @@ func startBed(t *testing.T) *bed { delivery.CheckerOwedBucket: delivery.CheckerOwedHistory, delivery.PreviewsBucket: delivery.PreviewsHistory, delivery.CandidatesBucket: delivery.CandidatesHistory, delivery.ModulesBucket: delivery.ModulesHistory, delivery.ManifestsBucket: delivery.ManifestsHistory, delivery.SourceBucket: delivery.SourceHistory, delivery.PresenceBucket: delivery.PresenceHistory, delivery.HaveBucket: delivery.HaveHistory, check.TriesBucket: 1, - delivery.TurnsBucket: delivery.TurnsHistory} { + delivery.TurnsBucket: delivery.TurnsHistory, delivery.BuilderBuildsBucket: 1} { kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: uint8(history)}) if err != nil { t.Fatal(err) @@ -578,12 +578,50 @@ func (b *bed) owed(t *testing.T, number int, head, state string) delivery.Owed { // TestCheckerJudgesACandidateAndOwesItsStatus: a candidate the indexer wrote, one pull request of mesh-delivery, is fetched // merged onto the trunk the gate names, digested and tested at both levels on the node, kept, previewed; the verdict passes // with the counts of both levels and the composition not run, and the status mesh/check is owed on the head as success. -func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300/AC5, 1445/AC1, 1929/AC1 +func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300/AC5, 1445/AC1, 1929/AC1, ADR 0603 §10 b := startBed(t) - key := b.candidate(t, "feat/x", pull(repo, 7, head1)) + // ADR 0603 §10: the candidate changes the module m, whose manifest the trunk holds; the builder has not built m's + // candidate fingerprint yet, so the check waits on the builder's key before it composes. + manifest := `{"module":"m"}` + digest := delivery.ManifestDigest([]byte(manifest)) + stored, err := delivery.EncodeManifest(delivery.Manifest{Module: "m", JSON: manifest}) + if err != nil { + t.Fatal(err) + } + mkey, _ := delivery.ManifestsKey(digest) + if _, err := b.buckets[delivery.ManifestsBucket].Put(b.ctx, mkey, stored); err != nil { + t.Fatal(err) + } + known, err := delivery.EncodeModule(delivery.Module{Repository: repo, Commit: trunk, Path: "modules/m", Manifest: digest, None2: "the fingerprint is computed with its chain"}) + if err != nil { + t.Fatal(err) + } + if _, err := b.buckets[delivery.ModulesBucket].Put(b.ctx, "m", known); err != nil { + t.Fatal(err) + } + fp := "src2:" + strings.Repeat("f", 64) + p := pull(repo, 7, head1) + p.Modules, p.Fps = []string{"m"}, map[string]delivery.CandidateModule{"m": {FP2: fp, Manifest: digest}} + key := b.candidate(t, "feat/x", p) // ADR 0593 §7, §9 (D12): n1 and n2 are placed; n1's engine answers compose-candidate on the bus, n2's never does. asked := b.engine(t, "n1", "n2") b.start(t, nil) + b.said(t, "waits on the builder's builds of m before it composes") + if got := asked(); len(got) != 0 { + t.Fatalf("the engines were asked %+v before m's build ended", got) + } + bkey, _ := delivery.BuilderBuildKey("m", fp) + building, _ := json.Marshal(delivery.BuilderBuild{V: 1, State: delivery.BuildBuilding}) + if _, err := b.buckets[delivery.BuilderBuildsBucket].Put(b.ctx, bkey, building); err != nil { + t.Fatal(err) + } + if v := b.verdict(t, key, func(delivery.Verdict) bool { return true }); v.State != delivery.VerdictRunning || len(asked()) != 0 { + t.Fatalf("verdict %s and %d asks while m builds: the check composes only once its build ended", v.State, len(asked())) + } + built, _ := json.Marshal(delivery.BuilderBuild{V: 1, State: delivery.BuildDone, Manifest: digest}) + if _, err := b.buckets[delivery.BuilderBuildsBucket].Put(b.ctx, bkey, built); err != nil { + t.Fatal(err) + } v := b.verdict(t, key, ended) if v.State != delivery.VerdictPass || v.Levels[delivery.LevelUnit].Run != 2 || v.Levels[delivery.LevelIntegration].Run != 2 || v.Trunk[repo] != trunk { t.Fatalf("verdict = %+v", v) @@ -592,6 +630,9 @@ func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300 !strings.Contains(c.Words, "n2: "+delivery.ComposeNoAnswer) || len(v.Machines) != 1 || v.Machines[0] != "n1" { t.Errorf("composition %+v machines %v", v.Composition, v.Machines) } + if v.Composition != nil && strings.Contains(v.Composition.Words, "did not end") { + t.Errorf("the composition says %q: m's build ended within the bound", v.Composition.Words) + } if got := asked(); len(got) == 0 || got[0].Candidate != key { t.Errorf("n1's engine was asked %+v, want the check's candidate %s", got, key) } diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go index 532f5fff..949f4376 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go @@ -8,6 +8,7 @@ import ( "sort" "strings" "sync" + "time" "github.com/nats-io/nats.go/jetstream" @@ -32,6 +33,10 @@ type judgeDeps struct { forge check.Forge announce check.Announcer builds check.BuildStates + // ends waits on the builder's builds of a candidate before the check composes (ADR 0603 §10); nil composes without waiting. + ends check.BuildEnds + // buildWait is the setting build-wait-bound; check.DefaultBuildWait when 0. + buildWait time.Duration // compositions makes a check's composition from the engines' answers (ADR 0593 §7); nil leaves it not run. compositions check.CompositionReader // nodes names every node with a record; nil hands no names. @@ -98,7 +103,7 @@ func newJudge(d judgeDeps) check.Judge { nodes = d.nodes } return &check.Examiner{Nodes: nodes, Compositions: d.compositions, Asks: d.asks, Toolchains: d.tools, - Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Predefined: d.predefined, Logf: d.logf} + Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Predefined: d.predefined, Logf: d.logf} } // manifestKV reads a manifest by its digest. @@ -294,6 +299,60 @@ func (r buildReader) Build(ctx context.Context, module, fp2 string) (delivery.Bu return b, err == nil, err } +// buildEnds hears the builder's build keys of a candidate on the bus (mesh-delivery-builder_builds): a watch of exactly +// those keys gives each one's value now and every write the builder makes after, so the check waits on the part that owns +// the fact and polls nothing (novox/hq ADR 0603 §10). +type buildEnds struct{ kv jetstream.KeyValue } + +func (e buildEnds) Ended(ctx context.Context, builds map[string]string) (map[string]delivery.BuilderBuild, error) { + byKey := map[string]string{} + keys := make([]string, 0, len(builds)) + for module, fp2 := range builds { + key, err := delivery.BuilderBuildKey(module, fp2) + if err != nil { + return nil, err + } + byKey[key] = module + keys = append(keys, key) + } + ended := map[string]delivery.BuilderBuild{} + if len(keys) == 0 { + return ended, nil + } + w, err := e.kv.WatchFiltered(ctx, keys) + if err != nil { + return nil, err + } + defer func() { _ = w.Stop() }() + for len(ended) < len(keys) { + select { + case <-ctx.Done(): + return ended, nil // the bound: the builds not ended are left out + case entry, open := <-w.Updates(): + if !open { + return ended, nil + } + if entry == nil || entry.Operation() != jetstream.KeyValuePut { + continue // the end of the values now, or a key deleted: it has not ended + } + module, ours := byKey[entry.Key()] + if !ours { + continue + } + b, err := delivery.DecodeBuilderBuild(entry.Value()) + if err != nil { + continue // not a build the check can read: it has not ended for the check + } + if b.State == delivery.BuildDone || b.State == delivery.BuildFailed { + ended[module] = b + } else { + delete(ended, module) // asked again (builder_build_again): it runs once more + } + } + } + return ended, nil +} + // ManifestDigests is the manifest digest of every module the indexer holds, by module: the catalogue the indexer's judge // judges a change's manifests in (novox/hq ADR 0578 §2). func (r *repoModules) ManifestDigests() map[string]string { diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go index ef544bcb..8ca5d68c 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go @@ -166,14 +166,16 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // The builder's builds say how the candidates' fingerprints stand (issue 1684); where its bucket is not there yet the line // says nothing of the builds, until the checker starts again. var builds check.BuildStates - if kv := process.OptionalBucket(ctx, js, delivery.BuilderBuildsBucket, "the line says nothing of the builds until the checker starts again", logger.Printf); kv != nil { - builds = buildReader{kvStore{kv}} + var ends check.BuildEnds + if kv := process.OptionalBucket(ctx, js, delivery.BuilderBuildsBucket, "the line says nothing of the builds, and a check composes without waiting on them, until the checker starts again", logger.Printf); kv != nil { + builds, ends = buildReader{kvStore{kv}}, buildEnds{kv} } // The check's composition is the checker's, from what each placed node's engine answers (ADR 0593 §7, §9). - answerBound, err := composeAnswerBound(getenv("MESH_CHECKER_SETTINGS")) + set, err := readSettings(getenv("MESH_CHECKER_SETTINGS")) if err != nil { return err } + answerBound := set.answer holdCallsFor(answerBound) verdicts := &manifestVerdicts{} compositions := check.EngineCompositions{ @@ -212,7 +214,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro ch.Judge = newJudge(judgeDeps{ asks: buildrun.Asker{Publisher: asks, Waiter: waiter, Bound: stepBound}, tools: tools, trunks: trunks, kept: kvStore{buckets[delivery.CheckerKeptBucket]}, previews: kvStore{buckets[delivery.PreviewsBucket]}, - repos: repos, published: published, builds: builds, forge: forgeReads{call: caller.Call}, logf: logger.Printf, + repos: repos, published: published, builds: builds, ends: ends, buildWait: set.buildWait, forge: forgeReads{call: caller.Call}, logf: logger.Printf, announce: announce, compositions: compositions, nodes: openNodeNames(ctx, js, logger.Printf), predefined: keptPredefined{process.AwaitBucket(ctx, js, conn, delivery.IndexerModule+"_"+delivery.IndexerPredefinedState, diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/settings.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/settings.go index c3c44afd..19d895f3 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/settings.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/settings.go @@ -10,38 +10,57 @@ import ( "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" + + "mesh-delivery-checker/internal/check" ) // The checker's own settings (its config, written by the node-engine into ${dir:state}/settings.json, named to the // process by MESH_CHECKER_SETTINGS): compose-answer-bound, the seconds a check waits for a node's engine to answer -// compose-candidate (novox/hq ADR 0593 §7, §9: 60 by default; the checker owns it). +// compose-candidate (novox/hq ADR 0593 §7, §9: 60 by default), and build-wait-bound, the seconds a check waits for the +// builder's builds of its candidate before it composes (ADR 0603 §10: 1800 by default). The checker owns both. -// composeAnswerBound reads compose-answer-bound from the settings file at path: delivery.ComposeAnswerBound where no -// file or no value is there; a value that is no number of seconds from 1 to 600 is refused, naming it. -func composeAnswerBound(path string) (time.Duration, error) { +// settings are the checker's settings as read. +type settings struct { + // answer is compose-answer-bound; buildWait is build-wait-bound. + answer, buildWait time.Duration +} + +// readSettings reads the settings file at path: each value's default where no file or no value is there; a value that is +// no whole number of seconds within its range is refused, naming it. +func readSettings(path string) (settings, error) { + out := settings{answer: delivery.ComposeAnswerBound, buildWait: check.DefaultBuildWait} if path == "" { - return delivery.ComposeAnswerBound, nil + return out, nil } raw, err := os.ReadFile(path) if errors.Is(err, fs.ErrNotExist) { - return delivery.ComposeAnswerBound, nil + return out, nil } if err != nil { - return 0, err + return settings{}, err } var s struct { - Bound *float64 `json:"compose-answer-bound"` + Answer *float64 `json:"compose-answer-bound"` + BuildWait *float64 `json:"build-wait-bound"` } if err := json.Unmarshal(raw, &s); err != nil { - return 0, fmt.Errorf("%s is no settings file: %w", path, err) + return settings{}, fmt.Errorf("%s is no settings file: %w", path, err) } - if s.Bound == nil { - return delivery.ComposeAnswerBound, nil + for _, v := range []struct { + name string + value *float64 + min, max float64 + into *time.Duration + }{{"compose-answer-bound", s.Answer, 1, 600, &out.answer}, {"build-wait-bound", s.BuildWait, 60, 7200, &out.buildWait}} { + if v.value == nil { + continue + } + if *v.value < v.min || *v.value > v.max || *v.value != float64(int(*v.value)) { + return settings{}, fmt.Errorf("%s is %v: a whole number of seconds from %v to %v", v.name, *v.value, v.min, v.max) + } + *v.into = time.Duration(*v.value) * time.Second } - if *s.Bound < 1 || *s.Bound > 600 || *s.Bound != float64(int(*s.Bound)) { - return 0, fmt.Errorf("compose-answer-bound is %v: a whole number of seconds from 1 to 600", *s.Bound) - } - return time.Duration(*s.Bound) * time.Second, nil + return out, nil } // holdCallsFor makes a bus call wait the compose-answer-bound (novox/hq ADR 0593 §7), not the SDK's CallTimeout of 30 s: diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/settings_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/settings_test.go index ad9a9863..02e1cc14 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/settings_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/settings_test.go @@ -8,19 +8,25 @@ import ( "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" + + "mesh-delivery-checker/internal/check" ) // ADR 0593 §7: the answer bound is the checker's setting compose-answer-bound, 60 s when the file or the value is -// absent; a value that is no whole number of seconds from 1 to 600 stops the start, naming it. +// absent; a value that is no whole number of seconds from 1 to 600 stops the start, naming it. ADR 0603 §10: the wait for +// the candidate's builds is build-wait-bound, 30 minutes when absent, from 60 to 7200 seconds. func TestTheAnswerBoundIsTheCheckersSetting(t *testing.T) { dir := t.TempDir() for _, row := range []struct { - body string - want time.Duration - err bool - }{{"", delivery.ComposeAnswerBound, false}, {`{}`, delivery.ComposeAnswerBound, false}, - {`{"compose-answer-bound": 90}`, 90 * time.Second, false}, {`{"compose-answer-bound": 0}`, 0, true}, - {`{"compose-answer-bound": 1.5}`, 0, true}} { + body string + want time.Duration + err bool + buildWait time.Duration + }{{"", delivery.ComposeAnswerBound, false, check.DefaultBuildWait}, {`{}`, delivery.ComposeAnswerBound, false, check.DefaultBuildWait}, + {`{"compose-answer-bound": 90}`, 90 * time.Second, false, check.DefaultBuildWait}, {`{"compose-answer-bound": 0}`, 0, true, 0}, + {`{"compose-answer-bound": 1.5}`, 0, true, 0}, + {`{"compose-answer-bound": 60, "build-wait-bound": 600}`, 60 * time.Second, false, 10 * time.Minute}, + {`{"build-wait-bound": 30}`, 0, true, 0}} { path := filepath.Join(dir, "settings.json") os.Remove(path) if row.body != "" { @@ -28,9 +34,10 @@ func TestTheAnswerBoundIsTheCheckersSetting(t *testing.T) { t.Fatal(err) } } - got, err := composeAnswerBound(path) - if (err != nil) != row.err || got != row.want { - t.Errorf("%q: %s, %v", row.body, got, err) + set, err := readSettings(path) + got := set.answer + if (err != nil) != row.err || got != row.want || set.buildWait != row.buildWait { + t.Errorf("%q: %s, %s, %v", row.body, got, set.buildWait, err) } // Issue 1126: the SDK's call gives up at CallTimeout (30 s), so the checker's calls must wait the bound read // (an answer at 31 s is heard under 60 s), and the bound never shortens the SDK's own. diff --git a/modules/mesh-delivery-checker/files/settings.json b/modules/mesh-delivery-checker/files/settings.json index b9a19671..c6d811e8 100644 --- a/modules/mesh-delivery-checker/files/settings.json +++ b/modules/mesh-delivery-checker/files/settings.json @@ -1,3 +1,4 @@ { - "compose-answer-bound": 60 + "compose-answer-bound": 60, + "build-wait-bound": 1800 } diff --git a/modules/mesh-delivery-checker/internal/check/build_change.go b/modules/mesh-delivery-checker/internal/check/build_change.go index 7065c080..d22339e4 100644 --- a/modules/mesh-delivery-checker/internal/check/build_change.go +++ b/modules/mesh-delivery-checker/internal/check/build_change.go @@ -13,56 +13,99 @@ import ( "git.novox.be/novox/mesh-delivery/sdk/graph" ) -// A change to a build module or a base (novox/hq ADR 0603 §10), as the check can say it. What the check runs is its own -// tests: a candidate Go build module judges its own change (runner workspace, ADR 0535); the check builds no module's -// artifacts, nor a changed base before what stands on it. A base's fingerprint enters its dependents' fingerprints (the -// indexer), so they are other builds once it changes, and the build module's recipe enters the fingerprint of every module -// it builds, so each is built again once the agreed toolchain moves (ADR 0603 §3). The composition composes them from their -// manifests, and the verdict names the changed modules this concerns, as warnings of its composition: words for the -// operator, no part decides on them. +// What the check builds of a candidate before it composes (novox/hq ADR 0603 §10). The builder builds every fingerprint the +// indexer's candidate carries, as a key of its own (builds..), with the build modules that run; the +// check adds no build path of its own and waits on those keys (awaitBuilds), so the node-engines compose the candidate's +// modules with their builds. It does not wait on two kinds of changed module, and names them in its composition's warnings: +// +// - one built by a build module the change alters (the recipe of mesh-build-go or mesh-build-node enters the fingerprint +// of every module it builds, ADR 0530 §1): its candidate build is the running build module's, and the check cannot run +// the candidate's build module to build it (it runs it only for its own digest and tests, ADR 0535 §1), so it is not +// composed with a build of the changed build module; it is built again once the agreed toolchain moves (ADR 0603 §3). +// The build module's own build is waited on, as any other: the running build module builds its new source. +// - one that stands on a module the change alters (build.on): its candidate fingerprint names the base's trunk +// fingerprint, and no build module builds on another module's artifact (ADR 0487 §1), so its build stands on the +// trunk's base; the base's own build is waited on. +// +// A module the trunk holds no record of yet is not built by the builder until it is merged, so it is not waited on either. -// buildChangeWarnings are the composition warnings of a change that alters the Go build module, or a module other changed -// modules are built on: one line each, naming those modules, sorted. None for a change that alters neither, or when the -// manifests cannot be read (nothing is said that was not read). -func (st *state) buildChangeWarnings(ctx context.Context) []string { +// buildsOfTheCheck are the builds the check waits on before it composes (module to fingerprint) and the words for the +// changed modules it composes without waiting on their build, one line each, sorted. A manifest that cannot be read leaves +// every build waited on and nothing said of build modules or bases (nothing is said that was not read). +func (st *state) buildsOfTheCheck(ctx context.Context) (map[string]string, []string) { + wait := map[string]string{} changed := map[string]catalogue.Manifest{} + readable := true for _, p := range st.c.Pulls { for module, cm := range p.Fps { - if cm.FP2 == "" || cm.Manifest == "" || st.j.Read == nil { + if cm.FP2 == "" || cm.Manifest == "" { + continue // the builder builds no fingerprint of it (candidates.go: none of the new form, or no manifest) + } + wait[module] = cm.FP2 + if !readable || st.j.Read == nil { + readable = false continue } m, err := st.j.Read.Manifest(ctx, cm.Manifest) if err != nil { - st.j.logf("%s: the manifest of %s cannot be read, so the modules built with a changed build module or base are not named: %v", st.c.Key, module, err) - return nil + st.j.logf("%s: the manifest of %s cannot be read, so the modules built with a changed build module or base are not named and every build is waited on: %v", st.c.Key, module, err) + readable = false + continue } changed[module] = m } } - var out []string - for _, t := range st.targets { - if t.module != buildrun.GoBuildModule || !changesBuildGo(t.pull.Repository, t.changed) { + var words []string + if st.j.Runs != nil { + var unheld []string + for _, module := range slices.Sorted(maps.Keys(wait)) { + if st.j.Runs.CommitOf(module) == "" { + unheld = append(unheld, module) + delete(wait, module) + } + } + if len(unheld) > 0 { + words = append(words, fmt.Sprintf("%s: the trunk holds no record of it yet, so the builder builds it once it is merged and the check composes it without a build (ADR 0603 §10)", + strings.Join(unheld, ", "))) + } + } + if !readable { + return wait, words + } + for _, bm := range slices.Sorted(maps.Keys(buildrun.LanguageOf)) { + if _, alters := changed[bm]; !alters { continue } var built []string for module, m := range changed { - if m.Build != nil && slices.ContainsFunc(m.Build.Artifacts, func(a catalogue.Artifact) bool { return a.Language == "go" }) { + if _, isBuildModule := buildrun.LanguageOf[module]; !isBuildModule && buildsWith(m, buildrun.LanguageOf[bm]) { built = append(built, module) + delete(wait, module) } } slices.Sort(built) if len(built) > 0 { - out = append(out, fmt.Sprintf("the change alters the build module %s: %s, built with it, are composed from their manifests, not built by the check, and built again once the agreed toolchain moves (ADR 0603 §3, §10)", - buildrun.GoBuildModule, strings.Join(built, ", "))) + words = append(words, fmt.Sprintf("the change alters the build module %s: %s, which it builds, are built by the running %s, not the changed one, which the check cannot run, so the check does not wait on their builds; they are built again once the agreed toolchain moves (ADR 0603 §3, §10)", + bm, strings.Join(built, ", "), bm)) } - break } g := graph.OfManifests(changed) for _, base := range slices.Sorted(maps.Keys(changed)) { if on := g.Dependents(base); len(on) > 0 { - out = append(out, fmt.Sprintf("the change alters %s, a base of %s: their fingerprints name its fingerprint, so they are composed from their manifests, not built by the check (ADR 0603 §10)", - base, strings.Join(on, ", "))) + for _, module := range on { + delete(wait, module) + } + words = append(words, fmt.Sprintf("the change alters %s, a base of %s: their builds stand on the trunk's %s, since no build module builds on a candidate's base, so the check waits on the build of %s alone (ADR 0603 §10)", + base, strings.Join(on, ", "), base, base)) } } - return out + return wait, words +} + +// buildsWith says whether a manifest has a bundle in the language: the artifacts whose fingerprint names that language's +// toolchain and its build module's recipe (an archive is packed with no toolchain, ADR 0478 clause 1). +func buildsWith(m catalogue.Manifest, language string) bool { + return m.Build != nil && slices.ContainsFunc(m.Build.Artifacts, func(a catalogue.Artifact) bool { + return a.Kind == catalogue.ArtifactBundle && a.Language == language + }) } diff --git a/modules/mesh-delivery-checker/internal/check/builds.go b/modules/mesh-delivery-checker/internal/check/builds.go index f70115fb..68e51c7c 100644 --- a/modules/mesh-delivery-checker/internal/check/builds.go +++ b/modules/mesh-delivery-checker/internal/check/builds.go @@ -2,7 +2,12 @@ package check import ( "context" + "fmt" + "maps" + "slices" "sort" + "strings" + "time" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -64,3 +69,67 @@ func (j *Examiner) buildNotes(ctx context.Context, c *Case) []BuildNote { } return notes } + +// DefaultBuildWait is how long a check waits for the candidate's builds to end before it composes (the setting +// build-wait-bound, novox/hq ADR 0603 §10): past it the check composes without the builds not ended and names them. +const DefaultBuildWait = 30 * time.Minute + +// BuildEnds waits on the builder's build keys (builds..), hearing each key's changes on the bus as the +// builder writes them: the builder owns the fact, and nothing is polled. +type BuildEnds interface { + // Ended waits until the build of every module at its fingerprint (module to fingerprint) is done or failed, or ctx ends, + // and answers the builds that ended, by module. An error is only builds that cannot be heard at all. + Ended(ctx context.Context, builds map[string]string) (map[string]delivery.BuilderBuild, error) +} + +func (j *Examiner) buildWait() time.Duration { + if j.BuildWait > 0 { + return j.BuildWait + } + return DefaultBuildWait +} + +// awaitBuilds waits for the candidate's builds before the check composes (ADR 0603 §10): the check builds what the builder +// builds of the candidate and composes with it. A failed build fails the check, naming the module and the builder's words; +// a build not ended within the bound is named, and the check composes without it. It answers the words of the composition's +// warnings: what the check composes without waiting on its build, and why. +func (st *state) awaitBuilds(ctx context.Context, v *delivery.Verdict) []string { + wait, words := st.buildsOfTheCheck(ctx) + if len(wait) == 0 { + return words + } + modules := slices.Sorted(maps.Keys(wait)) + if st.j.BuildEnds == nil { + return append(words, fmt.Sprintf("the builds of %s cannot be heard, so the check composes without waiting on them (ADR 0603 §10)", strings.Join(modules, ", "))) + } + bound := st.j.buildWait() + waits := fmt.Sprintf("waits on the builder's builds of %s before it composes, at most %s (ADR 0603 §10)", strings.Join(modules, ", "), bound) + st.c.note(waits) + st.j.logf("%s %s", st.c.Key, waits) + at, cancel := context.WithTimeout(ctx, bound) + ended, err := st.j.BuildEnds.Ended(at, wait) + cancel() + if err != nil { + st.j.logf("%s: the builds of %s cannot be heard: %v", st.c.Key, strings.Join(modules, ", "), err) + return append(words, fmt.Sprintf("the builds of %s cannot be heard (%v), so the check composes without waiting on them (ADR 0603 §10)", strings.Join(modules, ", "), err)) + } + var late []string + for _, module := range modules { + b, done := ended[module] + switch { + case !done: + late = append(late, module) + case b.State == delivery.BuildFailed: + f := delivery.FailedFirst{Test: "build: " + module, Words: fmt.Sprintf("the candidate's build at %s failed (builder_build_again builds it again, then mesh-delivery.recheck asks the check again): %s", wait[module], b.Error)} + if v.State != delivery.VerdictFail { + v.State, v.Failed = delivery.VerdictFail, f + continue + } + addFailure(v, f) + } + } + if len(late) > 0 && ctx.Err() == nil { + words = append(words, fmt.Sprintf("the builds of %s did not end within the build-wait-bound of %s, so the check composes without them (ADR 0603 §10)", strings.Join(late, ", "), bound)) + } + return words +} diff --git a/modules/mesh-delivery-checker/internal/check/composition_test.go b/modules/mesh-delivery-checker/internal/check/composition_test.go index c8dc90c3..53f42d07 100644 --- a/modules/mesh-delivery-checker/internal/check/composition_test.go +++ b/modules/mesh-delivery-checker/internal/check/composition_test.go @@ -104,41 +104,122 @@ type fixedTrunks struct{ heads map[string]string } func (f fixedTrunks) Heads() map[string]string { return f.heads } -// ADR 0603 §10: a change that alters the Go build module, or a module others stand on, names in the composition's warnings -// (and so the check's line) the changed modules built with it, from their manifests; a change that alters neither says nothing. +// fakeEnds answers the builder's builds of a candidate as fakeBuilds holds them, recording the builds waited on; the +// composition must not have been asked yet when it is (ADR 0603 §10: the check composes with the builds). +type fakeEnds struct { + builds fakeBuilds + asked map[string]string + composed *fakeCompositions + early bool +} + +func (f *fakeEnds) Ended(_ context.Context, builds map[string]string) (map[string]delivery.BuilderBuild, error) { + f.asked = builds + f.early = f.composed != nil && len(f.composed.asked) > 0 + out := map[string]delivery.BuilderBuild{} + for module, fp := range builds { + if b, ok := f.builds[module+" "+fp]; ok && (b.State == delivery.BuildDone || b.State == delivery.BuildFailed) { + out[module] = b + } + } + return out, nil +} + +// knownRuns are the modules the trunk holds a record of (the indexer's modules): any commit. +type knownRuns []string + +func (k knownRuns) CommitOf(module string) string { + if slices.Contains(k, module) { + return trunkA + } + return "" +} + +// ADR 0603 §10: before it composes, the check waits on the builder's builds of the candidate's changed modules and composes +// with them; a failed build fails the check naming it, and one not ended within the bound is named. It does not wait on a +// module built by a build module the change alters, nor on one standing on a base the change alters, nor on one the trunk +// holds no record of, and names each in the composition's warnings (and so the check's line); a change that alters none of +// these says nothing. func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) { dig := func(c string) string { return strings.Repeat(c, 64) } - goBuild := &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "bin", Language: "go"}}} + fp := "src2:" + dig("f") + goBuild := &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "bin", Kind: catalogue.ArtifactBundle, Language: "go"}}} + tsBuild := &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "app", Kind: catalogue.ArtifactBundle, Language: "typescript"}}} + done := delivery.BuilderBuild{State: delivery.BuildDone} for _, row := range []struct { name string - changed []string manifest map[string]catalogue.Manifest + builds map[string]delivery.BuilderBuild // by module, at fp; nil leaves the check without the builds' ends + runs knownRuns // nil: the trunk holds every module want []string + waited string // the modules waited on, sorted + failed string // the failure's test, when the check fails }{ - {name: "the build module changes", changed: []string{buildGoFile}, - manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}, "mail": {Module: "mail", Build: goBuild}, "docs": {Module: "docs"}}, - want: []string{"mesh-build-go: mail, web, built with it"}}, - {name: "a base changes", changed: []string{"README.md"}, + {name: "the Go build module changes", + manifest: map[string]catalogue.Manifest{"mesh-build-go": {Module: "mesh-build-go", Build: goBuild}, "web": {Module: "web", Build: goBuild}, + "mail": {Module: "mail", Build: goBuild}, "docs": {Module: "docs"}}, + builds: map[string]delivery.BuilderBuild{"mesh-build-go": done, "docs": done}, + want: []string{"the build module mesh-build-go: mail, web, which it builds"}, waited: "docs, mesh-build-go"}, + {name: "the Node build module changes", + manifest: map[string]catalogue.Manifest{"mesh-build-node": {Module: "mesh-build-node", Build: goBuild}, "panel": {Module: "panel", Build: tsBuild}, + "web": {Module: "web", Build: goBuild}}, + builds: map[string]delivery.BuilderBuild{"mesh-build-node": done, "web": done}, + want: []string{"the build module mesh-build-node: panel, which it builds"}, waited: "mesh-build-node, web"}, + {name: "a base changes", manifest: map[string]catalogue.Manifest{"image": {Module: "image"}, "web": {Module: "web", Build: &catalogue.Build{On: []catalogue.BuildsOn{{Arg: "BASE", Module: "image", Artifact: "img"}}}}}, - want: []string{"alters image, a base of web"}}, - {name: "neither changes", changed: []string{"README.md"}, - manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}}}, + builds: map[string]delivery.BuilderBuild{"image": done}, + want: []string{"alters image, a base of web", "waits on the build of image alone"}, waited: "image"}, + {name: "neither changes", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}}, + builds: map[string]delivery.BuilderBuild{"web": done}, waited: "web"}, + {name: "a build fails", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}, "mail": {Module: "mail", Build: goBuild}}, + builds: map[string]delivery.BuilderBuild{"web": {State: delivery.BuildFailed, Error: "go build: undefined: x"}, "mail": done}, + waited: "mail, web", failed: "build: web"}, + {name: "a build does not end within the bound", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}}, + builds: map[string]delivery.BuilderBuild{"web": {State: delivery.BuildBuilding}}, + want: []string{"the builds of web did not end within the build-wait-bound"}, waited: "web"}, + {name: "a module the trunk does not hold", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}, "new": {Module: "new", Build: goBuild}}, + builds: map[string]delivery.BuilderBuild{"web": done}, runs: knownRuns{"web"}, + want: []string{"new: the trunk holds no record of it yet"}, waited: "web"}, + {name: "the builds cannot be heard", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}}, + want: []string{"the builds of web cannot be heard"}}, } { b := candidateBench(t, true) - b.fetch.Changed = row.changed - b.ex.Compositions = &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionPass, SDK: "x", Machines: []string{"g14"}}} + comps := &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionPass, SDK: "x", Machines: []string{"g14"}}} + b.ex.Compositions = comps + var ends *fakeEnds + if row.builds != nil { + ends = &fakeEnds{builds: fakeBuilds{}, composed: comps} + for module, build := range row.builds { + ends.builds[module+" "+fp] = build + } + b.ex.BuildEnds = ends + } + if row.runs != nil { + b.ex.Runs = row.runs + } read, fps := fixedManifests{}, map[string]delivery.CandidateModule{} for module, m := range row.manifest { d := dig(string(rune('a' + len(read)))) - read[d], fps[module] = m, delivery.CandidateModule{FP2: "src2:" + dig("f"), Manifest: d} + read[d], fps[module] = m, delivery.CandidateModule{FP2: fp, Manifest: d} } b.ex.Read = read p := pullOf(catalogueRepository, 9, head1) p.Fps, p.Modules = fps, slices.Sorted(maps.Keys(fps)) v := b.judge(t, caseOf(t, p)).Verdict - if v.State != delivery.VerdictPass || v.Composition == nil { - t.Fatalf("%s: %+v", row.name, v) + if v.Composition == nil || len(comps.asked) != 1 { + t.Fatalf("%s: composed %d times: %+v", row.name, len(comps.asked), v) + } + if ends != nil { + if got := strings.Join(slices.Sorted(maps.Keys(ends.asked)), ", "); got != row.waited || ends.early { + t.Errorf("%s: waited on %q (after the composition: %v), want %q before it composes", row.name, got, ends.early, row.waited) + } + } + switch { + case row.failed != "" && (v.State != delivery.VerdictFail || v.Failed.Test != row.failed || !strings.Contains(v.Failed.Words, "undefined: x")): + t.Errorf("%s: the verdict is %s failing %+v, want it failed naming %s and the builder's words", row.name, v.State, v.Failed, row.failed) + case row.failed == "" && v.State != delivery.VerdictPass: + t.Errorf("%s: %+v", row.name, v) } words := compositionWords(v.Composition, v.Took.Composition) for _, w := range row.want { @@ -147,7 +228,7 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) { } } if row.want == nil && v.Composition.Words != "" { - t.Errorf("%s: warned %q though neither a build module nor a base changed", row.name, v.Composition.Words) + t.Errorf("%s: warned %q though the check waited on every build of the change", row.name, v.Composition.Words) } } } diff --git a/modules/mesh-delivery-checker/internal/check/judge.go b/modules/mesh-delivery-checker/internal/check/judge.go index f15c9429..cb77a6e5 100644 --- a/modules/mesh-delivery-checker/internal/check/judge.go +++ b/modules/mesh-delivery-checker/internal/check/judge.go @@ -102,6 +102,11 @@ type Examiner struct { // Builds says how the build of a fingerprint the candidate carries stands (the builder's builds bucket); nil says nothing // of the builds. Builds BuildStates + // BuildEnds waits on the builder's builds of the candidate before the check composes (ADR 0603 §10); nil composes + // without waiting, said in the composition's warnings. + BuildEnds BuildEnds + // BuildWait is how long the check waits for those builds (the setting build-wait-bound); DefaultBuildWait when 0. + BuildWait time.Duration // Announce says a preview was written under the key (EventPreviewWritten); nil says nothing. Announce Announcer // Packages names, of a repository, the paths (repository-relative) its TypeScript package artifacts are built from, from @@ -280,6 +285,7 @@ func (st *state) composed(ctx context.Context, v *delivery.Verdict) { strings.Join(st.unpreviewed, ", ") + " (its head does not merge or its fetch did not answer), so the change cannot be composed"} return } + words := st.awaitBuilds(ctx, v) // ADR 0603 §10: the check composes with the candidate's builds comp, err := st.j.Compositions.Compose(ctx, st.c) if err != nil { // Said in the verdict, never left as a composition not yet run (review finding 4). @@ -287,7 +293,7 @@ func (st *state) composed(ctx context.Context, v *delivery.Verdict) { v.Composition = &delivery.Composition{State: delivery.CompositionNotRun, Words: "not made: " + err.Error()} return } - comp.Warnings = append(comp.Warnings, st.buildChangeWarnings(ctx)...) // ADR 0603 §10 + comp.Warnings = append(comp.Warnings, words...) // ADR 0603 §10 withComposition(v, comp) if v.Composition != nil && v.Composition.State != delivery.CompositionNotRun { v.Composition.FPSet = st.c.FPSet // a carry keeps it while the fingerprints stay the same (ADR 0548 §5) diff --git a/modules/mesh-delivery-checker/manifest_test.go b/modules/mesh-delivery-checker/manifest_test.go index 7d995a3d..6a379ebe 100644 --- a/modules/mesh-delivery-checker/manifest_test.go +++ b/modules/mesh-delivery-checker/manifest_test.go @@ -174,13 +174,16 @@ func TestManifestProcess(t *testing.T) { // 1678/AC1 if len(env) != 2 || env["MESH_BROKER_FILE"] != "${dir:state}/broker" || env["MESH_CHECKER_SETTINGS"] != "${dir:state}/settings.json" { t.Errorf("env = %v, want the broker file and the settings file", p["env"]) } - // Its settings hold its answer bound alone (ADR 0593 §7: compose-answer-bound, 60 s, the checker's), never a capacity - // of its own (ADR 0525 §4). + // Its settings hold its answer bound (ADR 0593 §7: compose-answer-bound, 60 s, the checker's) and its wait for the + // candidate's builds (ADR 0603 §10: build-wait-bound, 1800 s), never a capacity of its own (ADR 0525 §4). for _, r := range m.Resources { - if r["id"] == "settings" && !reflect.DeepEqual(r["takes-config"], []any{"compose-answer-bound"}) { - t.Errorf("the settings resource %v takes more than compose-answer-bound", r) + if r["id"] == "settings" && !reflect.DeepEqual(r["takes-config"], []any{"compose-answer-bound", "build-wait-bound"}) { + t.Errorf("the settings resource %v takes other than compose-answer-bound and build-wait-bound", r) } } + if c, ok := m.Config["build-wait-bound"]; !ok || c.Default != float64(1800) || c.Trusted { + t.Errorf("build-wait-bound = %+v, want 1800 by default, not trusted", c) + } if c, ok := m.Config["compose-answer-bound"]; !ok || c.Default != float64(60) || c.Trusted { t.Errorf("compose-answer-bound = %+v, want 60 by default, not trusted", c) } diff --git a/modules/mesh-delivery-checker/module.json b/modules/mesh-delivery-checker/module.json index 767884cb..537551f1 100644 --- a/modules/mesh-delivery-checker/module.json +++ b/modules/mesh-delivery-checker/module.json @@ -81,6 +81,14 @@ "maximum": 600, "trusted": false, "default": 60 + }, + "build-wait-bound": { + "type": "number", + "description": "seconds a check waits for the builder's builds of its candidate to end before it composes with them (novox/hq ADR 0603 §10); a build failed fails the check, naming it, and a build not ended by then is named in the verdict and the check composes without it.", + "minimum": 60, + "maximum": 7200, + "trusted": false, + "default": 1800 } }, "resources": [ @@ -106,7 +114,8 @@ "mode": "0600", "owner": "mesh-delivery-checker", "takes-config": [ - "compose-answer-bound" + "compose-answer-bound", + "build-wait-bound" ], "merge": "json", "template": "files/settings.json" From 877b09e92b308d52b1cf6a6d80f8916f14cb1a6d Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 17:51:09 +0100 Subject: [PATCH 43/49] =?UTF-8?q?checker=5Fstatus=20names=20the=20builds?= =?UTF-8?q?=20a=20running=20check=20waits=20on=20(ADR=200603=20=C2=A710)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Worked out from the builder's builds state at each read, stored nowhere: of each running check, the candidate's builds that are neither done nor failed. Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- .../cmd/mesh-delivery-checker-tools/main.go | 6 ++- .../internal/check/read.go | 49 +++++++++++++++++++ .../internal/check/read_test.go | 21 +++++++- 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go index 2159d0e5..371003d2 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go @@ -244,7 +244,9 @@ func Tools(r check.Reader) []stdio.Tool { "build seats' streams, whose backlog the delivery view shows (novox/hq ADR 0525). held_by_turn names the candidates whose check an approved " + "merge's turn holds off (novox/hq ADR 0548 §3), with the repository and the merge that waits; the gate's turns tool reads every turn and its " + "hand-now tool steers one. dead_letters says whether the checker hears the asks the build seats give up on (issue 734), and why not: " + - "deaf, a try or check whose ask is given up waits until its bound. Read-only.", + "deaf, a try or check whose ask is given up waits until its bound. builds_not_ended names, of each running check, the builder's builds of " + + "its candidate's changed modules that are neither done nor failed: the check waits on them before it composes, within its build-wait-bound " + + "(novox/hq ADR 0603 §10); a failed one fails the check: the builder's builder_build_again builds it again, then mesh-delivery.recheck asks the check again. Read-only.", Input: map[string]any{"type": "object", "properties": map[string]any{}}, Run: func(map[string]any) (any, error) { return r.Status() }}, } @@ -283,7 +285,7 @@ func main() { StepStates: map[string]check.StateRead{"mesh-build-go": read(stdio.State("mesh-build-go.steps")), "mesh-build-node": read(stdio.State("mesh-build-node.steps"))}, HaveState: read(stdio.State("node-engine.have")), StepBound: stepBound(), TurnState: read(stdio.State("mesh-delivery-gate.turns")), HearingState: read(stdio.State(buildrun.HearingState)), - AskState: read(stdio.State(check.AsksState))} + AskState: read(stdio.State(check.AsksState)), BuildState: read(stdio.State("mesh-delivery-builder.builds"))} r.Runs = runsOf{read(stdio.State("mesh-delivery-indexer.modules"))} // the node-engine at the commit the mesh runs, as the checker reads it tries := stdio.State(check.TriesState) source := stdio.State("mesh-delivery-gate.source") diff --git a/modules/mesh-delivery-checker/internal/check/read.go b/modules/mesh-delivery-checker/internal/check/read.go index 90e57f28..2086eced 100644 --- a/modules/mesh-delivery-checker/internal/check/read.go +++ b/modules/mesh-delivery-checker/internal/check/read.go @@ -3,6 +3,7 @@ package check import ( "encoding/json" "fmt" + "slices" "sort" "strings" "time" @@ -47,6 +48,9 @@ type Reader struct { Runs interface{ CommitOf(module string) string } // HearingState is the checker's state hearing: whether it hears DEAD_LETTERS (issue 734). HearingState StateRead + // BuildState is the builder's builds (mesh-delivery-builder.builds): the builds of a running check's candidate that have + // not ended, which the check waits on before it composes (ADR 0603 §10); without it none is named. + BuildState StateRead // AskState is the checker's state asks: the asks each running check put (AsksState), which checker_ask_again reads. AskState StateRead Now func() time.Time @@ -344,6 +348,10 @@ type Status struct { // Lost are the candidates whose verdict is running while no step of theirs can still run and their newest step ended, by // group, with that step: nothing will judge them (the lost check of 2026-10-28); checker_ask_again asks one again. Lost map[string]string `json:"lost"` + // BuildsNotEnded are, of each candidate whose check runs, the builder's builds of its changed modules that are neither + // done nor failed, by group: ` `, sorted (no key yet is `not begun`). A check waits on them before it + // composes (ADR 0603 §10), but for the modules its line names as composed without them; builder_build_again builds a failed one again. + BuildsNotEnded map[string]string `json:"builds_not_ended,omitempty"` // DeadLetters says whether the checker hears the asks the build seats give up on (issue 734): deaf, a try or check // whose ask is given up waits until its bound. DeadLetters DeadLettersHeard `json:"dead_letters"` @@ -500,6 +508,12 @@ func (r Reader) Status() (Status, error) { if why := read.lostWhy(c); why != "" { st.Lost[c.Group] = why } + if words := r.buildsNotEnded(c); words != "" { + if st.BuildsNotEnded == nil { + st.BuildsNotEnded = map[string]string{} + } + st.BuildsNotEnded[c.Group] = words + } case IsNodesFailure(v): st.Waiting[KindNodes] = append(st.Waiting[KindNodes], c.Group) case IsUntrunked(v): @@ -663,3 +677,38 @@ func (r Reader) leftWhy(module string, step delivery.Step, now time.Time, bound } return "" } + +// buildsNotEnded are the builder's builds of the candidate's changed modules that are neither done nor failed, as +// Status.BuildsNotEnded says them; empty when none, or when the builder's builds are not readable here. +func (r Reader) buildsNotEnded(c delivery.Candidate) string { + if r.BuildState.Get == nil { + return "" + } + var out []string + for _, p := range c.Pulls { + for module, cm := range p.Fps { + if cm.FP2 == "" || cm.Manifest == "" { + continue + } + key, err := delivery.BuilderBuildKey(module, cm.FP2) + if err != nil { + continue + } + raw, err := r.BuildState.Get(key) + if err != nil { + continue + } + state := "not begun" + if len(raw) > 0 { + b, err := delivery.DecodeBuilderBuild(raw) + if err != nil || b.State == delivery.BuildDone || b.State == delivery.BuildFailed { + continue + } + state = b.State + } + out = append(out, module+" "+state) + } + } + sort.Strings(out) + return strings.Join(slices.Compact(out), ", ") +} diff --git a/modules/mesh-delivery-checker/internal/check/read_test.go b/modules/mesh-delivery-checker/internal/check/read_test.go index 9a866222..033ba635 100644 --- a/modules/mesh-delivery-checker/internal/check/read_test.go +++ b/modules/mesh-delivery-checker/internal/check/read_test.go @@ -147,10 +147,24 @@ func TestTheStatusToolReadsRunningSteps(t *testing.T) { passed, _ := delivery.EncodeStep(delivery.Step{ID: "abc2", Tool: "build_go_test", Node: "ace", State: delivery.StepPassed}) cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1)) ckey, _ := delivery.CandidatesKey("feat/x") + // ADR 0603 §10: feat/y's check runs and waits on the builds of its changed modules: web building, mail not begun; api + // is done and named nowhere. + fp, man := "src2:"+strings.Repeat("f", 64), strings.Repeat("e", 64) + py := pullOf(repoA, 8, head2) + py.Modules, py.Fps = []string{"api", "mail", "web"}, map[string]delivery.CandidateModule{"web": {FP2: fp, Manifest: man}, "mail": {FP2: fp, Manifest: man}, "api": {FP2: fp, Manifest: man}} + candY := candidateOf(t, "feat/y", py) + ykey, _ := delivery.CandidatesKey("feat/y") + runningV, _ := delivery.EncodeVerdict(delivery.Verdict{Group: "feat/y", Heads: "0123456789ab", State: delivery.VerdictRunning}) + builds := fakeState{} + for module, state := range map[string]string{"web": delivery.BuildBuilding, "api": delivery.BuildDone} { + k, _ := delivery.BuilderBuildKey(module, fp) + builds[k], _ = json.Marshal(delivery.BuilderBuild{V: 1, State: state}) + } rd := Reader{ StepStates: map[string]StateRead{"mesh-build-go": fakeState{"shanks.abc1": running, "ace.abc2": passed}.read()}, - CandidateState: fakeState{ckey: cand}.read(), - VerdictState: fakeState{}.read(), + CandidateState: fakeState{ckey: cand, ykey: candY}.read(), + VerdictState: fakeState{keyOf(t, "feat/y", candY): runningV}.read(), + BuildState: builds.read(), HaveState: fakeState{"shanks.mesh-build-go": haveOf(t, delivery.PhaseRunning)}.read(), HearingState: fakeState{"dead-letters": []byte(`{"heard":false,"streams":["SEAT_NODE_BUILD_GO"],` + `"why":"DEAD_LETTERS is not heard, so an ask a build seat gives up on is waited for until its bound: SEAT_NODE_BUILD_GO: permissions violation",` + @@ -167,6 +181,9 @@ func TestTheStatusToolReadsRunningSteps(t *testing.T) { if len(st.Unjudged) != 1 || st.Unjudged[0] != "feat/x" { t.Errorf("unjudged = %v", st.Unjudged) } + if got := st.BuildsNotEnded; len(got) != 1 || got["feat/y"] != "mail not begun, web building" { + t.Errorf("builds not ended = %v, want feat/y's mail not begun and web building", got) + } // Issue 734, review round 1: a checker that does not hear DEAD_LETTERS says so in its status, with why. if st.DeadLetters.Heard || !strings.Contains(st.DeadLetters.Why, "permissions violation") { t.Errorf("dead letters = %+v, want not heard, saying why", st.DeadLetters) From 3842a464ed9b577c3e352ded6d06b45d85f817f6 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 17:55:41 +0100 Subject: [PATCH 44/49] =?UTF-8?q?A=20check=20failed=20on=20a=20build=20ask?= =?UTF-8?q?s=20itself=20again=20once=20the=20build=20is=20done=20again,=20?= =?UTF-8?q?and=20a=20failed=20check=20never=20waits=20on=20builds=20(ADR?= =?UTF-8?q?=200603=20=C2=A710)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A failed candidate build no longer needs mesh-delivery.recheck, an exception verb (ADR 0421 §12): the checker hears the builder's keys the failed verdict names and, once each is written done (builder_build_again), retakes the check of the same heads as it does after a failure of the nodes, at its start too. When the tests have already failed the check composes at once and names the builds it did not wait on, so its red status is never held back by a build. Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- .../cmd/mesh-delivery-checker-tools/main.go | 2 +- .../mesh-delivery-checker/integration_test.go | 20 ++++- .../cmd/mesh-delivery-checker/judge.go | 33 +++++--- .../cmd/mesh-delivery-checker/main.go | 3 + .../internal/check/builds.go | 37 ++++++++- .../internal/check/checker.go | 77 ++++++++++++++++++- .../internal/check/composition_test.go | 24 +++++- .../internal/check/read.go | 2 +- 8 files changed, 173 insertions(+), 25 deletions(-) diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go index 371003d2..fd39fe86 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go @@ -246,7 +246,7 @@ func Tools(r check.Reader) []stdio.Tool { "hand-now tool steers one. dead_letters says whether the checker hears the asks the build seats give up on (issue 734), and why not: " + "deaf, a try or check whose ask is given up waits until its bound. builds_not_ended names, of each running check, the builder's builds of " + "its candidate's changed modules that are neither done nor failed: the check waits on them before it composes, within its build-wait-bound " + - "(novox/hq ADR 0603 §10); a failed one fails the check: the builder's builder_build_again builds it again, then mesh-delivery.recheck asks the check again. Read-only.", + "(novox/hq ADR 0603 §10); a failed one fails the check: rebuild it with the builder's builder_build_again, and the check asks itself again when the build is done. Read-only.", Input: map[string]any{"type": "object", "properties": map[string]any{}}, Run: func(map[string]any) (any, error) { return r.Status() }}, } diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go index d2dc3250..823deb6c 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go @@ -618,12 +618,23 @@ func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300 if v := b.verdict(t, key, func(delivery.Verdict) bool { return true }); v.State != delivery.VerdictRunning || len(asked()) != 0 { t.Fatalf("verdict %s and %d asks while m builds: the check composes only once its build ended", v.State, len(asked())) } + // m's build fails: the check fails naming it. Built again (builder_build_again writes the key done), the check asks + // itself again for the same heads, with no recheck, and passes. + failedBuild, _ := json.Marshal(delivery.BuilderBuild{V: 1, State: delivery.BuildFailed, Error: "go build: undefined: x"}) + if _, err := b.buckets[delivery.BuilderBuildsBucket].Put(b.ctx, bkey, failedBuild); err != nil { + t.Fatal(err) + } + if v := b.verdict(t, key, ended); v.State != delivery.VerdictFail || v.Failed.Test != check.BuildFailedTest+"m" { + t.Fatalf("verdict %+v, want it failed on m's build", v) + } built, _ := json.Marshal(delivery.BuilderBuild{V: 1, State: delivery.BuildDone, Manifest: digest}) if _, err := b.buckets[delivery.BuilderBuildsBucket].Put(b.ctx, bkey, built); err != nil { t.Fatal(err) } - v := b.verdict(t, key, ended) - if v.State != delivery.VerdictPass || v.Levels[delivery.LevelUnit].Run != 2 || v.Levels[delivery.LevelIntegration].Run != 2 || v.Trunk[repo] != trunk { + b.said(t, "the builds of m it failed on are done again, so it is checked again") + v := b.verdict(t, key, func(v delivery.Verdict) bool { return v.State == delivery.VerdictPass }) + // The check asked again takes the packages the failed check ran from their kept verdicts. + if v.State != delivery.VerdictPass || v.Levels[delivery.LevelUnit].Kept != 2 || v.Levels[delivery.LevelIntegration].Kept != 2 || v.Trunk[repo] != trunk { t.Fatalf("verdict = %+v", v) } if c := v.Composition; c == nil || c.State != delivery.CompositionPass || !slices.Equal(c.Machines, []string{"n1"}) || @@ -643,10 +654,11 @@ func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300 t.Errorf("the check ask named the nodes %v, want [n1] from the have's node record", names) } o := b.owed(t, 7, head1, "success") - if o.Context != delivery.StatusContextCheck || !strings.HasPrefix(o.Text, "pass 111111111111 on aaaaaaaaaaaa") || !strings.Contains(o.Text, "unit 2 run") { + if o.Context != delivery.StatusContextCheck || !strings.HasPrefix(o.Text, "pass 111111111111 on aaaaaaaaaaaa") || !strings.Contains(o.Text, "unit 0 run, 2 kept") { t.Errorf("owed = %+v", o) } - for tool, want := range map[string]int{"mesh-build-go.build_go_fetch": 1, "mesh-build-go.build_go_digest": 2, "mesh-build-go.build_go_test": 2} { + // Two checks of the same heads: the one failed on m's build and the one it asked itself again; the second runs no test. + for tool, want := range map[string]int{"mesh-build-go.build_go_fetch": 2, "mesh-build-go.build_go_digest": 4, "mesh-build-go.build_go_test": 2} { if got := b.fake.count(tool); got != want { t.Errorf("%s called %d times, want %d", tool, got, want) } diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go index 949f4376..047e9ab0 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go @@ -305,6 +305,18 @@ func (r buildReader) Build(ctx context.Context, module, fp2 string) (delivery.Bu type buildEnds struct{ kv jetstream.KeyValue } func (e buildEnds) Ended(ctx context.Context, builds map[string]string) (map[string]delivery.BuilderBuild, error) { + return e.until(ctx, builds, func(b delivery.BuilderBuild) bool { + return b.State == delivery.BuildDone || b.State == delivery.BuildFailed + }) +} + +func (e buildEnds) Done(ctx context.Context, builds map[string]string) bool { + done, err := e.until(ctx, builds, func(b delivery.BuilderBuild) bool { return b.State == delivery.BuildDone }) + return err == nil && len(done) == len(builds) +} + +// until hears the builds' keys until each holds a build that ended holds, or ctx ends, and answers those that do, by module. +func (e buildEnds) until(ctx context.Context, builds map[string]string, ended func(delivery.BuilderBuild) bool) (map[string]delivery.BuilderBuild, error) { byKey := map[string]string{} keys := make([]string, 0, len(builds)) for module, fp2 := range builds { @@ -315,22 +327,22 @@ func (e buildEnds) Ended(ctx context.Context, builds map[string]string) (map[str byKey[key] = module keys = append(keys, key) } - ended := map[string]delivery.BuilderBuild{} + out := map[string]delivery.BuilderBuild{} if len(keys) == 0 { - return ended, nil + return out, nil } w, err := e.kv.WatchFiltered(ctx, keys) if err != nil { return nil, err } defer func() { _ = w.Stop() }() - for len(ended) < len(keys) { + for len(out) < len(keys) { select { case <-ctx.Done(): - return ended, nil // the bound: the builds not ended are left out + return out, nil // the bound: the builds not ended are left out case entry, open := <-w.Updates(): if !open { - return ended, nil + return out, nil } if entry == nil || entry.Operation() != jetstream.KeyValuePut { continue // the end of the values now, or a key deleted: it has not ended @@ -340,17 +352,14 @@ func (e buildEnds) Ended(ctx context.Context, builds map[string]string) (map[str continue } b, err := delivery.DecodeBuilderBuild(entry.Value()) - if err != nil { - continue // not a build the check can read: it has not ended for the check - } - if b.State == delivery.BuildDone || b.State == delivery.BuildFailed { - ended[module] = b + if err == nil && ended(b) { + out[module] = b } else { - delete(ended, module) // asked again (builder_build_again): it runs once more + delete(out, module) // asked again (builder_build_again), or not a build the check can read: not ended } } } - return ended, nil + return out, nil } // ManifestDigests is the manifest digest of every module the indexer holds, by module: the catalogue the indexer's judge diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go index 8ca5d68c..1cc2b977 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go @@ -170,6 +170,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro if kv := process.OptionalBucket(ctx, js, delivery.BuilderBuildsBucket, "the line says nothing of the builds, and a check composes without waiting on them, until the checker starts again", logger.Printf); kv != nil { builds, ends = buildReader{kvStore{kv}}, buildEnds{kv} } + if ends != nil { + ch.Rebuilds = ends // a check failed on a build is asked again once that build is done again (ADR 0603 §10) + } // The check's composition is the checker's, from what each placed node's engine answers (ADR 0593 §7, §9). set, err := readSettings(getenv("MESH_CHECKER_SETTINGS")) if err != nil { diff --git a/modules/mesh-delivery-checker/internal/check/builds.go b/modules/mesh-delivery-checker/internal/check/builds.go index 68e51c7c..2afa2d42 100644 --- a/modules/mesh-delivery-checker/internal/check/builds.go +++ b/modules/mesh-delivery-checker/internal/check/builds.go @@ -80,6 +80,37 @@ type BuildEnds interface { // Ended waits until the build of every module at its fingerprint (module to fingerprint) is done or failed, or ctx ends, // and answers the builds that ended, by module. An error is only builds that cannot be heard at all. Ended(ctx context.Context, builds map[string]string) (map[string]delivery.BuilderBuild, error) + // Done waits until the build of every module at its fingerprint is done (a failed one built again, builder_build_again), + // and says whether they all are; false when ctx ends first or they cannot be heard. + Done(ctx context.Context, builds map[string]string) bool +} + +// BuildFailedTest is what a failure of the check names a failed build of the candidate by: the test `build: `. +// A verdict failed on builds is asked again by the checker itself once each is built again (Checker.Rebuilt). +const BuildFailedTest = "build: " + +// failedBuilds are the candidate's builds a failed verdict names as failed (BuildFailedTest), by module, at the fingerprint +// the case carries for it; none for a verdict that names no failed build. +func failedBuilds(v delivery.Verdict, cs *Case) map[string]string { + if v.State != delivery.VerdictFail { + return nil + } + out := map[string]string{} + for _, f := range append([]delivery.FailedFirst{v.Failed}, v.Failures...) { + module, named := strings.CutPrefix(f.Test, BuildFailedTest) + if !named { + continue + } + for _, p := range cs.Pulls { + if cm, ok := p.Fps[module]; ok && cm.FP2 != "" { + out[module] = cm.FP2 + } + } + } + if len(out) == 0 { + return nil + } + return out } func (j *Examiner) buildWait() time.Duration { @@ -99,6 +130,10 @@ func (st *state) awaitBuilds(ctx context.Context, v *delivery.Verdict) []string return words } modules := slices.Sorted(maps.Keys(wait)) + if v.State == delivery.VerdictFail { + // The check has failed already: its red status is never held back by a build (the lead, 2026-11-03). + return append(words, fmt.Sprintf("the check failed before its builds were waited on, so it composes at once, without waiting on the builds of %s (ADR 0603 §10)", strings.Join(modules, ", "))) + } if st.j.BuildEnds == nil { return append(words, fmt.Sprintf("the builds of %s cannot be heard, so the check composes without waiting on them (ADR 0603 §10)", strings.Join(modules, ", "))) } @@ -120,7 +155,7 @@ func (st *state) awaitBuilds(ctx context.Context, v *delivery.Verdict) []string case !done: late = append(late, module) case b.State == delivery.BuildFailed: - f := delivery.FailedFirst{Test: "build: " + module, Words: fmt.Sprintf("the candidate's build at %s failed (builder_build_again builds it again, then mesh-delivery.recheck asks the check again): %s", wait[module], b.Error)} + f := delivery.FailedFirst{Test: BuildFailedTest + module, Words: fmt.Sprintf("the candidate's build at %s failed (rebuild with builder_build_again; the check asks itself again when the build is done): %s", wait[module], b.Error)} if v.State != delivery.VerdictFail { v.State, v.Failed = delivery.VerdictFail, f continue diff --git a/modules/mesh-delivery-checker/internal/check/checker.go b/modules/mesh-delivery-checker/internal/check/checker.go index 3fbffddb..4e8e91bc 100644 --- a/modules/mesh-delivery-checker/internal/check/checker.go +++ b/modules/mesh-delivery-checker/internal/check/checker.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "maps" + "slices" "sort" "strings" "sync" @@ -79,6 +81,9 @@ type Checker struct { } // Bound is how long one check may run; none when 0 (DefaultBound). Bound time.Duration + // Rebuilds hears the builder's build keys a failed verdict names (failedBuilds): once each is written done, the check of + // those heads is asked again by the checker itself (ADR 0603 §10), as a failure of the nodes is; nil asks none again. + Rebuilds BuildEnds // Asks keeps the asks each running check put, by verdict key (AsksState), for checker_ask_again; nil keeps none. Asks AsksKV // Link is the base of the address a status points at; the verdict key follows. @@ -104,6 +109,8 @@ type Checker struct { held map[string]string // by verdict key: the turn that holds its check off, as last said live map[string]liveCheck // by verdict key: the check running in this process, ended when its heads go settling map[string]*window // by candidates key: the window its heads settle in + rebuilt map[string]bool // by verdict key: every failed build its verdict names is done again + awaiting map[string]bool // by verdict key: its failed builds are heard (awaitRebuilt) // outsideSaid are the candidates outside the mesh said in the log, by group and heads. outsideSaid map[string]bool } @@ -162,6 +169,7 @@ func (ch *Checker) init() { ch.known, ch.inflight, ch.again, ch.tried = map[string]delivery.Candidate{}, map[string]bool{}, map[string]bool{}, map[string]bool{} ch.nodesFail, ch.held = map[string]uint64{}, map[string]string{} ch.live, ch.settling = map[string]liveCheck{}, map[string]*window{} + ch.rebuilt, ch.awaiting = map[string]bool{}, map[string]bool{} ch.outsideSaid = map[string]bool{} }) } @@ -380,8 +388,9 @@ func (ch *Checker) consider(ctx context.Context, key string, c delivery.Candidat // Every check is asked at once: the build seats' streams hold the backlog, a new head's check and a check after a // failure of the nodes on check, a re-judge after a trunk move on recheck, pulled after them (ADR 0525 §4, §9; // ADR 0536 §4). The checker holds no place and no capacity of its own. - ch.prepare(ctx, cs) + ch.prepare(ctx, key, cs) ch.try(ctx, run, key, cs) + ch.hearFailedBuilds(ctx, key, cs) // a check it failed on a build is asked again once that build is done again ch.mu.Lock() more := ch.again[vkey] delete(ch.again, vkey) @@ -518,7 +527,7 @@ const ( // or sibling commits that moved since is carried, its packages run only where the move changed their digest, and a pass's // commits are named as the carry's CarriedFrom. A verdict that stands is ranked with the carries: its claim finds nothing // to do. -func (ch *Checker) prepare(ctx context.Context, cs *Case) { +func (ch *Checker) prepare(ctx context.Context, group string, cs *Case) { cs.Kind, cs.CarriedFrom, cs.Composition = KindNew, nil, nil value, _, found, err := ch.Verdicts.Get(ctx, cs.Key) if err != nil || !found { @@ -531,6 +540,10 @@ func (ch *Checker) prepare(ctx context.Context, cs *Case) { case IsNodesFailure(v): cs.Kind = KindNodes return + case failedBuilds(v, cs) != nil: + cs.Kind = KindNodes // asked again on check once its builds are done again (awaitRebuilt), never by a recheck + ch.awaitRebuilt(ctx, group, cs.Key, failedBuilds(v, cs)) + return case IsConflictFailure(v): return // a head git refused: its next check is whole (ADR 0548 §7) case IsEnded(v), IsUntrunked(v): @@ -687,6 +700,8 @@ func (ch *Checker) claim(ctx context.Context, cs *Case, first bool, epoch uint64 return nil, nil case IsEnded(cur), ch.stale(cur, cs): retake = true // heads held again after their check was ended unjudged (issue 1890), or a trunk that moved + case cur.State == delivery.VerdictFail && ch.takeRebuilt(cs.Key): + retake = true // every failed build it names is done again (ADR 0603 §10) case cur.State == delivery.VerdictFail && IsNodesFailure(cur): ch.mu.Lock() at, ok := ch.nodesFail[cs.Key] @@ -1070,3 +1085,61 @@ func (ch *Checker) movedSince(cs *Case) bool { } return false } + +// awaitRebuilt hears, once per verdict key, the builder's keys of the builds a failed verdict names: when each is written +// done again (builder_build_again), the check of those heads is asked again (claim retakes it), as a failure of the nodes +// is after the nodes changed, so no recheck by hand is needed (ADR 0603 §10). Heard on the builder's own writes, never +// polled; it ends with the checker. +func (ch *Checker) awaitRebuilt(ctx context.Context, group, vkey string, builds map[string]string) { + if ch.Rebuilds == nil { + return + } + ch.mu.Lock() + if ch.awaiting[vkey] { + ch.mu.Unlock() + return + } + ch.awaiting[vkey] = true + ch.mu.Unlock() + ch.wg.Add(1) + go func() { + defer ch.wg.Done() + done := ch.Rebuilds.Done(ctx, builds) + ch.mu.Lock() + delete(ch.awaiting, vkey) + if done { + ch.rebuilt[vkey] = true + } + c, known := ch.known[group] + ch.mu.Unlock() + if done && known && ctx.Err() == nil { + ch.logf("%s: the builds of %s it failed on are done again, so it is checked again (ADR 0603 §10)", vkey, strings.Join(slices.Sorted(maps.Keys(builds)), ", ")) + ch.consider(ctx, group, c) + } + }() +} + +// hearFailedBuilds starts awaitRebuilt for the case's verdict when it names failed builds (failedBuilds). +func (ch *Checker) hearFailedBuilds(ctx context.Context, group string, cs *Case) { + if ch.Rebuilds == nil || ctx.Err() != nil { + return + } + value, _, found, err := ch.Verdicts.Get(ctx, cs.Key) + if err != nil || !found { + return + } + if v, err := delivery.DecodeVerdict(value); err == nil { + if builds := failedBuilds(v, cs); builds != nil { + ch.awaitRebuilt(ctx, group, cs.Key, builds) + } + } +} + +// takeRebuilt says whether every failed build the verdict of vkey names is done again, and forgets it: one retake each. +func (ch *Checker) takeRebuilt(vkey string) bool { + ch.mu.Lock() + defer ch.mu.Unlock() + was := ch.rebuilt[vkey] + delete(ch.rebuilt, vkey) + return was +} diff --git a/modules/mesh-delivery-checker/internal/check/composition_test.go b/modules/mesh-delivery-checker/internal/check/composition_test.go index 53f42d07..72ec4ecb 100644 --- a/modules/mesh-delivery-checker/internal/check/composition_test.go +++ b/modules/mesh-delivery-checker/internal/check/composition_test.go @@ -125,6 +125,8 @@ func (f *fakeEnds) Ended(_ context.Context, builds map[string]string) (map[strin return out, nil } +func (f *fakeEnds) Done(context.Context, map[string]string) bool { return false } + // knownRuns are the modules the trunk holds a record of (the indexer's modules): any commit. type knownRuns []string @@ -153,7 +155,8 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) { runs knownRuns // nil: the trunk holds every module want []string waited string // the modules waited on, sorted - failed string // the failure's test, when the check fails + failed string // the failure's test, when the check fails on a build + tests bool // the tests fail: the check fails before it would wait }{ {name: "the Go build module changes", manifest: map[string]catalogue.Manifest{"mesh-build-go": {Module: "mesh-build-go", Build: goBuild}, "web": {Module: "web", Build: goBuild}, @@ -175,6 +178,9 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) { {name: "a build fails", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}, "mail": {Module: "mail", Build: goBuild}}, builds: map[string]delivery.BuilderBuild{"web": {State: delivery.BuildFailed, Error: "go build: undefined: x"}, "mail": done}, waited: "mail, web", failed: "build: web"}, + {name: "the tests fail", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}}, + builds: map[string]delivery.BuilderBuild{"web": {State: delivery.BuildBuilding}}, tests: true, + want: []string{"composes at once, without waiting on the builds of web"}}, {name: "a build does not end within the bound", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}}, builds: map[string]delivery.BuilderBuild{"web": {State: delivery.BuildBuilding}}, want: []string{"the builds of web did not end within the build-wait-bound"}, waited: "web"}, @@ -185,6 +191,13 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) { want: []string{"the builds of web cannot be heard"}}, } { b := candidateBench(t, true) + if row.tests { + b.test = func(c stepCall) (any, error) { + r := captured[Tested](t, "go-test-failing") + r.Level, _ = c.args["level"].(string) + return r, nil + } + } comps := &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionPass, SDK: "x", Machines: []string{"g14"}}} b.ex.Compositions = comps var ends *fakeEnds @@ -216,9 +229,12 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) { } } switch { - case row.failed != "" && (v.State != delivery.VerdictFail || v.Failed.Test != row.failed || !strings.Contains(v.Failed.Words, "undefined: x")): - t.Errorf("%s: the verdict is %s failing %+v, want it failed naming %s and the builder's words", row.name, v.State, v.Failed, row.failed) - case row.failed == "" && v.State != delivery.VerdictPass: + case row.failed != "" && (v.State != delivery.VerdictFail || v.Failed.Test != row.failed || !strings.Contains(v.Failed.Words, "undefined: x") || + !strings.Contains(v.Failed.Words, "the check asks itself again when the build is done")): + t.Errorf("%s: the verdict is %s failing %+v, want it failed naming %s, the builder's words and how it is asked again", row.name, v.State, v.Failed, row.failed) + case row.tests && v.State != delivery.VerdictFail: + t.Errorf("%s: %+v, want the tests' failure", row.name, v) + case row.failed == "" && !row.tests && v.State != delivery.VerdictPass: t.Errorf("%s: %+v", row.name, v) } words := compositionWords(v.Composition, v.Took.Composition) diff --git a/modules/mesh-delivery-checker/internal/check/read.go b/modules/mesh-delivery-checker/internal/check/read.go index 2086eced..1fc0efbe 100644 --- a/modules/mesh-delivery-checker/internal/check/read.go +++ b/modules/mesh-delivery-checker/internal/check/read.go @@ -350,7 +350,7 @@ type Status struct { Lost map[string]string `json:"lost"` // BuildsNotEnded are, of each candidate whose check runs, the builder's builds of its changed modules that are neither // done nor failed, by group: ` `, sorted (no key yet is `not begun`). A check waits on them before it - // composes (ADR 0603 §10), but for the modules its line names as composed without them; builder_build_again builds a failed one again. + // composes (ADR 0603 §10), but for the modules its line names as composed without them; builder_build_again builds a failed one again, and the check asks itself again when it is done. BuildsNotEnded map[string]string `json:"builds_not_ended,omitempty"` // DeadLetters says whether the checker hears the asks the build seats give up on (issue 734): deaf, a try or check // whose ask is given up waits until its bound. From 70ac4ff79a70c9a24c5380e125729cb86f5126e1 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 18:41:10 +0100 Subject: [PATCH 45/49] =?UTF-8?q?The=20indexer's=20integration=20test=20ex?= =?UTF-8?q?pects=20a=20field=20from=20later=20to=20wait,=20not=20be=20refu?= =?UTF-8?q?sed,=20while=20no=20engine=20says=20which=20build=20runs=20(ADR?= =?UTF-8?q?=200601=20=C2=A73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- .../mesh-delivery-indexer/integration_test.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/integration_test.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/integration_test.go index 5685dc01..a28b8db5 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/integration_test.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/integration_test.go @@ -229,7 +229,7 @@ func TestIndexerWritesModulesAtAMerge(t *testing.T) { // 1239/AC9; I1, I3: the m // its inputs and the reason it has no fingerprint of the new form, and manifests. with the manifest byte for byte. // // And it judges them (novox/hq ADR 0578, issue 1585): with three manifests more, one valid, one refused and one with a -// field the decoder does not know, mesh-sdk's bus reader read before the indexer judged composes all three, the field set +// field the decoder does not know (it waits, ADR 0601 §3), mesh-sdk's bus reader read before the indexer judged composes all three, the field set // aside and named (a missing verdict is accepted, §3); the indexer writes the three verdicts, says manifest-refused for // each refusal, and the reader then composes the valid one and holds the others, named, never ending its read. func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 1585 @@ -300,7 +300,7 @@ func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 15 if err != nil { t.Fatal(err) } - want := map[string]string{"good": delivery.VerdictAccepted, "bad": delivery.VerdictRefused, "later": delivery.VerdictRefused} + want := map[string]string{"good": delivery.VerdictAccepted, "bad": delivery.VerdictRefused, "later": delivery.VerdictManifestWaits} got := map[string]delivery.ManifestVerdict{} for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline) && len(got) < len(want); time.Sleep(50 * time.Millisecond) { for name := range want { @@ -316,10 +316,12 @@ func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 15 t.Errorf("the verdict on %s = %+v within 10 s, want %s by the running judge", name, v, state) } } - if !strings.Contains(strings.Join(got["bad"].Problems, " "), `provides "amqp"`) || !strings.Contains(strings.Join(got["later"].Problems, " "), "a-field-from-later") { - t.Errorf("the refusals name no problem: %+v", got) + // ADR 0601 §3: the field this judge does not know waits and is named, since no node-engine answers which build of the + // indexer runs here (the unit tests of the index package prove it refused once that build is the trunk's). + if !strings.Contains(strings.Join(got["bad"].Problems, " "), `provides "amqp"`) || strings.Join(got["later"].Unknown, " ") != "a-field-from-later" { + t.Errorf("the refusal names no problem or the wait no field: %+v", got) } - // manifest-refused, said once for each refusal. + // manifest-refused, said once for each refusal (a wait is no refusal). events, err := js.Stream(ctx, "INDEXER_EVENTS") if err != nil { t.Fatal(err) @@ -335,8 +337,8 @@ func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 15 said[r.Module] = true } } - if !said["bad"] || !said["later"] || said["good"] { - t.Errorf("manifest-refused said for %v, want bad and later", said) + if !said["bad"] || said["later"] || said["good"] { + t.Errorf("manifest-refused said for %v, want bad only", said) } // How a composer holds a refused module by these verdicts is the node-engine's to test (ADR 0593 §7: the composer // and its bus reader are its own). From b96a4f1d99c052fb8c8cffa68afed2029be629a8 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 18:50:12 +0100 Subject: [PATCH 46/49] Pin mesh-sdk b7abe203a929, the group's head of feat/l3b-bus: it keeps, deprecated, the exported names the running parts still use Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- modules/mesh-delivery-assigner/go.mod | 2 +- modules/mesh-delivery-assigner/go.sum | 4 ++-- modules/mesh-delivery-builder/buildrun/go.mod | 2 +- modules/mesh-delivery-builder/buildrun/go.sum | 4 ++-- modules/mesh-delivery-builder/go.mod | 2 +- modules/mesh-delivery-builder/go.sum | 4 ++-- modules/mesh-delivery-checker/go.mod | 2 +- modules/mesh-delivery-checker/go.sum | 4 ++-- modules/mesh-delivery-forge-bridge/go.mod | 2 +- modules/mesh-delivery-forge-bridge/go.sum | 4 ++-- modules/mesh-delivery-indexer/go.mod | 2 +- modules/mesh-delivery-indexer/go.sum | 4 ++-- modules/mesh-delivery-teller/go.mod | 2 +- modules/mesh-delivery-teller/go.sum | 4 ++-- modules/mesh-delivery-view/go.mod | 2 +- modules/mesh-delivery-view/go.sum | 4 ++-- modules/mesh-delivery-watchman/go.mod | 2 +- modules/mesh-delivery-watchman/go.sum | 4 ++-- modules/mesh-enrolment/go.mod | 2 +- modules/mesh-enrolment/go.sum | 4 ++-- modules/mesh-grants/go.mod | 2 +- modules/mesh-grants/go.sum | 4 ++-- sdk/go.mod | 2 +- sdk/go.sum | 4 ++-- 24 files changed, 36 insertions(+), 36 deletions(-) diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index 9ea2c53d..d08194bd 100644 --- a/modules/mesh-delivery-assigner/go.mod +++ b/modules/mesh-delivery-assigner/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-assigner go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-assigner/go.sum b/modules/mesh-delivery-assigner/go.sum index 764e281d..abad2969 100644 --- a/modules/mesh-delivery-assigner/go.sum +++ b/modules/mesh-delivery-assigner/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/buildrun/go.mod b/modules/mesh-delivery-builder/buildrun/go.mod index ee2e0bdc..bd7aa559 100644 --- a/modules/mesh-delivery-builder/buildrun/go.mod +++ b/modules/mesh-delivery-builder/buildrun/go.mod @@ -3,7 +3,7 @@ module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/buildrun/go.sum b/modules/mesh-delivery-builder/buildrun/go.sum index 764e281d..abad2969 100644 --- a/modules/mesh-delivery-builder/buildrun/go.sum +++ b/modules/mesh-delivery-builder/buildrun/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/go.mod b/modules/mesh-delivery-builder/go.mod index 188e0da9..70c1b694 100644 --- a/modules/mesh-delivery-builder/go.mod +++ b/modules/mesh-delivery-builder/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-builder go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/go.sum b/modules/mesh-delivery-builder/go.sum index bf8fed0c..8518fa28 100644 --- a/modules/mesh-delivery-builder/go.sum +++ b/modules/mesh-delivery-builder/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index 332e93f6..2927751e 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-checker go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-checker/go.sum b/modules/mesh-delivery-checker/go.sum index bf8fed0c..8518fa28 100644 --- a/modules/mesh-delivery-checker/go.sum +++ b/modules/mesh-delivery-checker/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-forge-bridge/go.mod b/modules/mesh-delivery-forge-bridge/go.mod index c26ab21a..c3e1e343 100644 --- a/modules/mesh-delivery-forge-bridge/go.mod +++ b/modules/mesh-delivery-forge-bridge/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-forge-bridge go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-forge-bridge/go.sum b/modules/mesh-delivery-forge-bridge/go.sum index 764e281d..abad2969 100644 --- a/modules/mesh-delivery-forge-bridge/go.sum +++ b/modules/mesh-delivery-forge-bridge/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-indexer/go.mod b/modules/mesh-delivery-indexer/go.mod index 24118bf4..36f08934 100644 --- a/modules/mesh-delivery-indexer/go.mod +++ b/modules/mesh-delivery-indexer/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-indexer go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-indexer/go.sum b/modules/mesh-delivery-indexer/go.sum index 764e281d..abad2969 100644 --- a/modules/mesh-delivery-indexer/go.sum +++ b/modules/mesh-delivery-indexer/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-teller/go.mod b/modules/mesh-delivery-teller/go.mod index e0b216ec..28754187 100644 --- a/modules/mesh-delivery-teller/go.mod +++ b/modules/mesh-delivery-teller/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-teller go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-teller/go.sum b/modules/mesh-delivery-teller/go.sum index 764e281d..abad2969 100644 --- a/modules/mesh-delivery-teller/go.sum +++ b/modules/mesh-delivery-teller/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index e9f4bcec..ff85b24b 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-view go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index 764e281d..abad2969 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-watchman/go.mod b/modules/mesh-delivery-watchman/go.mod index 575686f9..b7c67621 100644 --- a/modules/mesh-delivery-watchman/go.mod +++ b/modules/mesh-delivery-watchman/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-watchman go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-watchman/go.sum b/modules/mesh-delivery-watchman/go.sum index 0790a735..2f163f06 100644 --- a/modules/mesh-delivery-watchman/go.sum +++ b/modules/mesh-delivery-watchman/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-enrolment/go.mod b/modules/mesh-enrolment/go.mod index d7d484d9..733bf732 100644 --- a/modules/mesh-enrolment/go.mod +++ b/modules/mesh-enrolment/go.mod @@ -3,7 +3,7 @@ module mesh-enrolment go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-enrolment/go.sum b/modules/mesh-enrolment/go.sum index 764e281d..abad2969 100644 --- a/modules/mesh-enrolment/go.sum +++ b/modules/mesh-enrolment/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-grants/go.mod b/modules/mesh-grants/go.mod index 90f1e501..a90cee61 100644 --- a/modules/mesh-grants/go.mod +++ b/modules/mesh-grants/go.mod @@ -3,7 +3,7 @@ module mesh-grants go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-grants/go.sum b/modules/mesh-grants/go.sum index 764e281d..abad2969 100644 --- a/modules/mesh-grants/go.sum +++ b/modules/mesh-grants/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/sdk/go.mod b/sdk/go.mod index 31746712..1ae3ea03 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -2,4 +2,4 @@ module git.novox.be/novox/mesh-delivery/sdk go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 diff --git a/sdk/go.sum b/sdk/go.sum index cea3187b..30b01cbe 100644 --- a/sdk/go.sum +++ b/sdk/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 h1:RtAFbUJRP1qO9AIMM2ue+cXNzqj7WZ7IOlSqNvttwuU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= From 34ef185ba5af4648e541e5b492bee9da27bd14bd Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 19:33:43 +0100 Subject: [PATCH 47/49] The assigner's integration test puts the provider's module before the consumer's, so the consumer's requirement always has a provider to be judged against (the group check's flaky failure, 5 of 40 runs) --- .../cmd/mesh-delivery-assigner/integration_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go index cadba393..7b456f16 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go @@ -448,9 +448,12 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) { toolDigest := b.manifest(t, "tool", `{"module":"tool","version":"1"}`) fpOf := func(c byte) string { return delivery.SourcePrefix2 + strings.Repeat(string(c), 64) } toolOld := fpOf('1') - for module, row := range map[string]struct { - fp, digest string - }{"db": {fpOf('2'), dbDigest}, "app": {fpOf('3'), appDigest}, "tool": {fpOf('4'), toolDigest}} { + // In this order: the provider's module is in the indexer's state before the consumer's, so the consumer's requirement + // has a provider to be judged against whenever the assigner looks at it (a map's order made it flaky, 5 of 40 runs). + for _, row := range []struct { + module, fp, digest string + }{{"db", fpOf('2'), dbDigest}, {"app", fpOf('3'), appDigest}, {"tool", fpOf('4'), toolDigest}} { + module := row.module mod, _ := delivery.EncodeModule(delivery.Module{FP2: row.fp, Manifest: row.digest, Repository: "novox/" + module, Commit: strings.Repeat("2", 40), Path: delivery.RootPath}) b.putJSON(t, delivery.ModulesBucket, module, mod) fps := []string{row.fp} From 1c66d58bce0d93901b34a99371cf8acda97d485a Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 19:36:18 +0100 Subject: [PATCH 48/49] =?UTF-8?q?The=20assigner=20waits=20for=20a=20provis?= =?UTF-8?q?ion=20whose=20provider=20is=20not=20known=20yet:=20an=20unknown?= =?UTF-8?q?=20provider=20has=20provided=20nothing,=20so=20a=20consumer=20n?= =?UTF-8?q?ever=20moves=20ahead=20of=20a=20provider=20the=20indexer=20has?= =?UTF-8?q?=20not=20taken=20in=20(ADR=200603=20=C2=A75);=20the=20integrati?= =?UTF-8?q?on=20test's=20write=20order=20is=20unordered=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mesh-delivery-assigner/integration_test.go | 9 +++------ .../internal/assign/wants.go | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go index 7b456f16..cadba393 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go @@ -448,12 +448,9 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) { toolDigest := b.manifest(t, "tool", `{"module":"tool","version":"1"}`) fpOf := func(c byte) string { return delivery.SourcePrefix2 + strings.Repeat(string(c), 64) } toolOld := fpOf('1') - // In this order: the provider's module is in the indexer's state before the consumer's, so the consumer's requirement - // has a provider to be judged against whenever the assigner looks at it (a map's order made it flaky, 5 of 40 runs). - for _, row := range []struct { - module, fp, digest string - }{{"db", fpOf('2'), dbDigest}, {"app", fpOf('3'), appDigest}, {"tool", fpOf('4'), toolDigest}} { - module := row.module + for module, row := range map[string]struct { + fp, digest string + }{"db": {fpOf('2'), dbDigest}, "app": {fpOf('3'), appDigest}, "tool": {fpOf('4'), toolDigest}} { mod, _ := delivery.EncodeModule(delivery.Module{FP2: row.fp, Manifest: row.digest, Repository: "novox/" + module, Commit: strings.Repeat("2", 40), Path: delivery.RootPath}) b.putJSON(t, delivery.ModulesBucket, module, mod) fps := []string{row.fp} diff --git a/modules/mesh-delivery-assigner/internal/assign/wants.go b/modules/mesh-delivery-assigner/internal/assign/wants.go index a7f19d62..56704d68 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants.go @@ -154,10 +154,26 @@ func (m Module) provisionWaiting(node string) (string, bool) { } keyed := m.Provisions(node) // Only a provision the newest manifest still requires is waited on (ADR 0603 §9): a key of an older revision's edge, - // asked or retiring for the operator's answer (ADR 0570 §5), holds nothing. + // asked or retiring for the operator's answer (ADR 0570 §5), holds nothing. A requirement catalogue.Awaited drops for a + // reason the decision keeps (optional, not keyed: a toolchain on the module's own machine, a login the controller makes) is + // not waited on; one it drops only because no answering provider is known yet is (ADR 0603 §5: an unknown provider has + // provided nothing), so a consumer never moves ahead of a provider whose module the indexer has not yet taken in. needed := m.Manifest.Requires if m.Answering != nil { needed = catalogue.Awaited(m.Manifest, m.Answering) + have := map[string]bool{} + for _, p := range keyed { + have[p.Provision] = true + } + for _, r := range slices.Sorted(slices.Values(m.Manifest.Requires)) { + if m.Manifest.OptionalRequires[r] || len(m.Answering(r)) > 0 || slices.Contains(needed, r) { + continue + } + needed = append(needed, r) + if !have[r] { + return fmt.Sprintf("no provider of %s is known yet for %s on %s: an unknown provider has provided nothing", r, m.Name, node), true + } + } } for _, key := range slices.Sorted(maps.Keys(keyed)) { if p := keyed[key]; p.State != provisions.Provided && slices.Contains(needed, p.Provision) { From 945906ac976c4ec61a0947f128bdc07ac2e3f358 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 19:45:15 +0100 Subject: [PATCH 49/49] =?UTF-8?q?The=20assigner=20waits=20for=20a=20requir?= =?UTF-8?q?ement=20nothing=20offers,=20and=20for=20a=20provision=20the=20p?= =?UTF-8?q?rovisioner=20keys=20whoever=20is=20placed=20yet;=20a=20requirem?= =?UTF-8?q?ent=20something=20offers=20(a=20module's=20own=20name,=20a=20mo?= =?UTF-8?q?dule=20the=20mesh=20provides=20itself)=20is=20left=20to=20Await?= =?UTF-8?q?ed=20as=20before=20(ADR=200603=20=C2=A75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration_test.go | 19 ++++++++-------- .../cmd/mesh-delivery-assigner/pass.go | 22 ++++++++++++++++++- .../cmd/mesh-delivery-assigner/pass_test.go | 8 +++++++ .../internal/assign/wants.go | 22 ++++++++++++++----- .../internal/assign/wants_test.go | 18 +++++++++++++++ 5 files changed, 73 insertions(+), 16 deletions(-) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go index cadba393..32069148 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go @@ -446,6 +446,16 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) { dbDigest := b.manifest(t, "db", `{"module":"db","version":"1",`+offer+`}`) appDigest := b.manifest(t, "app", `{"module":"app","version":"1","requires":["database"]}`) toolDigest := b.manifest(t, "tool", `{"module":"tool","version":"1"}`) + asked := func(state, why string) { + raw, err := provisions.Encode(provisions.Provision{V: provisions.Version, Provision: "database", Consumer: provisions.End{Module: "app", Node: "ace"}, + Provider: provisions.End{Module: "db", Node: "ace"}, State: state, Why: why, At: time.Now()}) + if err != nil { + t.Fatal(err) + } + b.putJSON(t, provisions.ProvisionsBucket, "ace.app.database", raw) + } + // The provisioner keys it from the manifests, as soon as they are known and whether the provider is placed yet or not. + asked(provisions.Asked, "the provider's build is not done") fpOf := func(c byte) string { return delivery.SourcePrefix2 + strings.Repeat(string(c), 64) } toolOld := fpOf('1') for module, row := range map[string]struct { @@ -469,15 +479,6 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) { } runsOld, _ := delivery.EncodeHave(delivery.Have{FP: toolOld, Phase: delivery.PhaseRunning, Health: delivery.HealthOK}) b.putJSON(t, delivery.HaveBucket, "ace.tool", runsOld) - asked := func(state, why string) { - raw, err := provisions.Encode(provisions.Provision{V: provisions.Version, Provision: "database", Consumer: provisions.End{Module: "app", Node: "ace"}, - Provider: provisions.End{Module: "db", Node: "ace"}, State: state, Why: why, At: time.Now()}) - if err != nil { - t.Fatal(err) - } - b.putJSON(t, provisions.ProvisionsBucket, "ace.app.database", raw) - } - asked(provisions.Asked, "the provider's build is not done") if w := b.wantNow(t, "ace", "tool", "the build module's want at the second build", func(w delivery.Want) bool { return w.FP == fpOf('4') }); w.FP != fpOf('4') { t.Errorf("tool's want = %+v", w) } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go index 1992c412..73e1b9c4 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go @@ -158,7 +158,7 @@ func (p *passer) wants(ctx context.Context) { for name := range names { fp := trunk[name].FP2 name := name - m := assign.Module{Name: name, FP: fp, Manifest: manifests[name], Chosen: chosenOf(chosen, name), Provisions: provisionsOf(name), Answering: answering(manifests, nodes), Nodes: nodesOf[name], Now: time.Now(), Puts: puts[name], + m := assign.Module{Name: name, FP: fp, Manifest: manifests[name], Chosen: chosenOf(chosen, name), Provisions: provisionsOf(name), Answering: answering(manifests, nodes), Offered: offered(manifests), Nodes: nodesOf[name], Now: time.Now(), Puts: puts[name], Self: func(node string) bool { return placed[node] }, Composed: func(node, fp string) (delivery.ComposedDigest, bool) { return p.w.composed(node, name, fp) // a placed node's own: the digest its own have names (ADR 0571 §6) @@ -1086,6 +1086,26 @@ func (p *passer) seatHolders(ctx context.Context) { } } +// offered says whether some manifest of the trunk, or a module the mesh provides itself, offers a provision under its name +// (a module offers its own name too, catalogue.Manifest.Offers): what a requirement nobody offers is told from. +func offered(manifests map[string]catalogue.Manifest) func(string) bool { + names := map[string]bool{} + add := func(m catalogue.Manifest) { + for _, o := range m.Offers() { + names[o] = true + } + } + for _, m := range manifests { + add(m) + } + for _, module := range delivery.ProvidedModules() { + if m, _, err := delivery.ProvidedManifest(module); err == nil { + add(m) + } + } + return func(provision string) bool { return names[provision] } +} + // answering is, by provision, the manifests of the modules placed on some node that provide it: what a want's // provisions are judged by (catalogue.Awaited, issue 774). A provider in the catalogue placed nowhere answers nobody. func answering(manifests map[string]catalogue.Manifest, n assign.Nodes) func(string) []catalogue.Manifest { diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go index d9912d36..79baa718 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go @@ -111,6 +111,14 @@ func TestAHoldAskedAgainAfterALiftAndARestartHasANewID(t *testing.T) { // 1906/H4: an answer counts only for the asking it answered: an answer to an earlier hold of the same fingerprint, or to an ask // taken away, is not taken for the hold that stands now. func TestAnAnswerCountsOnlyForTheAskingItAnswered(t *testing.T) { + // ADR 0603 §5: a provision is offered by a manifest's provides, by a module's own name, and by a module the mesh provides + // itself (mesh-addressing, mesh-wireguard's); nothing else. + off := offered(map[string]catalogue.Manifest{"node-tools": {Module: "node-tools"}, "db": {Module: "db", Provides: []catalogue.Offer{{Name: "database"}}}}) + for provision, want := range map[string]bool{"node-tools": true, "database": true, "mesh-addressing": true, "queue": false} { + if off(provision) != want { + t.Errorf("offered(%s) = %v, want %v", provision, !want, want) + } + } key, _ := delivery.HoldAskKey("web", fpTest) p := newPasser(buckets{}, time.Now()) put := func(asking int) string { diff --git a/modules/mesh-delivery-assigner/internal/assign/wants.go b/modules/mesh-delivery-assigner/internal/assign/wants.go index 56704d68..ac7cc4fa 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants.go @@ -36,6 +36,10 @@ type Module struct { // Answering is the manifests of the modules placed somewhere that provide a provision: what catalogue.Awaited // judges a want's provisions by (issue 774). Nil: none known, and no key is waited for that is not there. Answering func(provision string) []catalogue.Manifest + // Offered says whether any module offers a provision by name or by its own name (catalogue.Manifest.Offers) among every + // manifest the trunk holds and the modules the mesh provides itself, placed or not. Nil: not known, and no requirement + // is waited on for want of an offer. + Offered func(provision string) bool // Chosen is a person's choice of the module's upgrade policy (the assigner's `policies`, ADR 0565 §7); nil: none. Chosen *catalogue.PersonPolicy Nodes []string @@ -154,10 +158,12 @@ func (m Module) provisionWaiting(node string) (string, bool) { } keyed := m.Provisions(node) // Only a provision the newest manifest still requires is waited on (ADR 0603 §9): a key of an older revision's edge, - // asked or retiring for the operator's answer (ADR 0570 §5), holds nothing. A requirement catalogue.Awaited drops for a - // reason the decision keeps (optional, not keyed: a toolchain on the module's own machine, a login the controller makes) is - // not waited on; one it drops only because no answering provider is known yet is (ADR 0603 §5: an unknown provider has - // provided nothing), so a consumer never moves ahead of a provider whose module the indexer has not yet taken in. + // asked or retiring for the operator's answer (ADR 0570 §5), holds nothing. A requirement catalogue.Awaited leaves out is + // not waited on when something offers it (a module's own name, a module the mesh provides itself, a provision the + // composition does not key: optional, a toolchain on the module's own machine, a login the controller makes). One that + // nothing offers is waited on (ADR 0603 §5: an unknown provider has provided nothing), so a consumer never moves ahead of + // a provider whose module the indexer has not yet taken in; it holds loudly, since the watchman raises the hold past its + // bound (ADR 0603 §8). needed := m.Manifest.Requires if m.Answering != nil { needed = catalogue.Awaited(m.Manifest, m.Answering) @@ -166,12 +172,16 @@ func (m Module) provisionWaiting(node string) (string, bool) { have[p.Provision] = true } for _, r := range slices.Sorted(slices.Values(m.Manifest.Requires)) { - if m.Manifest.OptionalRequires[r] || len(m.Answering(r)) > 0 || slices.Contains(needed, r) { + if have[r] && !m.Manifest.OptionalRequires[r] && !slices.Contains(needed, r) { + needed = append(needed, r) // the provisioner keys it from the manifests, whoever is placed yet + continue + } + if m.Offered == nil || m.Manifest.OptionalRequires[r] || len(m.Answering(r)) > 0 || m.Offered(r) || slices.Contains(needed, r) { continue } needed = append(needed, r) if !have[r] { - return fmt.Sprintf("no provider of %s is known yet for %s on %s: an unknown provider has provided nothing", r, m.Name, node), true + return fmt.Sprintf("no module offers %s yet for %s on %s: an unknown provider has provided nothing, so the want waits for one, and the watchman raises it past its bound", r, m.Name, node), true } } } diff --git a/modules/mesh-delivery-assigner/internal/assign/wants_test.go b/modules/mesh-delivery-assigner/internal/assign/wants_test.go index 00c91352..39972c46 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants_test.go @@ -176,6 +176,24 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { } } } + // ADR 0603 §5: a requirement nobody offers is an unknown provider and waits, loudly; one something offers (a module's own + // name, a module the mesh provides itself) is left to catalogue.Awaited as before and does not. + for _, c := range []struct { + name, requires string + offered bool + }{{"a module's own name (node-tools)", "node-tools", true}, {"a module the mesh provides itself (mesh-addressing)", "mesh-addressing", true}, + {"a provision nobody offers", "database", false}} { + m, n := ready() + m.Manifest.Requires = []string{c.requires} + m.Answering = func(string) []catalogue.Manifest { return nil } + m.Offered = func(p string) bool { return c.offered && p == c.requires } + m.Provisions = func(string) map[string]provisions.Provision { return map[string]provisions.Provision{} } + out := Wants(m, n, wantedOld(), nil) + if got := out.Wants["g14"].FP == fNew; got != c.offered || (!c.offered && (out.Rules["g14"].Rule != delivery.RuleProvisions || + !strings.Contains(out.Rules["g14"].Words, "no module offers database yet") || !strings.Contains(out.Rules["g14"].Words, "past its bound"))) { + t.Errorf("%s: want %+v, held by %+v", c.name, out.Wants["g14"], out.Rules["g14"]) + } + } // 1960: rule 5 holds no want, and no module is left without a first want for a provider down or a node offline; the // provider down is said (Unreached). novox on 2026-10-29: route-proxy down held de-spiegel and link2pay, minio not // healthy held photos, and novox offline left a module with no canary.