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
This commit is contained in:
2026-11-09 14:01:41 +01:00
parent c2e7249897
commit 4d75646bf4
+126
View File
@@ -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 "<path> <function>", 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
}