From 4d75646bf4fd3af4fafdee150ab59f09eefecfbb Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 9 Nov 2026 13:51:44 +0100 Subject: [PATCH] Guard that kept and issued state is read only through mesh-sdk's two rules (issue 1966) A reader of a manifest's or a membership's State beside catalogue.KeptStates and Membership.StateIssued misses a key-value entry asked in requires, the kind of this issue's faults. The row reads the repository's syntax trees and allows only the rules that judge or compose the old section itself. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- modules/mesh-grants/grants_test.go | 126 +++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 9338a153..78b9c19a 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -9,6 +9,9 @@ import ( "fmt" "git.novox.be/novox/mesh-contracts/go/seats/meshbroker" stdio "git.novox.be/novox/mesh-sdk/go" + "go/ast" + "go/parser" + "go/token" "io/fs" "maps" "os" @@ -2870,6 +2873,68 @@ func TestGrants(t *testing.T) { } }) + // Issue 1966 (ADR 0595 §1, §11): the state a manifest keeps and a membership issues is read through mesh-sdk's two rules, + // catalogue.KeptStates and Membership.StateIssued or StateNames, which answer the old `state` section and a key-value + // entry asked in `requires` alike. A reader of a manifest's or a membership's `State` beside them misses the new form, + // the kind of this issue's faults; only the rules that judge or compose the old section itself read it. Read by the + // syntax tree across the repository, since the field's name alone is shared by other types. + t.Run("the state a manifest keeps or a membership issues is read through mesh-sdk's two rules", func(t *testing.T) { + judgesTheOldSection := map[string]bool{ + "modules/mesh-delivery-indexer/judge/busstate.go BusStateProblems": true, + "modules/mesh-delivery-indexer/judge/oldsections.go SectionsUsed": true, + "modules/mesh-delivery-indexer/judge/rules.go readOnlyStateProblems": true, + "modules/mesh-delivery-indexer/judge/rules.go parseManifest": true, + "modules/mesh-delivery-indexer/judge/rules.go StateProblems": true, + "modules/mesh-delivery-indexer/judge/rules.go ObjectsProblems": true, + "modules/mesh-grants/internal/grants/records.go bucketsOf": true, + } + planted := "package p\n\nimport \"git.novox.be/novox/mesh-sdk/go/catalogue\"\n\n" + + "func planted(m catalogue.Manifest) (n int) {\n\tfor range m.State {\n\t\tn++\n\t}\n\treturn n\n}\n" + if got, err := oldStateReaders("p.go", []byte(planted)); err != nil || !slices.Equal(got, []string{"p.go planted"}) { + t.Fatalf("a planted range m.State is read as %v (%v), want it found", got, err) + } + files, seen := 0, map[string]bool{} + err := filepath.WalkDir(filepath.Join("..", ".."), func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() && path != filepath.Join("..", "..") && (d.Name() == "testdata" || d.Name() == "node_modules" || (len(d.Name()) > 1 && d.Name()[0] == '.')) { + return filepath.SkipDir + } + if d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { + return nil + } + src, err := os.ReadFile(path) + if err != nil { + return err + } + files++ + rel, _ := filepath.Rel(filepath.Join("..", ".."), path) + readers, err := oldStateReaders(filepath.ToSlash(rel), src) + if err != nil { + return err + } + for _, r := range readers { + seen[r] = true + if !judgesTheOldSection[r] { + t.Errorf("%s reads a manifest's or a membership's State beside catalogue.KeptStates and Membership.StateIssued", r) + } + } + return nil + }) + if err != nil { + t.Fatal(err) + } + if files < 100 { + t.Fatalf("read %d source files, want the repository's", files) + } + for r := range judgesTheOldSection { + if !seen[r] { + t.Errorf("%s no longer reads the old section: take it off the list", r) + } + } + }) + // 774/S29/AC1 (ADR 0549 §5, ADR 0571 §10): the memberships composed from the assignments alone, with no read of the // controller, are the ones the union of assignments and assignments gave on the same input: the fixture's (two nodes, // the bus and mod-x on a, mod-y on b, as placed on the live mesh's own shape of nats beside its modules), whose @@ -4476,3 +4541,64 @@ func channelTakes(t *testing.T, say func(oc.Condition) error) func(oc.Condition) return say(c) } } + +// oldStateReaders names, as " ", every function of the source that reads the field State of a +// catalogue.Manifest or a membership.Membership: a receiver, parameter or declared variable of either type, or a field +// named Manifest or Membership (issue 1966's source guard). +func oldStateReaders(path string, src []byte) ([]string, error) { + f, err := parser.ParseFile(token.NewFileSet(), path, src, 0) + if err != nil { + return nil, err + } + typed := func(e ast.Expr) bool { + if s, ok := e.(*ast.StarExpr); ok { + e = s.X + } + sel, ok := e.(*ast.SelectorExpr) + if !ok { + return false + } + x, ok := sel.X.(*ast.Ident) + return ok && ((x.Name == "catalogue" && sel.Sel.Name == "Manifest") || (x.Name == "membership" && sel.Sel.Name == "Membership")) + } + var out []string + for _, decl := range f.Decls { + fn, ok := decl.(*ast.FuncDecl) + if !ok || fn.Body == nil { + continue + } + names := map[string]bool{} + var fields []*ast.Field + if fn.Recv != nil { + fields = append(fields, fn.Recv.List...) + } + for _, fl := range append(fields, fn.Type.Params.List...) { + if typed(fl.Type) { + for _, n := range fl.Names { + names[n.Name] = true + } + } + } + reads := false + ast.Inspect(fn.Body, func(n ast.Node) bool { + if vs, ok := n.(*ast.ValueSpec); ok && vs.Type != nil && typed(vs.Type) { + for _, n := range vs.Names { + names[n.Name] = true + } + } + if s, ok := n.(*ast.SelectorExpr); ok && s.Sel.Name == "State" { + switch x := s.X.(type) { + case *ast.Ident: + reads = reads || names[x.Name] + case *ast.SelectorExpr: + reads = reads || x.Sel.Name == "Manifest" || x.Sel.Name == "Membership" + } + } + return true + }) + if reads { + out = append(out, path+" "+fn.Name.Name) + } + } + return out, nil +}