From d543ff7126e6e2bbc917806c9d368ec2fc37c1a9 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 30 Oct 2026 18:15:17 +0100 Subject: [PATCH] Judge each read for its reader with mesh-sdk's ReadsAnnouncedBy, so the parts named for the lease are granted it, and say a refusal once (issue 774) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2536 reads of mesh-controller.lease were refused on 2026-10-30 (mesh-vault, mesh-grants, the assigner, artifact-store-tools, the watchman, mesh-config). The controller composed the bus's users with its own copy of a reader-less judgement (internal/broker ReadsAnnounced), which knows no named readers: the lease is announced readable by nobody and read by the parts named for it (ADR 0547 clause 9, ADR 0561 §3). The copy is deleted; BusRecords judges each read for its module with buscompose.ReadsAnnouncedBy. Each refusal was also printed on every composition, about 420 times a read; it is now said once per process. mesh-config's read of the lease stays refused: it reads the lease for its take-over (hand-back ends at the lease write, ADR 0561 §3) and is not among the named readers. That is mesh-sdk's NamedReaders, routed to the lead. Quick fix: mark the lease readable in the controller's announcement (tried and reverted: it would open the lease to every module and contradict ADR 0561 §3). Thorough fix: one judgement, the SDK's, for every composer. Chose thorough. Research 072 piece: 03-domains, mesh-grants (bus membership and grants leave the controller): the controller composes reads with the same SDK function the grants part uses. --- internal/broker/announced.go | 17 ---------- internal/broker/announced_test.go | 17 ---------- internal/inventory/busrecords.go | 44 ++++++++++++++++++++------ internal/inventory/judge_reads_test.go | 43 +++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 44 deletions(-) create mode 100644 internal/inventory/judge_reads_test.go diff --git a/internal/broker/announced.go b/internal/broker/announced.go index 1c8304a9..d4bc23ee 100644 --- a/internal/broker/announced.go +++ b/internal/broker/announced.go @@ -41,23 +41,6 @@ func ControllerAnnouncement(node string) announce.States { return s } -// ReadsAnnounced splits a reader's `reads` (`.`) by what each owner announced: granted when the owner -// announced the state readable, or announced nothing yet (the transition); refused when the owner announced and did -// not announce that state readable. -func ReadsAnnounced(reads []string, announced map[string]announce.States) (granted, refused []string) { - for _, r := range reads { - owner, name, ok := strings.Cut(r, ".") - s, said := announced[owner] - switch { - case !ok || !said || s.Readable(name): - granted = append(granted, r) - default: - refused = append(refused, r) - } - } - return granted, refused -} - // NotAnnounced are the owners some module reads the state of and that have announced nothing yet, sorted: read from the // readers' `reads` as before, until they announce. func NotAnnounced(r Records) []string { diff --git a/internal/broker/announced_test.go b/internal/broker/announced_test.go index ccb4fdb0..dc0a0c47 100644 --- a/internal/broker/announced_test.go +++ b/internal/broker/announced_test.go @@ -7,23 +7,6 @@ import ( "git.novox.be/novox/mesh-sdk/go/announce" ) -// D3 (ADR 0495 clause 3, ADR 0513 §3; issue 1790): a reader's `reads` is granted where the owner announced the state -// readable, refused where it announced and did not, and granted as before where the owner announced nothing yet. -func TestAReadIsGrantedFromItsOwnersAnnouncement(t *testing.T) { - announced := map[string]announce.States{ - "owner": {Module: "owner", States: []announce.State{{Name: "open", Readable: true}, {Name: "closed"}}}, - } - granted, refused := ReadsAnnounced([]string{"owner.open", "owner.closed", "owner.unsaid", "quiet.any"}, announced) - if !slices.Equal(granted, []string{"owner.open", "quiet.any"}) || !slices.Equal(refused, []string{"owner.closed", "owner.unsaid"}) { - t.Fatalf("granted %v, refused %v", granted, refused) - } - r := Records{Announced: announced, Assigned: map[string][]Declared{"one": {{Module: "reader", Reads: granted, - ReadsRefused: refused}}}} - if got := NotAnnounced(r); !slices.Equal(got, []string{"quiet"}) { - t.Fatalf("not yet announced: %v", got) - } -} - // D1 (ADR 0495 clause 2): the controller announces every bucket of its own, and readable exactly the record mirrors (the // declared packages the pacman module reads among them, ADR 0507 §5), the chain's buckets, the checks running and the // plans building. diff --git a/internal/inventory/busrecords.go b/internal/inventory/busrecords.go index 9ca930fb..e79ab5c7 100644 --- a/internal/inventory/busrecords.go +++ b/internal/inventory/busrecords.go @@ -6,7 +6,9 @@ import ( "git.novox.be/novox/mesh-sdk/go/announce" "slices" "strings" + "sync" + "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" ) @@ -119,22 +121,18 @@ func (i *Inventory) BusRecords(ctx context.Context) (broker.Records, error) { // announcement does not make readable is granted nothing, and said; an owner that announced nothing yet is read as // before. Announcements that cannot be read are said, and every read is granted as before: the bus's users are // composed either way. + // + // **Judged for its reader, by the SDK's judgement** (sweep 2026-10-30, issue 774): mesh-sdk buscompose's + // ReadsAnnouncedBy knows the named readers (ADR 0547 clause 9, ADR 0561 §3), so the lease is granted to the parts + // named for it and refused to every other. The controller's own reader-less copy refused every named reader: 2536 + // refused reads of mesh-controller.lease on 2026-10-30, each said on every composition. if Announcements != nil { announced, err := Announcements(ctx) if err != nil { fmt.Printf("the states the modules announced cannot be read, so every read is granted from `reads` as before: %v\n", err) } else { out.Announced = announced - for node, ds := range out.Assigned { - for k := range ds { - granted, refused := broker.ReadsAnnounced(ds[k].Reads, announced) - ds[k].Reads, ds[k].ReadsRefused = granted, refused - for _, r := range refused { - fmt.Printf("%s on %s reads %s, which its owner does not announce readable: not granted\n", - ds[k].Module, node, r) - } - } - } + judgeReads(out.Assigned, announced, sayRefusedOnce) } } @@ -615,3 +613,29 @@ func hearsOf(m catalogue.Manifest, declared map[string]catalogue.Manifest, node } return out, touches, onEvents } + +// refusalsSaid are the refused reads already said by this process, by `@ `: a refusal is said once, +// when first composed, not on every composition (about 420 times a read on 2026-10-30). `status` names them all. +var refusalsSaid sync.Map + +// sayRefusedOnce says a refused read the first time this process composes it. +func sayRefusedOnce(module, node, read string) { + if _, said := refusalsSaid.LoadOrStore(module+"@"+node+" "+read, true); said { + return + } + fmt.Printf("%s on %s reads %s, which its owner does not make readable by it: not granted\n", module, node, read) +} + +// judgeReads splits each assigned module's reads into granted and refused, for that module, by what the owners announced +// and the states' named readers (buscompose.ReadsAnnouncedBy), and says each refusal. +func judgeReads(assigned map[string][]broker.Declared, announced map[string]announce.States, say func(module, node, read string)) { + for node, ds := range assigned { + for k := range ds { + granted, refused := buscompose.ReadsAnnouncedBy(ds[k].Module, ds[k].Reads, announced) + ds[k].Reads, ds[k].ReadsRefused = granted, refused + for _, r := range refused { + say(ds[k].Module, node, r) + } + } + } +} diff --git a/internal/inventory/judge_reads_test.go b/internal/inventory/judge_reads_test.go new file mode 100644 index 00000000..7a1b0044 --- /dev/null +++ b/internal/inventory/judge_reads_test.go @@ -0,0 +1,43 @@ +package inventory + +import ( + "slices" + "testing" + + "git.novox.be/novox/mesh-sdk/go/announce" + "github.com/novox/mesh-controller/internal/broker" +) + +// Sweep 2026-10-30 (issue 774): a read is judged for its reader. The controller's lease, which its announcement marks +// readable by nobody, is granted to the parts named for it (ADR 0561 §3) and refused to every other; a state its owner +// announced readable is granted; one it keeps and did not is refused; an owner that announced nothing is read as before. +// Each refusal is said, and a refusal already said is not said again. +func TestAReadIsJudgedForItsReader(t *testing.T) { + announced := map[string]announce.States{ + "mesh-controller": broker.ControllerAnnouncement("novox"), + "owner": {Module: "owner", States: []announce.State{{Name: "open", Readable: true}, {Name: "closed"}}}, + } + for name, c := range map[string]struct { + reader, read string + granted bool + }{ + "the lease, by the vault": {"mesh-vault", "mesh-controller.lease", true}, + "the lease, by the grants part": {"mesh-grants", "mesh-controller.lease", true}, + "the lease, by a module not named": {"mesh-config", "mesh-controller.lease", false}, + "a readable state": {"reader", "owner.open", true}, + "a state kept and not readable": {"reader", "owner.closed", false}, + "an owner that announced nothing": {"reader", "quiet.any", true}, + "a mirror the controller marks open": {"pacman", "mesh-controller.declared-packages", true}, + } { + assigned := map[string][]broker.Declared{"novox": {{Module: c.reader, Reads: []string{c.read}}}} + var said []string + judgeReads(assigned, announced, func(module, node, read string) { said = append(said, read) }) + d := assigned["novox"][0] + if slices.Contains(d.Reads, c.read) != c.granted || slices.Contains(d.ReadsRefused, c.read) == c.granted { + t.Errorf("%s: granted %v, refused %v", name, d.Reads, d.ReadsRefused) + } + if (len(said) == 1) == c.granted { + t.Errorf("%s: said %v", name, said) + } + } +}