// 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" "strings" ) // 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 } for _, f := range files { raw, err := os.ReadFile(f) if err != nil { return nil, err } var doc any if err := json.Unmarshal(raw, &doc); err != nil { return nil, fmt.Errorf("%s: %w", f, err) } 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)) } } 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 }