deps skips a forge requirement the go.mod already replaces to a directory (a package of the module's own repository), which it used to look for in a clone beside the check and fail. The new check removedpackages refuses an import of a package mesh-sdk gave up and a use of the build runner through mesh-sdk's buildstep; it is red on mesh-provisioner until the provisioner stops composing (the release's provisioner subtask). controllerdeps also refuses a go.mod requiring the controller's repository. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
165 lines
6.3 KiB
Go
165 lines
6.3 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 go.mod of a module or of a check requiring the controller's repository (its module path, github.com/novox/mesh-controller,
|
|
// or the forge's git.novox.be/novox/mesh-controller): the repository is archived once its last module is delivered
|
|
// elsewhere (novox/hq ADR 0593 §11), so nothing here may be built from it;
|
|
// - 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])`)
|
|
|
|
// repository is a go.mod line naming the controller's repository by either of its module paths.
|
|
var repository = regexp.MustCompile(`(github\.com|git\.novox\.be)/novox/mesh-controller(/|\s|$)`)
|
|
|
|
// 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))
|
|
}
|
|
}
|
|
mods, err := filepath.Glob(filepath.Join(root, "*", "*", "go.mod"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sort.Strings(mods)
|
|
for _, f := range mods {
|
|
if rel, _ := filepath.Rel(root, f); !strings.HasPrefix(rel, "modules"+string(filepath.Separator)) && !strings.HasPrefix(rel, "checks"+string(filepath.Separator)) {
|
|
continue
|
|
}
|
|
raw, err := os.ReadFile(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for n, l := range strings.Split(string(raw), "\n") {
|
|
if repository.MatchString(l) {
|
|
rel, _ := filepath.Rel(root, f)
|
|
out = append(out, fmt.Sprintf("%s:%d: requires the controller's repository: it is archived once its last module is built elsewhere, so nothing here is built from it (novox/hq ADR 0593 §11)", filepath.ToSlash(rel), n+1))
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|