From 26852f4a9d76fef1e50a342ebc4c08b8070bcd84 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 1 Nov 2026 02:09:50 +0100 Subject: [PATCH] =?UTF-8?q?Raise=20a=20refused=20manifest=20as=20the=20wat?= =?UTF-8?q?chman's=20finding,=20so=20the=20operator=20hears=20it=20once=20?= =?UTF-8?q?(ADR=200578=20=C2=A71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue 1585, S10: the watchman watches the indexer's modules and finds manifest-refused for a module whose value carries a refusal, ending it once accepted. Its mesh-sdk pin moves to the group head, so it takes health.Finding's Ref (issue 1952 renamed Token). Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- .../cmd/mesh-delivery-watchman/main.go | 14 +++++++ modules/mesh-delivery-watchman/go.mod | 2 +- modules/mesh-delivery-watchman/go.sum | 2 + .../internal/watch/finding.go | 4 +- .../internal/watch/findings_test.go | 18 +++++++++ .../internal/watch/verdicts.go | 40 +++++++++++++++++++ .../mesh-delivery-watchman/manifest_test.go | 2 +- modules/mesh-delivery-watchman/module.json | 1 + 8 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 modules/mesh-delivery-watchman/internal/watch/verdicts.go 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 95af732..a513fdc 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go @@ -345,6 +345,17 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro go func() { _ = follow(ctx, hw, healthEach) }() }) } + // The indexer's modules (ADR 0578 §1): a module whose value carries a refusal of its manifest is a finding; read + // before the findings are reconciled, each change heard on the watch. + verdictEach := func(e jetstream.KeyValueEntry) { w.ModuleVerdict(e.Key(), e.Value(), opOf(e)) } + var verdictWatch jetstream.KeyWatcher + if kv, err := js.KeyValue(ctx, delivery.ModulesBucket); err == nil { + if verdictWatch, err = settle(ctx, kv, verdictEach); err != nil { + process.NotWatched(logger.Printf, delivery.ModulesBucket, err, "no refused manifest is said until the watchman starts again") + } + } else { + process.NotWatched(logger.Printf, delivery.ModulesBucket, err, "no refused manifest is said until the watchman starts again") + } dead := &deadKeeper{ctx: ctx, js: js, w: w, logf: logger.Printf} w.SetRecount(dead.count) dead.count() @@ -360,6 +371,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro if healthWatch != nil { go func() { errs <- follow(ctx, healthWatch, healthEach) }() } + if verdictWatch != nil { + go func() { errs <- follow(ctx, verdictWatch, verdictEach) }() + } if provWatch != nil { go func() { errs <- follow(ctx, provWatch, func(e jetstream.KeyValueEntry) { w.Provision(e.Key(), e.Value(), opOf(e)) }) diff --git a/modules/mesh-delivery-watchman/go.mod b/modules/mesh-delivery-watchman/go.mod index b37d585..f1f14a5 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.20261030201806-eff81f1220e2 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502 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 f59f301..c879817 100644 --- a/modules/mesh-delivery-watchman/go.sum +++ b/modules/mesh-delivery-watchman/go.sum @@ -1,5 +1,7 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030201806-eff81f1220e2 h1:RF73nVI1dTrrmlFosVDTMJmpUQ+rEeB7++BHH/RvZFg= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030201806-eff81f1220e2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502 h1:XndvkzyAFNde/821lEfYObqMTURzmve/w9Ks+QrBBJ8= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005736-7b09de1e5502/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/finding.go b/modules/mesh-delivery-watchman/internal/watch/finding.go index fce809b..d3d435c 100644 --- a/modules/mesh-delivery-watchman/internal/watch/finding.go +++ b/modules/mesh-delivery-watchman/internal/watch/finding.go @@ -67,7 +67,7 @@ func EncodeFinding(f Finding) ([]byte, error) { if said == "" { said = f.Summary } - return health.EncodeFinding(health.Finding{Key: f.Key(), Kind: f.Kind, Token: f.Token, + return health.EncodeFinding(health.Finding{Key: f.Key(), Kind: f.Kind, Ref: f.Token, Subject: health.Subject{Scope: f.Scope, ID: f.ID, Machine: f.Machine, Also: f.Also}, Severity: health.Severity(f.Severity), Summary: f.Summary, Headline: f.Headline, Explanation: f.Explanation, Resolved: f.Resolved, Needs: f.Needs, Evidence: []health.Evidence{{At: f.Since.UTC(), Said: said}}, @@ -80,7 +80,7 @@ func DecodeFinding(data []byte) (Finding, error) { if err != nil { return Finding{}, err } - f := Finding{Scope: h.Subject.Scope, ID: h.Subject.ID, Token: h.Token, Kind: h.Kind, Machine: h.Subject.Machine, + f := Finding{Scope: h.Subject.Scope, ID: h.Subject.ID, Token: h.Ref, Kind: h.Kind, Machine: h.Subject.Machine, Also: h.Subject.Also, Severity: string(h.Severity), Summary: h.Summary, Headline: h.Headline, Explanation: h.Explanation, Needs: h.Needs, Resolved: h.Resolved, Source: h.Source, Observer: h.Observer, Since: h.At} if len(h.Evidence) > 0 { diff --git a/modules/mesh-delivery-watchman/internal/watch/findings_test.go b/modules/mesh-delivery-watchman/internal/watch/findings_test.go index 4466789..582d23b 100644 --- a/modules/mesh-delivery-watchman/internal/watch/findings_test.go +++ b/modules/mesh-delivery-watchman/internal/watch/findings_test.go @@ -33,6 +33,24 @@ func TestTheWatchmansFindings(t *testing.T) { // want are the finding keys standing at the end, with their severity. want map[string]string }{ + {"ADR 0578 §1: a module whose value carries a refusal of its manifest is a finding until it is accepted", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { + value := func(verdict string, problems ...string) []byte { + b, err := delivery.EncodeModule(delivery.Module{FP2: "src2:" + strings.Repeat("a", 64), Manifest: strings.Repeat("b", 64), + Repository: "novox/mesh-catalog", Commit: "c", Verdict: verdict, Problems: problems}) + if err != nil { + t.Fatal(err) + } + return b + } + w.ModuleVerdict("nats", value(delivery.VerdictRefused, "removes a stream"), Put) + w.ModuleVerdict("power", value(delivery.VerdictRefused, "shows nothing"), Put) + k := FindingKey(ScopeModule, "nats", KindManifestRefused) + if f := o.findings[k]; !strings.Contains(f.Summary, "refused the manifest of nats: removes a stream") { + t.Fatalf("findings = %v", o.findings) + } + w.ModuleVerdict("nats", value(""), Put) + w.ModuleVerdict("power", 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/verdicts.go b/modules/mesh-delivery-watchman/internal/watch/verdicts.go new file mode 100644 index 0000000..6044d49 --- /dev/null +++ b/modules/mesh-delivery-watchman/internal/watch/verdicts.go @@ -0,0 +1,40 @@ +package watch + +import ( + "strings" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// KindManifestRefused is a module whose manifest the indexer's judge refused (novox/hq ADR 0578 §1): every reader holds +// it, composing nothing of its own, until a mended manifest is merged. +const KindManifestRefused = "manifest-refused" + +// 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) { + w.mu.Lock() + defer w.mu.Unlock() + key := FindingKey(ScopeModule, module, KindManifestRefused) + if op != Put { + w.unfind(key) + return + } + m, err := delivery.DecodeModule(value) + if err != nil { + w.say("the indexer's value of %s cannot be read, so whether its manifest is refused is left as it was: %v", module, err) + return + } + v := m.VerdictOf(module) + if refused, _ := delivery.TakeManifestVerdict(v); !refused { + w.unfind(key) + return + } + w.find(Finding{Scope: ScopeModule, ID: module, Kind: KindManifestRefused, Severity: Warning, Source: delivery.ModulesBucket, + Summary: v.Why(), + Said: strings.Join(v.Problems, "; "), + Headline: "The manifest of " + module + " is refused", + Explanation: "The indexer's judge refused it, so every node, the grants part and the assigner hold " + module + ": " + + "nothing of it changes until a mended manifest is merged, and every other module goes on.", + Needs: "a pull request that mends the manifest of " + module}) +} diff --git a/modules/mesh-delivery-watchman/manifest_test.go b/modules/mesh-delivery-watchman/manifest_test.go index a57ddfe..afdf4bf 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-controller.lease", - "mesh-delivery-assigner.wants", "mesh-host.health", "mesh-provisioner.provisions", "node-engine.have"}) { + "mesh-delivery-assigner.wants", "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 fcc3077..9b1932d 100644 --- a/modules/mesh-delivery-watchman/module.json +++ b/modules/mesh-delivery-watchman/module.json @@ -21,6 +21,7 @@ "mesh-build-node.steps", "mesh-controller.lease", "mesh-delivery-assigner.wants", + "mesh-delivery-indexer.modules", "mesh-host.health", "mesh-provisioner.provisions", "node-engine.have"