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
139 lines
5.0 KiB
Go
139 lines
5.0 KiB
Go
// 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.<name>`, `seat:mesh-controller.<verb>`, `mesh-controller_<bucket>`), 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
|
|
}
|