From 7bb07ce349e70f8b62a417afa238694e82609bae Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 17:58:53 +0100 Subject: [PATCH] New check: no placement directory and no manifest reading the indexer's placement, so the repository path cannot come back --- checks/noplacement/go.mod | 3 + checks/noplacement/main.go | 94 ++++++++++++++++++++++++++++++++ checks/noplacement/main_test.go | 54 ++++++++++++++++++ checks/noplacement/reads-outside | 6 ++ 4 files changed, 157 insertions(+) create mode 100644 checks/noplacement/go.mod create mode 100644 checks/noplacement/main.go create mode 100644 checks/noplacement/main_test.go create mode 100644 checks/noplacement/reads-outside diff --git a/checks/noplacement/go.mod b/checks/noplacement/go.mod new file mode 100644 index 00000000..a09ac5ec --- /dev/null +++ b/checks/noplacement/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-catalog/checks/noplacement + +go 1.22 diff --git a/checks/noplacement/main.go b/checks/noplacement/main.go new file mode 100644 index 00000000..e7af4e27 --- /dev/null +++ b/checks/noplacement/main.go @@ -0,0 +1,94 @@ +// Command noplacement refuses what the repository path of placement left behind (novox/hq ADR 0567 §1, §8, §10). +// +// Placement, which module runs on which node, is bus state held by the seat mesh-placement; no file of the +// catalogue says it, and no manifest reads it from the delivery indexer, which no longer keeps it. It refuses: +// +// 1. a `placement` directory (or file) at the root of the catalogue; +// +// 2. 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`. +// +// go run . +// +// Exits 1 on a finding. +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" + "strings" +) + +// Prefixes are the names of the indexer's placement parts; a manifest string starting with one names them. +var Prefixes = []string{"mesh-delivery-indexer.placement", "mesh-delivery-indexer_placement"} + +// Findings is every refusal in the catalogue at root, sorted. +func Findings(root string) ([]string, error) { + var out []string + if _, err := os.Lstat(filepath.Join(root, "placement")); err == nil { + out = append(out, "placement: the repository holds no placement; it is bus state of the seat mesh-placement (novox/hq ADR 0567 §1)") + } + manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json")) + if err != nil { + return nil, err + } + sort.Strings(manifests) + for _, file := range manifests { + raw, err := os.ReadFile(file) + if err != nil { + return nil, err + } + var doc any + if err := json.Unmarshal(raw, &doc); err != nil { + return nil, fmt.Errorf("%s: %w", file, err) + } + module := filepath.Base(filepath.Dir(file)) + for _, s := range named(doc, nil) { + 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 §8)", module, s)) + } + } + return out, 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) { + case string: + for _, p := range Prefixes { + if strings.HasPrefix(v, p) { + return append(into, v) + } + } + case []any: + for _, e := range v { + into = named(e, into) + } + case map[string]any: + for _, e := range v { + into = named(e, into) + } + } + return into +} + +func main() { + if len(os.Args) != 2 { + fmt.Fprintln(os.Stderr, "usage: noplacement ") + 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 placement directory, and no manifest reads the indexer's placement") +} diff --git a/checks/noplacement/main_test.go b/checks/noplacement/main_test.go new file mode 100644 index 00000000..db3caf85 --- /dev/null +++ b/checks/noplacement/main_test.go @@ -0,0 +1,54 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestFindingsRefuseWhatThePlacementPathLeft(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","reads":["mesh-controller.assignments"]}`}, ""}, + {"a placement directory", map[string]string{"placement/ace.json": `{}`}, "placement: the repository holds no placement"}, + {"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"}, + {"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) { + 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 script's run of this check over the real tree (novox/hq ADR 0529 clause 2). +func TestTheCatalogueHoldsNoPlacementOfTheRepositoryPath(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/noplacement/reads-outside b/checks/noplacement/reads-outside new file mode 100644 index 00000000..2fc76de8 --- /dev/null +++ b/checks/noplacement/reads-outside @@ -0,0 +1,6 @@ +# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole; +# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept +# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing. +# the root, for a placement directory, and every manifest, for a read of the indexer's placement states. +?placement +modules/*/module.json