From 0bfc72d2307f6f64ed2abe2e3298a10d82727b51 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 11:21:51 +0100 Subject: [PATCH] Make the controller checks strict: nothing names the placed-nowhere controller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit controllerdeps refuses any manifest naming the controller's seat, states or buckets, the lease and the entries once kept to its write among them, and any module file naming one of its buckets (ADR 0590 §5). The words check refuses a controller address in any text a module shows. The handacts check and noplacement's list of assignments readers are folded into it (issue 1126 B10). Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- checks/controllerdeps/controllerdeps.go | 133 +++++++++++++++--- checks/controllerdeps/controllerdeps_test.go | 49 ++++--- checks/controllerdeps/go.mod | 13 +- checks/controllerdeps/go.sum | 16 --- checks/controllerdeps/reads-outside | 2 +- checks/handacts/fallbacks | 5 - checks/handacts/go.mod | 3 - checks/handacts/main.go | 109 -------------- checks/handacts/main_test.go | 59 -------- checks/handacts/reads-outside | 3 - checks/noplacement/assignments-readers | 4 - checks/noplacement/go.mod | 4 - checks/noplacement/main.go | 73 +--------- checks/noplacement/main_test.go | 33 ----- checks/words/main.go | 38 ++++- checks/words/main_test.go | 22 +++ modules/mesh-hand-acts/README.md | 4 +- .../mesh-hand-acts/cmd/mesh-hand-acts/acts.go | 4 +- .../mesh-hand-acts/cmd/mesh-hand-acts/copy.go | 2 +- .../cmd/mesh-hand-acts/tools.go | 2 +- modules/nats/cmd/nats-tools/expiry.go | 4 +- 21 files changed, 215 insertions(+), 367 deletions(-) delete mode 100644 checks/controllerdeps/go.sum delete mode 100644 checks/handacts/fallbacks delete mode 100644 checks/handacts/go.mod delete mode 100644 checks/handacts/main.go delete mode 100644 checks/handacts/main_test.go delete mode 100644 checks/handacts/reads-outside delete mode 100644 checks/noplacement/assignments-readers diff --git a/checks/controllerdeps/controllerdeps.go b/checks/controllerdeps/controllerdeps.go index 9c9177f3..0d784fb0 100644 --- a/checks/controllerdeps/controllerdeps.go +++ b/checks/controllerdeps/controllerdeps.go @@ -1,39 +1,138 @@ -// Package controllerdeps is the catalogue's side of the lease write's manifest test (novox/hq ADR 0571 §13, ADR 0582 -// clause 2, issue 774): no module of this tree reads, invokes, consumes, waits on or uses the controller beyond what the -// named list of mesh-sdk's catalogue.KeepList keeps to the write itself, and the lease key, which names the lease's holder -// whoever that is. It decodes every manifest under root/modules and judges them with mesh-sdk's ManifestRefusals, the -// judge the grants part's grants_lease_check and the other two repositories' tests run, so the rule is said once. +// Package controllerdeps is the catalogue's dependence check on the controller (novox/hq ADR 0590 §5, issue 1126): the +// controller is not placed, so no module of this tree reads, requires, invokes, consumes, waits on, uses or otherwise +// names it. Nothing is kept: not the lease, not the entries once kept to its write (ADR 0571 §13, superseded with ADR +// 0582). A later decision that places a controller again must first lift this check. +// +// It refuses, over the tree at root: +// +// - a directory modules/mesh-controller: the controller is no module of the catalogue; +// - a manifest string naming the controller's seat, a state, verb or event of it, or one of its buckets +// (`mesh-controller.`, `seat:mesh-controller.`, `mesh-controller_`), anywhere in the manifest; +// - a manifest string that is the bare module name `mesh-controller` outside the module's own `config`, where a value +// is the module's data (an issue owner naming the controller's repository is no dependency), not an address; +// - a line of any module's file naming one of the controller's buckets (`mesh-controller_`): its buckets are removed +// after the controller's end (ADR 0590 §6), so a reader of one reads nothing. A test (`_test.go`, `.test.ts`) reads +// no live bucket, its names are its own data, and a `testdata` directory is a copy of text from elsewhere (a design, +// a backlog): both are skipped. +// +// The rule is said once, here, for the catalogue: decoding stays the manifests check's (a manifest that is no JSON is +// an error here, never judged). package controllerdeps import ( + "bufio" + "encoding/json" "fmt" + "io/fs" "os" "path/filepath" + "regexp" "sort" - - "git.novox.be/novox/mesh-sdk/go/catalogue" + "strings" ) -// Refusals is each reason the lease write's check gives over the manifests under root/modules in one phase -// (catalogue.PhaseBefore or catalogue.PhaseWrite), none when it passes. A module.json that does not decode is an error: -// the manifests check says which, and nothing here is judged on a tree it cannot read. -func Refusals(root, phase string) ([]string, error) { +// Module is the controller's module name, and Bucket the prefix every bucket of its carries. +const ( + Module = "mesh-controller" + Bucket = Module + "_" +) + +// address is a string naming the controller's seat, one of its states, verbs or events, or one of its buckets. +var address = regexp.MustCompile(`(^|[^A-Za-z0-9_-])(seat:)?mesh-controller([._][A-Za-z0-9])`) + +// Refusals is each reason the check gives over the tree at root, sorted; none when it passes. +func Refusals(root string) ([]string, error) { + var out []string + if _, err := os.Lstat(filepath.Join(root, "modules", Module)); err == nil { + out = append(out, Module+": the controller is no module of the catalogue: it is not placed and nothing may depend on it (novox/hq ADR 0590 §5)") + } files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json")) if err != nil { return nil, err } - sort.Strings(files) - manifests := map[string]catalogue.Manifest{} for _, f := range files { raw, err := os.ReadFile(f) if err != nil { return nil, err } - m, err := catalogue.Decode(raw) - if err != nil { + var doc any + if err := json.Unmarshal(raw, &doc); err != nil { return nil, fmt.Errorf("%s: %w", f, err) } - manifests[m.Module] = m + module := filepath.Base(filepath.Dir(f)) + for _, s := range names(doc, false, nil) { + out = append(out, fmt.Sprintf("%s: names %q: the controller is not placed, so no manifest reads, requires, invokes, "+ + "consumes, waits on or uses it; name the seat that holds the job now (novox/hq ADR 0590 §5)", module, s)) + } } - return catalogue.ManifestRefusals(manifests, phase), nil + buckets, err := bucketLines(root) + if err != nil { + return nil, err + } + out = append(out, buckets...) + sort.Strings(out) + return out, nil +} + +// names collects the strings of a decoded manifest that name the controller; inConfig is under the module's `config`. +func names(v any, inConfig bool, into []string) []string { + switch v := v.(type) { + case string: + if address.MatchString(v) || (!inConfig && strings.TrimSpace(v) == Module) { + into = append(into, v) + } + case []any: + for _, e := range v { + into = names(e, inConfig, into) + } + case map[string]any: + keys := make([]string, 0, len(v)) + for k := range v { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + into = names(v[k], inConfig || k == "config", into) + } + } + return into +} + +// bucketLines is every line of a module's file, outside tests and testdata, that names one of the controller's buckets. +func bucketLines(root string) ([]string, error) { + var out []string + modules := filepath.Join(root, "modules") + err := filepath.WalkDir(modules, func(path string, d fs.DirEntry, err error) error { + if err != nil { + if os.IsNotExist(err) && path == modules { + return nil + } + return err + } + if d.IsDir() { + if n := d.Name(); n == "testdata" || n == "node_modules" || n == ".git" { + return filepath.SkipDir + } + return nil + } + if !d.Type().IsRegular() || strings.HasSuffix(d.Name(), "_test.go") || strings.HasSuffix(d.Name(), ".test.ts") { + return nil + } + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + sc := bufio.NewScanner(f) + sc.Buffer(make([]byte, 0, 64*1024), 16*1024*1024) + rel, _ := filepath.Rel(root, path) + for n := 1; sc.Scan(); n++ { + if strings.Contains(sc.Text(), Bucket) { + out = append(out, fmt.Sprintf("%s:%d: names the controller's bucket (%s…): its buckets are removed after its "+ + "end (novox/hq ADR 0590 §6); read the seat that holds the job now", filepath.ToSlash(rel), n, Bucket)) + } + } + return sc.Err() + }) + return out, err } diff --git a/checks/controllerdeps/controllerdeps_test.go b/checks/controllerdeps/controllerdeps_test.go index 5f5cd28e..9a2e221a 100644 --- a/checks/controllerdeps/controllerdeps_test.go +++ b/checks/controllerdeps/controllerdeps_test.go @@ -5,46 +5,51 @@ import ( "path/filepath" "strings" "testing" - - "git.novox.be/novox/mesh-sdk/go/catalogue" ) -// 774/S13/AC1: every manifest of the catalogue at this head, at the phase of the lease write's own delivery, reads, -// invokes and consumes nothing of the controller beyond the lease and the entries kept to the write. -func TestNoManifestDependsOnTheControllerBeyondTheLeaseAndTheEntriesKeptToTheWrite(t *testing.T) { - got, err := Refusals("../..", catalogue.PhaseWrite) +// ADR 0590 §5 (issue 1126 B10): no manifest of the catalogue at this head reads, invokes, consumes, waits on, uses or +// otherwise names the controller's seat, states or buckets, and no module's code names a bucket of the controller's. +func TestNothingInTheCatalogueNamesTheController(t *testing.T) { + got, err := Refusals("../..") if err != nil { t.Fatal(err) } - if len(got) > 0 { - t.Errorf("the catalogue still declares on the controller (novox/hq ADR 0571 §13):\n%s", strings.Join(got, "\n")) + for _, r := range got { + t.Error(r) } } -// The check refuses what it must and lets the lease and the kept entries through, on a tree of its own. -func TestRefusalsJudgeATreeByPhase(t *testing.T) { +// The check refuses every name of the controller, the lease and the entries once kept to its write among them, on a +// tree of its own; a repository named mesh-controller (an issue's owner) and a word in prose are no dependency. +func TestRefusalsJudgeATree(t *testing.T) { for _, row := range []struct { - name, phase string - manifests map[string]string - want string // empty: passes + name string + files map[string]string + want string // empty: passes }{ - {"the lease is no dependency", catalogue.PhaseWrite, map[string]string{"a": `{"module":"a","reads":["mesh-controller.lease"]}`}, ""}, - {"a read of the controller's states", catalogue.PhaseWrite, map[string]string{"a": `{"module":"a","reads":["mesh-controller.conditions"]}`}, "a declares read mesh-controller.conditions"}, - {"a kept entry passes before the delivery", catalogue.PhaseBefore, map[string]string{"artifact-store-tools": `{"module":"artifact-store-tools","invokes":["seat:mesh-controller.collect"]}`}, ""}, - {"a kept-to-the-write entry passes at the write", catalogue.PhaseWrite, map[string]string{"artifact-store-tools": `{"module":"artifact-store-tools","invokes":["seat:mesh-controller.collect"]}`}, ""}, + {"the lease is refused like any read", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-controller.lease"]}`}, `a: names "mesh-controller.lease"`}, + {"a verb once kept to the write", map[string]string{"modules/b/module.json": `{"module":"b","invokes":["seat:mesh-controller.collect"]}`}, `b: names "seat:mesh-controller.collect"`}, + {"an event consumed", map[string]string{"modules/c/module.json": `{"module":"c","consumes":[{"event":"mesh-controller.artifacts-gone"}]}`}, "mesh-controller.artifacts-gone"}, + {"a requirement", map[string]string{"modules/d/module.json": `{"module":"d","requires":[{"name":"x","from":"mesh-controller"}]}`}, `d: names "mesh-controller"`}, + {"the controller's own module", map[string]string{"modules/mesh-controller/module.json": `{"module":"mesh-controller"}`}, "mesh-controller: the controller is no module of the catalogue"}, + {"a bucket in code", map[string]string{"modules/e/cmd/e/main.go": "package main\nconst b = \"mesh-controller_assignments\"\n"}, "modules/e/cmd/e/main.go:2: names the controller's bucket"}, + {"a bucket in a test is its data", map[string]string{"modules/e/cmd/e/main_test.go": "package main\n// mesh-controller_lease\n"}, ""}, + {"a fixture copied from elsewhere", map[string]string{"modules/e/cmd/e/testdata/x.md": "mesh-controller_rollouts\n"}, ""}, + {"an owner and prose pass", map[string]string{"modules/f/module.json": `{"module":"f","config":{"owners":{"default":["mesh-controller","mesh-controller modules/route-proxy"]}},"why":"the mesh-controller module is gone"}`}, ""}, + {"the seat's successors pass", map[string]string{"modules/g/module.json": `{"module":"g","reads":["mesh-placement.placement"],"invokes":["seat:mesh-hand-acts.hand-act"]}`}, ""}, } { t.Run(row.name, func(t *testing.T) { root := t.TempDir() - for module, text := range row.manifests { - dir := filepath.Join(root, "modules", module) - if err := os.MkdirAll(dir, 0o755); err != nil { + for path, text := range row.files { + p := filepath.Join(root, path) + if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(text), 0o644); err != nil { + if err := os.WriteFile(p, []byte(text), 0o644); err != nil { t.Fatal(err) } } - got, err := Refusals(root, row.phase) + got, err := Refusals(root) if err != nil { t.Fatal(err) } diff --git a/checks/controllerdeps/go.mod b/checks/controllerdeps/go.mod index b4782dc7..4c0306fa 100644 --- a/checks/controllerdeps/go.mod +++ b/checks/controllerdeps/go.mod @@ -1,14 +1,3 @@ module checks/controllerdeps -go 1.26.0 - -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101235037-c21d8fd9692c - -require ( - github.com/klauspost/compress v1.20.0 // indirect - github.com/nats-io/nats.go v1.54.0 // indirect - github.com/nats-io/nkeys v0.4.16 // indirect - github.com/nats-io/nuid v1.0.1 // indirect - golang.org/x/crypto v0.57.0 // indirect - golang.org/x/sys v0.48.0 // indirect -) +go 1.22 diff --git a/checks/controllerdeps/go.sum b/checks/controllerdeps/go.sum deleted file mode 100644 index 733cedc9..00000000 --- a/checks/controllerdeps/go.sum +++ /dev/null @@ -1,16 +0,0 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915 h1:x8k7IeMirMTxUo8BH+xUO+MDnHYgU+636jl1NIw1wvs= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101235037-c21d8fd9692c h1:/YCxh+NYfqry3PxZYY1nQXaccupobxpsFt8c9BYuGBg= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101235037-c21d8fd9692c/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/checks/controllerdeps/reads-outside b/checks/controllerdeps/reads-outside index efc1a11c..894424ad 100644 --- a/checks/controllerdeps/reads-outside +++ b/checks/controllerdeps/reads-outside @@ -1,3 +1,3 @@ # What these tests read outside their package, as paths from the repository's root (issue 1936). -# every module's directory (its manifest), decoded and judged for what it still declares on the controller. +# every module's directory: its manifest, for a name of the controller, and its files, for one of the controller's buckets. modules diff --git a/checks/handacts/fallbacks b/checks/handacts/fallbacks deleted file mode 100644 index 238233b7..00000000 --- a/checks/handacts/fallbacks +++ /dev/null @@ -1,5 +0,0 @@ -# The calls of the controller's hand-act log a manifest of this catalogue may still invoke, each ` `: -# the fallback of a recorder (mesh-sdk handacts.Recorder) while the seat mesh-hand-acts has no holder that answers, until -# the controller's lease is written to a holder that is no controller. The check of this directory (handacts) refuses any -# other, and an entry whose module no longer invokes it: the list only shrinks, and it is empty before the lease write -# (novox/hq ADR 0565 §11; issue 1953, issue 774). It is empty: no manifest invokes the controller's log (issue 1953, 1955). diff --git a/checks/handacts/go.mod b/checks/handacts/go.mod deleted file mode 100644 index 29e4c52b..00000000 --- a/checks/handacts/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-catalog/checks/handacts - -go 1.22 diff --git a/checks/handacts/main.go b/checks/handacts/main.go deleted file mode 100644 index 1a222d60..00000000 --- a/checks/handacts/main.go +++ /dev/null @@ -1,109 +0,0 @@ -// Command handacts refuses a manifest that records into the controller's hand-act log (novox/hq ADR 0565 §11): every -// recorder records through the seat mesh-hand-acts from its delivery, so no manifest invokes -// `seat:mesh-controller.hand-act`, `.warranted` or `.drill`. The recorders' fallbacks to the controller's log while the -// seat has no holder are named in checks/handacts/fallbacks, each module and verb with why; that list only shrinks, and -// an entry whose module no longer invokes the verb is refused too. -// -// go run . -// -// Exits 1 on a finding. -package main - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - "sort" - "strings" -) - -// Verbs are the controller's hand-act verbs, as a manifest invokes them, and Allowed the list of named fallbacks. -var Verbs = []string{"hand-act", "warranted", "drill"} - -const ( - invokePrefix = "seat:mesh-controller." - Allowed = "checks/handacts/fallbacks" -) - -// Findings is every refusal in the catalog at root, sorted. -func Findings(root string) ([]string, error) { - allowed := map[string]bool{} - raw, err := os.ReadFile(filepath.Join(root, Allowed)) - if err != nil && !os.IsNotExist(err) { - return nil, err - } - for _, line := range strings.Split(string(raw), "\n") { - if f := strings.Fields(line); len(f) >= 3 && !strings.HasPrefix(f[0], "#") { - allowed[f[0]+" "+f[1]] = true - } else if len(f) > 0 && !strings.HasPrefix(f[0], "#") { - return nil, fmt.Errorf("%s: %q is not ` `", Allowed, line) - } - } - manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json")) - if err != nil { - return nil, err - } - var out []string - invoked := map[string]bool{} - for _, path := range manifests { - raw, err := os.ReadFile(path) - if err != nil { - return nil, err - } - var m struct { - Module string `json:"module"` - Invokes []string `json:"invokes"` - } - if json.Unmarshal(raw, &m) != nil || m.Module == "mesh-controller" { - continue - } - for _, inv := range m.Invokes { - verb, ok := strings.CutPrefix(strings.TrimSpace(inv), invokePrefix) - if !ok || !contains(Verbs, verb) { - continue - } - invoked[m.Module+" "+verb] = true - if !allowed[m.Module+" "+verb] { - out = append(out, fmt.Sprintf("%s invokes %s%s: record through seat:mesh-hand-acts.%s (novox/hq ADR 0565 §11), "+ - "or name the fallback in %s", m.Module, invokePrefix, verb, verb, Allowed)) - } - } - } - for entry := range allowed { - if !invoked[entry] { - out = append(out, fmt.Sprintf("%s names %q, which is no longer invoked: take it off the list, which only shrinks", - Allowed, entry)) - } - } - sort.Strings(out) - return out, nil -} - -func contains(list []string, s string) bool { - for _, x := range list { - if x == s { - return true - } - } - return false -} - -func main() { - if len(os.Args) != 2 { - fmt.Fprintln(os.Stderr, "usage: handacts ") - os.Exit(2) - } - found, err := Findings(os.Args[1]) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - for _, f := range found { - fmt.Println(f) - } - if len(found) > 0 { - os.Exit(1) - } - fmt.Println("no manifest records into the controller's hand-act log but the fallbacks named") -} diff --git a/checks/handacts/main_test.go b/checks/handacts/main_test.go deleted file mode 100644 index 61e197e6..00000000 --- a/checks/handacts/main_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - "strings" - "testing" -) - -// No manifest invokes the controller's hand-act verbs but a named fallback; a named fallback no longer invoked is -// refused (novox/hq ADR 0565 §11's catalog test). -func TestFindings(t *testing.T) { - for _, row := range []struct { - name string - files map[string]string - want string // empty: clean - }{ - {"clean", map[string]string{"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-hand-acts.hand-act"]}`}, ""}, - {"an unnamed call", map[string]string{"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-controller.warranted"]}`}, - "a invokes seat:mesh-controller.warranted"}, - {"a named fallback, and the controller's own", map[string]string{ - "modules/a/module.json": `{"module":"a","invokes":["seat:mesh-controller.drill"]}`, - "modules/mesh-controller/module.json": `{"module":"mesh-controller","invokes":["seat:mesh-controller.hand-act"]}`, - Allowed: "# why\na drill fallback until the lease write\n"}, ""}, - {"a fallback no longer invoked", map[string]string{Allowed: "gone hand-act fallback\n"}, `names "gone hand-act"`}, - {"another verb of the controller", map[string]string{"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-controller.conditions"]}`}, ""}, - } { - t.Run(row.name, func(t *testing.T) { - root := t.TempDir() - for path, text := range row.files { - p := filepath.Join(root, path) - if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(p, []byte(text), 0o644); err != nil { - t.Fatal(err) - } - } - got, err := Findings(root) - if err != nil { - t.Fatal(err) - } - if all := strings.Join(got, "\n"); (row.want == "") != (all == "") || !strings.Contains(all, row.want) { - t.Fatalf("findings = %q, want one containing %q", all, row.want) - } - }) - } -} - -// The run of this check over the real tree. -func TestTheCatalogRecordsOnlyThroughTheSeat(t *testing.T) { - got, err := Findings(filepath.Join("..", "..")) - if err != nil { - t.Fatal(err) - } - for _, f := range got { - t.Error(f) - } -} diff --git a/checks/handacts/reads-outside b/checks/handacts/reads-outside deleted file mode 100644 index 61f9814a..00000000 --- a/checks/handacts/reads-outside +++ /dev/null @@ -1,3 +0,0 @@ -# What these tests read outside their package, as paths from the repository's root (issue 1936). -# every manifest, for what it invokes. -modules/*/module.json diff --git a/checks/noplacement/assignments-readers b/checks/noplacement/assignments-readers deleted file mode 100644 index 64985d46..00000000 --- a/checks/noplacement/assignments-readers +++ /dev/null @@ -1,4 +0,0 @@ -# The modules of this catalogue that still read the controller's assignments (mesh-controller.assignments), each with -# why. The check of this directory (noplacement) refuses any other module that reads them, and an entry here whose module no longer -# does: the list only shrinks, and it is empty before the controller's lease is written to a holder that is no -# controller (novox/hq ADR 0549 §3, §6; issue 774). diff --git a/checks/noplacement/go.mod b/checks/noplacement/go.mod index 782a236b..a09ac5ec 100644 --- a/checks/noplacement/go.mod +++ b/checks/noplacement/go.mod @@ -1,7 +1,3 @@ module git.novox.be/novox/mesh-catalog/checks/noplacement go 1.22 - -require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0 - -replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk diff --git a/checks/noplacement/main.go b/checks/noplacement/main.go index cd145e32..1bcce199 100644 --- a/checks/noplacement/main.go +++ b/checks/noplacement/main.go @@ -10,11 +10,8 @@ // 3. a manifest naming a state, stream, event or bucket of the indexer's placement (`mesh-delivery-indexer.placement…` // or `mesh-delivery-indexer_placement…`) in any string value, such as `reads`; // -// 4. a manifest, other than mesh-controller's, that reads the controller's assignments (`mesh-controller.assignments`): -// every reader takes placement from the seat mesh-placement (novox/hq ADR 0549 §3, ADR 0567), and the lease write is -// refused while one still reads them (ADR 0549 §6; issue 774). The modules that still read them for what the -// placement does not carry are named in checks/noplacement/assignments-readers, each with why; that list only -// shrinks: an entry whose module no longer reads them is refused too. +// The controller's assignments, which item 4 once listed readers of, are refused with every other name of the +// controller by checks/controllerdeps (novox/hq ADR 0590 §5). // // go run . // @@ -61,74 +58,10 @@ func Findings(root string) ([]string, error) { out = append(out, fmt.Sprintf("%s: names %q, which the indexer no longer keeps; placement is the seat mesh-placement's (novox/hq ADR 0567 §12)", module, s)) } } - readers, err := assignmentsReaders(root) - if err != nil { - return nil, err - } - out = append(out, readers...) sort.Strings(out) return out, nil } -// AssignmentsRead is the controller's assignments as a manifest reads them, and Allowed the file, relative to the -// catalogue's root, that names the modules still reading them: ` ` per line, `#` a comment. -const ( - AssignmentsRead = "mesh-controller.assignments" - Allowed = "checks/noplacement/assignments-readers" -) - -// assignmentsReaders is every finding on the reads of the controller's assignments: a module other than the controller -// reading them that Allowed does not name, and a module Allowed names that no longer reads them (issue 774). -func assignmentsReaders(root string) ([]string, error) { - allowed := map[string]bool{} - raw, err := os.ReadFile(filepath.Join(root, Allowed)) - if err != nil && !os.IsNotExist(err) { - return nil, err - } - for _, line := range strings.Split(string(raw), "\n") { - if f := strings.Fields(line); len(f) > 0 && !strings.HasPrefix(f[0], "#") { - allowed[f[0]] = true - } - } - manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json")) - if err != nil { - return nil, err - } - var found []string - reads := map[string]bool{} - for _, path := range manifests { - raw, err := os.ReadFile(path) - if err != nil { - return nil, err - } - var m struct { - Module string `json:"module"` - Reads []string `json:"reads"` - } - if json.Unmarshal(raw, &m) != nil || m.Module == "mesh-controller" { - continue // a manifest that does not read is the manifest checks' to refuse - } - for _, r := range m.Reads { - if strings.TrimSpace(r) != AssignmentsRead { - continue - } - reads[m.Module] = true - if !allowed[m.Module] { - found = append(found, fmt.Sprintf("%s reads %s: read where modules run from mesh-delivery-assigner.placement, "+ - "the seat mesh-placement's (novox/hq ADR 0549 §3, ADR 0567); the lease write is refused while a module reads "+ - "the assignments (ADR 0549 §6)", m.Module, AssignmentsRead)) - } - } - } - for module := range allowed { - if !reads[module] { - found = append(found, fmt.Sprintf("%s names %s, which no longer reads %s: take it off the list, which only shrinks", - Allowed, module, AssignmentsRead)) - } - } - return found, nil -} - // named collects the strings of a decoded manifest that start with one of the Prefixes. func named(v any, into []string) []string { switch v := v.(type) { @@ -166,5 +99,5 @@ func main() { if len(found) > 0 { os.Exit(1) } - fmt.Println("no placement directory, no manifest reads the indexer's placement, and only the modules listed read the controller's assignments") + fmt.Println("no placement directory, and no manifest reads the indexer's placement") } diff --git a/checks/noplacement/main_test.go b/checks/noplacement/main_test.go index efa884dc..0db98f06 100644 --- a/checks/noplacement/main_test.go +++ b/checks/noplacement/main_test.go @@ -5,8 +5,6 @@ import ( "path/filepath" "strings" "testing" - - "git.novox.be/novox/mesh-catalog/checks/trunk" ) func TestFindingsRefuseWhatThePlacementPathLeft(t *testing.T) { @@ -21,12 +19,6 @@ func TestFindingsRefuseWhatThePlacementPathLeft(t *testing.T) { {"a read of the indexer's placement", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-delivery-indexer.placement"]}`}, `a: names "mesh-delivery-indexer.placement"`}, {"a read of its candidates", map[string]string{"modules/b/module.json": `{"module":"b","reads":["mesh-delivery-indexer.placement-candidates"]}`}, "placement-candidates"}, {"its event", map[string]string{"modules/c/module.json": `{"module":"c","on":[{"event":"mesh-delivery-indexer.placement-changed"}]}`}, "placement-changed"}, - // Issue 774 (ADR 0549 §3, §6): a reader of the controller's assignments is refused unless the list names it, the - // controller's own manifest is not judged, and a name on the list whose module no longer reads them is refused. - {"an unlisted reader of the assignments", map[string]string{"modules/r/module.json": `{"module":"r","reads":["mesh-controller.assignments"]}`}, "r reads mesh-controller.assignments"}, - {"a listed reader and the controller", map[string]string{"modules/l/module.json": `{"module":"l","reads":["mesh-controller.assignments"]}`, - "modules/mesh-controller/module.json": `{"module":"mesh-controller","reads":["mesh-controller.assignments"]}`, Allowed: "# why\nl its plan\n"}, ""}, - {"a listed module that reads them no more", map[string]string{Allowed: "gone no longer reads\n"}, "names gone, which no longer reads"}, {"the seat's own state is allowed", map[string]string{"modules/d/module.json": `{"module":"d","reads":["mesh-placement.placement"]}`}, ""}, } { t.Run(row.name, func(t *testing.T) { @@ -60,29 +52,4 @@ func TestTheCatalogueHoldsNoPlacementOfTheRepositoryPath(t *testing.T) { for _, f := range got { t.Error(f) } - // Issue 774 (ADR 0549 §3, §6; the comparison of novox/hq ADR 0529 clause 2): the list of modules still reading the - // controller's assignments gains none the trunk's copy at the merge base does not hold. Its first copy gains nothing. - path, ok := trunk.Copy(t, Allowed) - if !ok { - return - } - names := func(path string) map[string]bool { - raw, err := os.ReadFile(path) - if err != nil { - t.Fatal(err) - } - out := map[string]bool{} - for _, line := range strings.Split(string(raw), "\n") { - if f := strings.Fields(line); len(f) > 0 && !strings.HasPrefix(f[0], "#") { - out[f[0]] = true - } - } - return out - } - base := names(path) - for module := range names(filepath.Join("..", "..", Allowed)) { - if !base[module] { - t.Errorf("%s gains %s, which the trunk's copy does not hold: the list only shrinks", Allowed, module) - } - } } diff --git a/checks/words/main.go b/checks/words/main.go index 8f9b8893..0bab4308 100644 --- a/checks/words/main.go +++ b/checks/words/main.go @@ -4,7 +4,9 @@ // own code that is not a test (a tool's description, the notes and errors it answers with) and every // description in its manifest. Comments are not read: an agent never sees them. And no daemon's line // (novox/hq ADR 0272, lines.go): the copy in retired-lines, the lines a person would otherwise be told to -// type of the node-engine's or the controller's binary, where the operator's command is `nox`. +// type of the node-engine's or the controller's binary, where the operator's command is `nox`. And no address of the +// controller (novox/hq ADR 0590 §5): it is not placed, so a text naming one of its verbs, states or buckets sends its +// reader nowhere. // // A word is matched whole and in any case, a space in it matching any run of white space, and its plural // (`s` or `es` on its last part) is matched as the word. A vendor word is @@ -89,11 +91,45 @@ func run(root string) int { len(findings)) return 1 } + // And no address of the controller (novox/hq ADR 0590 §5): it is not placed, so a text that sends an agent or the + // operator to one of its verbs, states or buckets sends them nowhere. + if findings := checkAddresses(texts); len(findings) > 0 { + for _, f := range findings { + fmt.Println(f) + } + fmt.Printf("words: %d address(es) of the controller in what a module shows — name the seat that holds the job now (novox/hq ADR 0590)\n", + len(findings)) + return 1 + } fmt.Printf("words: %d retired words and %d daemons' lines, none in the %d strings of %d files\n", len(words), len(lines), len(texts), files) return 0 } +// controllerAddress is an address of the controller: its seat's verb or state (`mesh-controller.`, with or without +// `seat:`) or one of its buckets (`mesh-controller_`). The module's name in prose is no address. +var controllerAddress = regexp.MustCompile(`(^|[^A-Za-z0-9_-])(seat:)?mesh-controller[._][A-Za-z0-9<]`) + +// checkAddresses is every text that names an address of the controller (novox/hq ADR 0590 §5). +func checkAddresses(texts []text) []string { + var out []string + for _, t := range texts { + if !strings.Contains(t.s, "mesh-controller") { + continue + } + if m := controllerAddress.FindStringIndex(t.s); m != nil { + line := t.line + if line > 0 { + line += strings.Count(t.s[:m[0]], "\n") + } + out = append(out, fmt.Sprintf("%s:%d: names an address of the controller, which is not placed — in %q", t.file, line, + excerpt(t.s, m[0], m[1]))) + } + } + sort.Strings(out) + return out +} + // collect reads every text a module can show under root: the strings of its own code that is not a test, and every string of // its manifests. func collect(root string) ([]text, int, error) { diff --git a/checks/words/main_test.go b/checks/words/main_test.go index 79d8f64a..2f05f0ed 100644 --- a/checks/words/main_test.go +++ b/checks/words/main_test.go @@ -95,3 +95,25 @@ func TestTheCatalogueHoldsNoRetiredWordAndNoDaemonsLine(t *testing.T) { t.Fatalf("the check of the real catalogue exits %d (see what it printed above)", code) } } + +// ADR 0590 §5 (issue 1126 B10): no text a module shows names an address of the controller, which is not placed; prose +// naming it passes. +func TestCheckAddressesRefusesTheControllersAddresses(t *testing.T) { + for _, c := range []struct { + s string + want bool + }{ + {"push this machine (mesh-controller.push)", true}, + {"ask seat:mesh-controller.nodes", true}, + {"the bucket mesh-controller_assignments", true}, + {"`mesh-controller.`", true}, + {"the mesh-controller module is gone.", false}, + {"located in mesh-controller#246", false}, + {"ask mesh-placement.placement", false}, + } { + got := checkAddresses([]text{{file: "m/a.go", line: 1, s: c.s}}) + if (len(got) == 1) != c.want { + t.Errorf("checkAddresses(%q) = %q, want a finding: %v", c.s, got, c.want) + } + } +} diff --git a/modules/mesh-hand-acts/README.md b/modules/mesh-hand-acts/README.md index 3a13fdb4..50f37a9f 100644 --- a/modules/mesh-hand-acts/README.md +++ b/modules/mesh-hand-acts/README.md @@ -19,8 +19,8 @@ alone writes the log. `hand-act drill` for a drill, whose cause is always `drill`). Who did it is the caller the runtime proves. - `mesh-hand-acts.warranted`: asker and ask. Read from the router's own record of that ask (`messenger.asks`), never from the caller, and recorded once per ask; an ask still open, ended without a choice or another asker's is refused. -- `mesh-hand-acts.hand-acts`: both logs, this one and the controller's `mesh-controller_hand-acts` until its lease - ends, newest first, with the causes repaired by hand twice within a fortnight; `warrants=true` lists the warrants. +- `mesh-hand-acts.hand-acts`: this log (the controller's acts were copied into it once), newest first, with the + causes repaired by hand twice within a fortnight; `warrants=true` lists the warrants. - `hand_acts_copied` and `hand_acts_copy` (research 072 row 26): the controller's hand acts are copied once into this seat's log, each with its own key, time and author. It runs at the module's start while the old bucket is readable diff --git a/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts.go b/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts.go index 858faf67..289e97fe 100644 --- a/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts.go +++ b/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts.go @@ -9,8 +9,8 @@ package main // // **Who did it is the runtime's word, never the caller's**: `by` is the bus user the runtime proved. A warrant is the // router's word, never the caller's: `warranted` reads the router's own record of the asker's ask and records who -// chose, through which channel, with which proofs and which answer, once per ask. The controller's bucket -// `mesh-controller_hand-acts` is no longer read (issue 774, ADR 0571 §13): its acts were copied into this log once, and +// chose, through which channel, with which proofs and which answer, once per ask. The controller's hand-act +// bucket is no longer read (issue 774, ADR 0571 §13): its acts were copied into this log once, and // `hand-acts` answers from this log alone. import ( diff --git a/modules/mesh-hand-acts/cmd/mesh-hand-acts/copy.go b/modules/mesh-hand-acts/cmd/mesh-hand-acts/copy.go index 65a492c6..1705bde8 100644 --- a/modules/mesh-hand-acts/cmd/mesh-hand-acts/copy.go +++ b/modules/mesh-hand-acts/cmd/mesh-hand-acts/copy.go @@ -1,7 +1,7 @@ package main // The one-time copy of the controller's hand-act log into this seat's log is over (novox/hq research 072 row 26, ADR 0571 -// §13, issue 774). The controller kept every act of its life in its bucket `mesh-controller_hand-acts`; they were copied +// §13, issue 774). The controller kept every act of its life in a bucket of its own; they were copied // once into `acts`, each as the controller wrote it, by the earlier builds of this module, which left the marker // `controller-hand-acts` of the state `copied`. This module no longer reads that bucket, so the copy cannot run: the // tools `hand_acts_copy` and `hand_acts_copied` stay and say so, with the marker's figures when it exists. diff --git a/modules/mesh-hand-acts/cmd/mesh-hand-acts/tools.go b/modules/mesh-hand-acts/cmd/mesh-hand-acts/tools.go index 3944137d..38bf6400 100644 --- a/modules/mesh-hand-acts/cmd/mesh-hand-acts/tools.go +++ b/modules/mesh-hand-acts/cmd/mesh-hand-acts/tools.go @@ -79,7 +79,7 @@ func Tools(l *Log) []stdio.Tool { Input: obj(map[string]any{}), Run: func(map[string]any) (any, error) { return l.Copied() }}, {Name: CopyTool, - Description: "Answers that the copy is over (the controller's hand-act log, its bucket mesh-controller_hand-acts, " + + Description: "Answers that the copy is over (the controller's hand-act log " + "is no longer read, novox/hq ADR 0571 §13) with the marker of the copy that was made; copies nothing.", Input: obj(map[string]any{"top_up": str("\"true\": after the copy is done, append the acts written since")}), Run: func(a map[string]any) (any, error) { diff --git a/modules/nats/cmd/nats-tools/expiry.go b/modules/nats/cmd/nats-tools/expiry.go index 21756657..6c74ca4f 100644 --- a/modules/nats/cmd/nats-tools/expiry.go +++ b/modules/nats/cmd/nats-tools/expiry.go @@ -148,8 +148,8 @@ func SetBucketExpiry(ctx context.Context, bus Bus, said Said, record Recorded, a // expiryTools is the module's tool that sets a bucket's expiry. func expiryTools(bus Bus, said Said, record Recorded) []stdio.Tool { return []stdio.Tool{{Name: "nats_bucket_expiry", - Description: "Sets or restores the expiry of one key-value bucket: `max_age` is a duration such as 15s, or `none` to keep keys with no expiry (e.g. the lease bucket of mesh-controller at the lease write, back to 15s at the hand-back; novox/hq ADR 0582). Needs `why`, kept in the audit trail and recorded as a hand act through mesh-hand-acts (`warrant` names the operator's ask when there is one); refuses a name that is no key-value bucket; reads the expiry first and back after, and a bucket already at that expiry is answered as such. `dry_run` only reads. The bus refuses any bucket the module's credential is not granted.", - Input: map[string]any{"bucket": str("the bucket as nats_buckets lists it, e.g. mesh-controller_lease"), "max_age": str("a duration such as 15s, or none to remove the expiry"), + Description: "Sets or restores the expiry of one key-value bucket: `max_age` is a duration such as 15s, or `none` to keep keys with no expiry (e.g. a bucket whose keys must outlive their usual age while a person works on them, then back). Needs `why`, kept in the audit trail and recorded as a hand act through mesh-hand-acts (`warrant` names the operator's ask when there is one); refuses a name that is no key-value bucket; reads the expiry first and back after, and a bucket already at that expiry is answered as such. `dry_run` only reads. The bus refuses any bucket the module's credential is not granted.", + Input: map[string]any{"bucket": str("the bucket as nats_buckets lists it, e.g. mesh-config_resolved"), "max_age": str("a duration such as 15s, or none to remove the expiry"), "why": str("why it is changed, for the audit trail (required)"), "warrant": str("the operator's ask this follows, when there is one"), "dry_run": map[string]any{"type": "boolean", "description": "read the expiry, change nothing"}}, Run: func(a map[string]any) (any, error) {