diff --git a/checks/handacts/fallbacks b/checks/handacts/fallbacks new file mode 100644 index 00000000..c4fdf709 --- /dev/null +++ b/checks/handacts/fallbacks @@ -0,0 +1,8 @@ +# 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). +mesh-config hand-act fallback until the lease write: its sets, clears and held values record through mesh-hand-acts first +mesh-config warranted fallback until the lease write: its warrants record through mesh-hand-acts first +mesh-healers drill fallback until the lease write: a drill records through mesh-hand-acts first diff --git a/checks/handacts/go.mod b/checks/handacts/go.mod new file mode 100644 index 00000000..29e4c52b --- /dev/null +++ b/checks/handacts/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-catalog/checks/handacts + +go 1.22 diff --git a/checks/handacts/main.go b/checks/handacts/main.go new file mode 100644 index 00000000..1a222d60 --- /dev/null +++ b/checks/handacts/main.go @@ -0,0 +1,109 @@ +// 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 new file mode 100644 index 00000000..61e197e6 --- /dev/null +++ b/checks/handacts/main_test.go @@ -0,0 +1,59 @@ +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 new file mode 100644 index 00000000..61f9814a --- /dev/null +++ b/checks/handacts/reads-outside @@ -0,0 +1,3 @@ +# 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