Compare commits

..
1466 changed files with 22660 additions and 114461 deletions
+28 -233
View File
@@ -1,236 +1,31 @@
node_modules/ node_modules/
dist/ dist/
*.test
# Every program a local `go build` leaves, in its main package's directory and at its Go module's root, each anchored to # Go tool bundles built in place (go build in a module's cmd/<name>-tools) are build output.
# its one path so it never hides source (issue 1577: twice in one milestone a module's program was committed). The modules/slack/cmd/slack-tools/slack-tools
# checks/gitignored test refuses a main package whose program no ignore file names: add its line here. modules/jetbrains-toolbox/cmd/toolbox-tools/toolbox-tools
/checks/agenthome/agenthome modules/messenger/cmd/messenger/messenger
/checks/config/config modules/mesh-watcher/cmd/mesh-watcher/mesh-watcher
/checks/deps/deps modules/claude-code/cmd/claude-code/claude-code
/checks/environment/environment modules/dnsmasq/cmd/dnsmasq-tools/dnsmasq-tools
/checks/imagebuild/imagebuild # ...and the same built from the module's root (go build ./cmd/<name>), which names it after the module.
/checks/imageplace/imageplace modules/messenger/messenger
/checks/locks/locks modules/claude-code/claude-code
/checks/mailstore/mailstore modules/mesh-watcher/mesh-watcher
/checks/noplacement/noplacement modules/dnsmasq/dnsmasq
/checks/owned/owned modules/mesh-issues/cmd/mesh-issues/mesh-issues
/checks/particulars/particulars modules/mounts/cmd/mounts/mounts
/checks/paths/paths modules/mesh-knowledge/cmd/mesh-knowledge/mesh-knowledge
/checks/predefined/predefined modules/mesh-knowledge/mesh-knowledge
/checks/readable/readable modules/mesh-reads/mesh-reads
/checks/removedpackages/removedpackages modules/mesh-reads/cmd/mesh-reads/mesh-reads
/checks/secrets/secrets modules/mesh-files/mesh-files
/checks/sshdir/sshdir modules/mesh-files/cmd/mesh-files/mesh-files
/checks/templates/templates
/checks/toolchains/toolchains # The provisioner client built in place (issue 671).
/checks/words/words modules/mesh-provisioner-client/cmd/mesh-provisioner-client/mesh-provisioner-client
/modules/adwaita/adwaita-tools modules/brother/cmd/brother/brother
/modules/adwaita/cmd/adwaita-tools/adwaita-tools modules/mesh-build-go/cmd/mesh-build-go/mesh-build-go
/modules/artifact-store-tools/artifact-store-tools modules/mesh-config/mesh-config
/modules/artifact-store-tools/cmd/artifact-store-tools/artifact-store-tools modules/mesh-build-image/cmd/mesh-build-image/mesh-build-image
/modules/asus-zephyrus-g14/cmd/zephyrus/zephyrus *.test
/modules/asus-zephyrus-g14/zephyrus
/modules/avahi/avahi-tools
/modules/avahi/cmd/avahi-tools/avahi-tools
/modules/blueman/blueman-tools
/modules/blueman/cmd/blueman-tools/blueman-tools
/modules/bluetooth/bluetooth-tools
/modules/bluetooth/cmd/bluetooth-tools/bluetooth-tools
/modules/brother/brother
/modules/brother/cmd/brother/brother
/modules/claude-code/claude-code
/modules/claude-code/cmd/claude-code/claude-code
/modules/claude-licence-manager/claude-licence-manager
/modules/claude-licence-manager/cmd/claude-licence-manager/claude-licence-manager
/modules/clipmenu/clipmenu-tools
/modules/clipmenu/cmd/clipmenu-tools/clipmenu-tools
/modules/cups/cmd/cups-tools/cups-tools
/modules/cups/cups-tools
/modules/dbus/cmd/dbus-tools/dbus-tools
/modules/dbus/dbus-tools
/modules/desk-channel/cmd/desk-channel/desk-channel
/modules/desk-channel/desk-channel
/modules/disk-load/cmd/disk-load/disk-load
/modules/disk-load/disk-load
/modules/distribution/cmd/distribution-tools/distribution-tools
/modules/distribution/cmd/distribution/distribution
/modules/distribution/distribution
/modules/distribution/distribution-tools
/modules/dmenu/cmd/dmenu-tools/dmenu-tools
/modules/dmenu/dmenu-tools
/modules/dnsmasq/cmd/dnsmasq-tools/dnsmasq-tools
/modules/dnsmasq/dnsmasq-tools
/modules/docker-compose/cmd/docker-compose-tools/docker-compose-tools
/modules/docker-compose/docker-compose-tools
/modules/docker/cmd/docker-tools/docker-tools
/modules/docker/docker-tools
/modules/dunst/cmd/dunst-tools/dunst-tools
/modules/dunst/dunst-tools
/modules/fail2ban/cmd/fail2ban-tools/fail2ban-tools
/modules/fail2ban/fail2ban-tools
/modules/feh/cmd/feh-tools/feh-tools
/modules/feh/feh-tools
/modules/flatpak/cmd/flatpak-tools/flatpak-tools
/modules/flatpak/flatpak-tools
/modules/fonts/cmd/fonts-tools/fonts-tools
/modules/fonts/fonts-tools
/modules/forticlient/cmd/forticlient-tools/forticlient-tools
/modules/forticlient/forticlient-tools
/modules/gitea/agent-teams
/modules/gitea/agent-token
/modules/gitea/cmd/agent-teams/agent-teams
/modules/gitea/cmd/agent-token/agent-token
/modules/gitea/cmd/forge-account-provider/forge-account-provider
/modules/gitea/cmd/forge/forge
/modules/gitea/cmd/package-registry/package-registry
/modules/gitea/cmd/repositories/repositories
/modules/gitea/forge
/modules/gitea/forge-account-provider
/modules/gitea/package-registry
/modules/gitea/repositories
/modules/gnome-keyring/cmd/gnome-keyring-tools/gnome-keyring-tools
/modules/gnome-keyring/gnome-keyring-tools
/modules/hostname/cmd/hostname-tools/hostname-tools
/modules/hostname/hostname-tools
/modules/i3/cmd/i3-tools/i3-tools
/modules/i3/i3-tools
/modules/i3status-rust/cmd/i3status-rust-tools/i3status-rust-tools
/modules/i3status-rust/i3status-rust-tools
/modules/kernel/cmd/kernel-tools/kernel-tools
/modules/kernel/kernel-tools
/modules/keycloak/cmd/keycloak-provider/keycloak-provider
/modules/keycloak/keycloak-provider
/modules/lemurs/cmd/lemurs-tools/lemurs-tools
/modules/lemurs/lemurs-tools
/modules/localization/cmd/localization-tools/localization-tools
/modules/localization/localization-tools
/modules/logrotate/cmd/logrotate-tools/logrotate-tools
/modules/logrotate/logrotate-tools
/modules/mailu/automx
/modules/mailu/cmd/automx/automx
/modules/memory-pressure/cmd/memory-pressure/memory-pressure
/modules/memory-pressure/memory-pressure
/modules/mesh-build-go/buildstep/cmd/mesh-pack/mesh-pack
/modules/mesh-build-go/buildstep/cmd/seatgen/seatgen
/modules/mesh-build-go/buildstep/mesh-pack
/modules/mesh-build-go/cmd/mesh-build-go/mesh-build-go
/modules/mesh-build-go/mesh-build-go
/modules/mesh-build-image/cmd/mesh-build-image/mesh-build-image
/modules/mesh-build-image/mesh-build-image
/modules/mesh-build-node/cmd/mesh-build-node/mesh-build-node
/modules/mesh-build-node/mesh-build-node
/modules/mesh-cli/cmd/mesh-cli/mesh-cli
/modules/mesh-cli/mesh-cli
/modules/mesh-conditions/cmd/mesh-conditions/mesh-conditions
/modules/mesh-conditions/mesh-conditions
/modules/mesh-config/cmd/mesh-config/mesh-config
/modules/mesh-config/mesh-config
/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/mesh-delivery-gate
/modules/mesh-delivery-gate/mesh-delivery-gate
/modules/mesh-files/cmd/mesh-files/mesh-files
/modules/mesh-files/mesh-files
/modules/mesh-hand-acts/cmd/mesh-hand-acts/mesh-hand-acts
/modules/mesh-hand-acts/mesh-hand-acts
/modules/mesh-healers/cmd/mesh-healers/mesh-healers
/modules/mesh-healers/mesh-healers
/modules/mesh-issues/cmd/mesh-issues/mesh-issues
/modules/mesh-issues/mesh-issues
/modules/mesh-knowledge/cmd/mesh-knowledge/mesh-knowledge
/modules/mesh-knowledge/mesh-knowledge
/modules/mesh-mail-notifier/cmd/mesh-mail-notifier/mesh-mail-notifier
/modules/mesh-mail-notifier/mesh-mail-notifier
/modules/mesh-mail-reader/cmd/mesh-mail-reader/mesh-mail-reader
/modules/mesh-mail-reader/mesh-mail-reader
/modules/mesh-mail-web/cmd/mesh-mail-web/mesh-mail-web
/modules/mesh-mail-web/mesh-mail-web
/modules/mesh-mail/cmd/mesh-mail/mesh-mail
/modules/mesh-mail/mesh-mail
/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/mesh-provisioner-client
/modules/mesh-provisioner-client/mesh-provisioner-client
/modules/mesh-provisioner/cmd/mesh-provisioner/mesh-provisioner
/modules/mesh-provisioner/mesh-provisioner
/modules/mesh-reads/cmd/mesh-reads/mesh-reads
/modules/mesh-reads/mesh-reads
/modules/mesh-self-check/cmd/mesh-self-check/mesh-self-check
/modules/mesh-self-check/mesh-self-check
/modules/mesh-vault/cmd/mesh-vault/mesh-vault
/modules/mesh-vault/mesh-vault
/modules/mesh-watcher/cmd/mesh-watcher/mesh-watcher
/modules/mesh-watcher/mesh-watcher
/modules/messenger/cmd/messenger/messenger
/modules/messenger/messenger
/modules/minio/cmd/object-store/object-store
/modules/minio/object-store
/modules/mounts/cmd/mounts/mounts
/modules/mounts/mounts
/modules/nats/cmd/mesh-nats-snapshot/mesh-nats-snapshot
/modules/nats/cmd/nats-tools/nats-tools
/modules/nats/mesh-nats-snapshot
/modules/nats/nats-tools
/modules/netcheck/cmd/netcheck/netcheck
/modules/netcheck/netcheck
/modules/networkmanager/cmd/uplink-tools/uplink-tools
/modules/networkmanager/uplink-tools
/modules/nextcloud-client/cmd/nextcloud-client-tools/nextcloud-client-tools
/modules/nextcloud-client/nextcloud-client-tools
/modules/nfs-server/cmd/nfs-server/nfs-server
/modules/nfs-server/nfs-server
/modules/nm-applet/cmd/nm-applet-tools/nm-applet-tools
/modules/nm-applet/nm-applet-tools
/modules/only-office/cmd/only-office/only-office
/modules/only-office/only-office
/modules/openrazer/cmd/openrazer-tools/openrazer-tools
/modules/openrazer/openrazer-tools
/modules/pacman/cmd/pacman-tools/pacman-tools
/modules/pacman/pacman-tools
/modules/picom/cmd/picom-tools/picom-tools
/modules/picom/picom-tools
/modules/polychromatic/cmd/polychromatic-tools/polychromatic-tools
/modules/polychromatic/polychromatic-tools
/modules/postgres/cmd/postgres-provider/postgres-provider
/modules/postgres/postgres-provider
/modules/power/cmd/power/power
/modules/power/power
/modules/records/cmd/records/records
/modules/records/records
/modules/restic/cmd/restic-backups/restic-backups
/modules/restic/restic-backups
/modules/rofi/cmd/rofi-tools/rofi-tools
/modules/rofi/rofi-tools
/modules/route-adapter/cmd/route-provider/route-provider
/modules/route-adapter/route-provider
/modules/screen-lock/cmd/screen-lock-tools/screen-lock-tools
/modules/screen-lock/screen-lock-tools
/modules/sensors/cmd/sensors/sensors
/modules/sensors/sensors
/modules/slack/cmd/slack-tools/slack-tools
/modules/slack/slack-tools
/modules/snapd/cmd/snapd-tools/snapd-tools
/modules/snapd/snapd-tools
/modules/ssh-client/cmd/ssh-client-tools/ssh-client-tools
/modules/ssh-client/ssh-client-tools
/modules/sudo/cmd/sudo-tools/sudo-tools
/modules/sudo/sudo-tools
/modules/systemd-networkd/cmd/uplink-tools/uplink-tools
/modules/systemd-networkd/uplink-tools
/modules/systemd-resolved/cmd/resolver-tools/resolver-tools
/modules/systemd-resolved/resolver-tools
/modules/systemd/cmd/systemd-tools/systemd-tools
/modules/systemd/systemd-tools
/modules/telegram/cmd/telegram/telegram
/modules/telegram/telegram
/modules/time-sync/cmd/time-sync-tools/time-sync-tools
/modules/time-sync/time-sync-tools
/modules/triggerhappy/cmd/triggerhappy-tools/triggerhappy-tools
/modules/triggerhappy/triggerhappy-tools
/modules/xclip/cmd/xclip-tools/xclip-tools
/modules/xclip/xclip-tools
/modules/xdg/cmd/xdg-tools/xdg-tools
/modules/xdg/xdg-tools
/modules/xorg/cmd/xorg/xorg
/modules/xorg/xorg
/modules/xterm/cmd/xterm-tools/xterm-tools
/modules/xterm/xterm-tools
/modules/zfs/cmd/zfs-tools/zfs-tools
/modules/zfs/zfs-tools
-5
View File
@@ -1,5 +0,0 @@
# 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.
# Check("../.."): every manifest.
modules/*/module.json
+1 -1
View File
@@ -2,6 +2,6 @@ module git.novox.be/novox/mesh-catalog/checks/config
go 1.22 go 1.22
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0-00010101000000-000000000000 require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
-5
View File
@@ -1,5 +0,0 @@
# 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.
# Check("../.."): every manifest and the files it names.
modules
-164
View File
@@ -1,164 +0,0 @@
// 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
}
@@ -1,64 +0,0 @@
package controllerdeps
import (
"os"
"path/filepath"
"strings"
"testing"
)
// ADR 0590 §5 (issue 1126 B10): no manifest of the catalogue at this head reads, invokes, consumes, waits on, uses or
// otherwise names the controller's seat, states or buckets, and no module's code names a bucket of the controller's.
func TestNothingInTheCatalogueNamesTheController(t *testing.T) {
got, err := Refusals("../..")
if err != nil {
t.Fatal(err)
}
for _, r := range got {
t.Error(r)
}
}
// The check refuses every name of the controller, the lease and the entries once kept to its write among them, on a
// tree of its own; a repository named mesh-controller (an issue's owner) and a word in prose are no dependency.
func TestRefusalsJudgeATree(t *testing.T) {
for _, row := range []struct {
name string
files map[string]string
want string // empty: passes
}{
{"the lease is refused like any read", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-controller.lease"]}`}, `a: names "mesh-controller.lease"`},
{"a verb once kept to the write", map[string]string{"modules/b/module.json": `{"module":"b","invokes":["seat:mesh-controller.collect"]}`}, `b: names "seat:mesh-controller.collect"`},
{"an event consumed", map[string]string{"modules/c/module.json": `{"module":"c","consumes":[{"event":"mesh-controller.artifacts-gone"}]}`}, "mesh-controller.artifacts-gone"},
{"a requirement", map[string]string{"modules/d/module.json": `{"module":"d","requires":[{"name":"x","from":"mesh-controller"}]}`}, `d: names "mesh-controller"`},
{"the controller's own module", map[string]string{"modules/mesh-controller/module.json": `{"module":"mesh-controller"}`}, "mesh-controller: the controller is no module of the catalogue"},
{"a bucket in code", map[string]string{"modules/e/cmd/e/main.go": "package main\nconst b = \"mesh-controller_assignments\"\n"}, "modules/e/cmd/e/main.go:2: names the controller's bucket"},
{"a requirement of the controller's repository", map[string]string{"modules/h/go.mod": "module h\n\nrequire github.com/novox/mesh-controller v0.0.0\n"}, "modules/h/go.mod:3: requires the controller's repository"},
{"its forge path in a check", map[string]string{"checks/i/go.mod": "module i\n\nrequire (\n\tgit.novox.be/novox/mesh-controller/modules/route-proxy v0.1.0\n)\n"}, "checks/i/go.mod:4"},
{"another repository of the forge passes", map[string]string{"modules/j/go.mod": "module j\n\nrequire git.novox.be/novox/mesh-sdk/go v0.1.0\n"}, ""},
{"a bucket in a test is its data", map[string]string{"modules/e/cmd/e/main_test.go": "package main\n// mesh-controller_lease\n"}, ""},
{"a fixture copied from elsewhere", map[string]string{"modules/e/cmd/e/testdata/x.md": "mesh-controller_rollouts\n"}, ""},
{"an owner and prose pass", map[string]string{"modules/f/module.json": `{"module":"f","config":{"owners":{"default":["mesh-controller","mesh-controller modules/route-proxy"]}},"why":"the mesh-controller module is gone"}`}, ""},
{"the seat's successors pass", map[string]string{"modules/g/module.json": `{"module":"g","reads":["mesh-module-assigner.assignments"],"invokes":["seat:mesh-hand-acts.hand-act"]}`}, ""},
} {
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 := Refusals(root)
if err != nil {
t.Fatal(err)
}
if joined := strings.Join(got, "\n"); (row.want == "") != (joined == "") || !strings.Contains(joined, row.want) {
t.Errorf("refusals %q, want %q", joined, row.want)
}
})
}
}
-3
View File
@@ -1,3 +0,0 @@
module checks/controllerdeps
go 1.22
-3
View File
@@ -1,3 +0,0 @@
# What these tests read outside their package, as paths from the repository's root (issue 1936).
# every module's directory: its manifest, for a name of the controller, and its files, for one of the controller's buckets.
modules
-6
View File
@@ -1,6 +0,0 @@
module checks/credentialform
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
View File
-98
View File
@@ -1,98 +0,0 @@
// Package credentialform refuses what the catalogue's tree holds about credentials and routes that no manifest judge
// sees: a module that provides and keeps a `bound` state the provisioner does not read, and a route received as a file.
// It judges no offer's credential form: that rule (an own secret, an issuer, or `form` pair or none; the lead,
// 2026-10-29) is part of the catalogue's rules, which the checker's judge runs on every change (CatalogueProblems,
// novox/hq ADR 0578 §2); this package called it itself until 2026-11.
//
// It refuses a route received as a file too (issue 774, research 072 row 34, the operator on 2026-10-31: "there is only
// 1 path, the other is simply deprecated/invalid"): a route is a provision, served from the seat mesh-provisioner's
// plans alone, and a manifest's `receives.route` is the deprecated path. A module still moving off it is listed in
// stillReceivingRoutes with why; every refusal names them all, and a listed module that no longer receives one is
// refused too, so the list only shrinks.
package credentialform
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Problems is every module of the manifests under root/modules that keeps a `bound` state the provisioner does not read,
// and every route received as a file, one line each.
func Problems(root string) ([]string, error) {
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(files)
var out []string
var receiving []string
reads := map[string]bool{}
var binders []string
for _, f := range files {
raw, err := os.ReadFile(f)
if err != nil {
return nil, err
}
m, err := catalogue.Decode(raw)
if err != nil {
return nil, fmt.Errorf("%s: %w", f, err)
}
if _, received := m.Receives["route"]; received {
receiving = append(receiving, m.Module)
} else if _, listed := stillReceivingRoutes[m.Module]; listed {
out = append(out, fmt.Sprintf("%s no longer receives route; take it off stillReceivingRoutes", m.Module))
}
var kept struct {
Reads []string `json:"reads"`
State []json.RawMessage `json:"state"`
}
_ = json.Unmarshal(raw, &kept) // reads may hold objects in another module; only the provisioner's are read
if m.Module == "mesh-provisioner" {
for _, r := range kept.Reads {
reads[r] = true
}
}
if len(m.Provides) > 0 && keepsBound(kept.State) {
binders = append(binders, m.Module)
}
}
for _, module := range binders {
if !reads[module+".bound"] {
out = append(out, fmt.Sprintf("%s provides and keeps a `bound` state, and mesh-provisioner's manifest does not read %s.bound", module, module))
}
}
for _, module := range receiving {
if _, listed := stillReceivingRoutes[module]; !listed {
out = append(out, fmt.Sprintf("%s receives route as a file: a route is a provision, served from the seat "+
"mesh-provisioner's plans alone (issue 774); still on the deprecated path: %s", module,
strings.Join(receiving, ", ")))
}
}
return out, nil
}
// stillReceivingRoutes is every module still receiving its routes as a file, and why it has not moved yet.
var stillReceivingRoutes = map[string]string{
"route-adapter": "migration scaffolding writing into a predecessor's proxy (novox/hq ADR 0104), deleted when that " +
"migration ends; it reads the received file its TypeScript adapter turns into the predecessor's route files",
}
// keepsBound says whether a manifest's `state` names `bound`, as a plain name or an object with a name.
func keepsBound(state []json.RawMessage) bool {
for _, s := range state {
var name string
var obj struct {
Name string `json:"name"`
}
if json.Unmarshal(s, &name) == nil && name == "bound" || json.Unmarshal(s, &obj) == nil && obj.Name == "bound" {
return true
}
}
return false
}
-54
View File
@@ -1,54 +0,0 @@
package credentialform
import (
"os"
"path/filepath"
"strings"
"testing"
)
func aCatalogue(t *testing.T, manifests map[string]string) string {
t.Helper()
root := t.TempDir()
for module, body := range manifests {
dir := filepath.Join(root, "modules", module)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
return root
}
// A binder the provisioner does not read is refused and named; an offer that says no form is the judge's to refuse, not
// this check's; a route received as a file is refused but for the module still moving off it; and the real catalogue is
// clean.
func TestNoBinderIsUnreadAndNoRouteIsReceivedAsAFile(t *testing.T) {
root := aCatalogue(t, map[string]string{
"silent": `{"module":"silent","provides":[{"name":"thing","scope":"mesh"}]}`,
"binder": `{"module":"binder","provides":[{"name":"d","credential":{"form":"pair"}}],"state":["bound"]}`,
"mesh-provisioner": `{"module":"mesh-provisioner","reads":["other.bound"]}`,
"says": `{"module":"says","provides":[{"name":"a","credential":{"form":"pair"}},{"name":"b","credential":{"form":"none"}},{"name":"c","credential":{"own":"jwt"}}]}`,
// A route received as a file (issue 774, research 072 row 34): refused, naming every module still on it; the one
// listed as still moving passes.
"old-proxy": `{"module":"old-proxy","receives":{"route":"/var/lib/old-proxy/mesh.json"}}`,
"route-adapter": `{"module":"route-adapter","receives":{"route":"/var/lib/route-adapter/mesh.json"}}`,
})
got, err := Problems(root)
if err != nil {
t.Fatal(err)
}
if len(got) != 2 || !strings.Contains(got[0], "binder") || !strings.Contains(got[0], "mesh-provisioner") ||
!strings.HasPrefix(got[1], "old-proxy receives route") || !strings.Contains(got[1], "old-proxy, route-adapter") {
t.Fatalf("refused %v, wanted the binder the provisioner does not read and the received route", got)
}
real, err := Problems(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
if len(real) != 0 {
t.Fatalf("the catalogue has binders or routes it must not:\n%s", strings.Join(real, "\n"))
}
}
-3
View File
@@ -1,3 +0,0 @@
# What these tests read outside their package, as paths from the repository's root (issue 1936).
# every manifest, for its bound state and its received routes.
modules/*/module.json
+2 -13
View File
@@ -111,24 +111,13 @@ func forgeRequirements(dir string) ([]requirement, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("its go.mod cannot be read: %s", lastLine(out)) return nil, fmt.Errorf("its go.mod cannot be read: %s", lastLine(out))
} }
var mod struct { var mod struct{ Require []requirement }
Require []requirement
Replace []struct{ Old, New requirement }
}
if err := json.Unmarshal(out, &mod); err != nil { if err := json.Unmarshal(out, &mod); err != nil {
return nil, fmt.Errorf("its go.mod cannot be read: %w", err) return nil, fmt.Errorf("its go.mod cannot be read: %w", err)
} }
// A requirement the go.mod replaces to a directory is a package of the module's own repository (novox/hq ADR 0593 §1): it
// is in the tree under check, so it is fetched from nowhere.
local := map[string]bool{}
for _, r := range mod.Replace {
if r.New.Version == "" {
local[r.Old.Path] = true
}
}
var on []requirement var on []requirement
for _, r := range mod.Require { for _, r := range mod.Require {
if strings.HasPrefix(r.Path, forge) && !local[r.Path] { if strings.HasPrefix(r.Path, forge) {
on = append(on, r) on = append(on, r)
} }
} }
-25
View File
@@ -175,28 +175,3 @@ func TestTheRefOfAVersion(t *testing.T) {
} }
} }
} }
// ADR 0593 §1: a module requires a sibling package of its own repository and replaces it to the directory beside it; that
// package is no fetch from the forge (it is in the tree under check), so it is never looked for in a clone beside the check,
// and the module builds with the SDK taken from there.
func TestASiblingPackageReplacedToItsDirectoryIsNotFetched(t *testing.T) {
offline(t)
beside, commit := sdkBeside(t)
version := "v0.1.14-0.20261012082113-" + commit[:12]
root := t.TempDir()
sibling := "git.novox.be/novox/mesh-catalog/modules/builder/runner"
write(t, filepath.Join(root, "builder", "runner", "go.mod"), "module "+sibling+"\n\ngo 1.22\n\nrequire git.novox.be/novox/mesh-sdk/go "+version+"\n")
write(t, filepath.Join(root, "builder", "runner", "run.go"), "package runner\n\nimport \"git.novox.be/novox/mesh-sdk/go/sdk\"\n\nfunc Run() string { return sdk.Hello() }\n")
dir := filepath.Join(root, "user")
write(t, filepath.Join(dir, "go.mod"), "module git.novox.be/novox/mesh-catalog/modules/user\n\ngo 1.22\n\nrequire (\n\t"+sibling+" v0.0.0\n"+
"\tgit.novox.be/novox/mesh-sdk/go "+version+"\n)\n\nreplace "+sibling+" => ../builder/runner\n")
write(t, filepath.Join(dir, "main.go"), "package main\n\nimport \""+sibling+"\"\n\nfunc main() { println(runner.Run()) }\n")
modfile, err := resolve(dir, beside, t.TempDir())
if err != nil {
t.Fatalf("resolve: %v", err)
}
if modfile == "" {
t.Fatal("resolve said the module fetches its dependencies, with nothing to fetch the SDK from")
}
builds(t, dir, modfile)
}
-3
View File
@@ -1,3 +0,0 @@
module git.novox.be/novox/mesh-catalog/checks/environment
go 1.22
-252
View File
@@ -1,252 +0,0 @@
// Command environment refuses a module's code reading a MESH_* word of its environment that nothing gives it: not its
// manifest, and not the node-engine or the tool runner for every process (the lead's ruling on A3n, issue 1966).
//
// A MESH_* word is the mesh's contract with a module's process: the manifest sets it (an `env` of a build artifact or a
// resource, an `environment`'s `variables`, a `NAME=` line of a file the module ships for an env-file), or the mesh gives
// it to every process (Given). A word read and set by nothing is a contract nobody keeps: the program runs on its
// default or on nothing, and says so to nobody (MESH_REGISTRY_REACH answered every published reference without its
// registry; MESH_CONFIG_CALLERS held every window shut). Found here, it is refused: the code reads what the mesh gives
// (a manifest word, a config value through its tools, a seat's name), or the manifest sets it.
//
// A read is `os.Getenv` or `os.LookupEnv` of a literal or of a constant of the same directory (Go), and `env.MESH_*` or
// `env["MESH_*"]` (TypeScript, `process.env` included); tests, test directories, node_modules and dist are not read. The
// modules still reading such a word are listed in the file `unset` beside this check, `<module> <WORD>` a line, until
// each is fixed: a line no longer found is refused, so the list only shrinks.
//
// go run . <catalogue root>
//
// Exits 1 on a finding.
package main
import (
"bufio"
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// Given is every MESH_* word the mesh gives a module's processes whatever their manifest says, and who gives it.
var Given = map[string]string{
// The tool runner, to every bundle and to a module's own runtime (mesh-tools node-tools runtime.BundleEnv).
"MESH_MODULE": "the tool runner",
"MESH_SERVED_MODULE": "the tool runner",
"MESH_NODE": "the tool runner",
// The node-engine, to the tool runner it runs as the operator's account; its bundles inherit them (mesh-host
// compose/resolution runtime.go).
"MESH_OPERATOR_ACCOUNT": "the node-engine",
"MESH_OPERATOR_HOME": "the node-engine",
// The node-engine, to every module that yields to the node's builds (ADR 0553 §3; catalogue.BuildCPUShareWord).
"MESH_BUILD_CPU_SHARE": "the node-engine",
}
const listFile = "unset"
var (
goRead = regexp.MustCompile(`os\.(?:Getenv|LookupEnv)\(\s*(?:"(MESH_[A-Z0-9_]+)"|([A-Za-z_][A-Za-z0-9_]*))\s*\)`)
goConst = regexp.MustCompile(`\b([A-Za-z_][A-Za-z0-9_]*)\s*(?:string\s*)?=\s*"(MESH_[A-Z0-9_]+)"`)
tsRead = regexp.MustCompile(`\benv(?:\.|\[\s*["'])(MESH_[A-Z0-9_]+)`)
envLine = regexp.MustCompile(`(?m)^\s*(?:export\s+)?(MESH_[A-Z0-9_]+)=`)
)
// skipped are the directories whose code no process of the module runs.
var skipped = map[string]bool{"node_modules": true, "dist": true, "test": true, "testdata": true, ".git": true}
// Check answers, per module, each MESH_* word its code reads that its manifest does not set and the mesh does not
// give, with the files reading it.
func Check(root string) (map[string]map[string][]string, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(manifests)
out := map[string]map[string][]string{}
for _, path := range manifests {
dir := filepath.Dir(path)
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var manifest any
if err := json.Unmarshal(raw, &manifest); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
set := map[string]bool{}
setBy(manifest, set)
reads := map[string]map[string]bool{}
consts := map[string]map[string]string{} // directory -> constant -> word
var goFiles []string
err = filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
if p != dir && skipped[d.Name()] {
return filepath.SkipDir
}
return nil
}
name := d.Name()
text, err := os.ReadFile(p)
if err != nil {
return err
}
rel, _ := filepath.Rel(dir, p)
rel = filepath.ToSlash(rel)
switch {
case strings.HasSuffix(name, "_test.go"), strings.HasSuffix(name, ".test.ts"), strings.HasSuffix(name, ".d.ts"):
case strings.HasSuffix(name, ".go"):
goFiles = append(goFiles, p)
c := consts[filepath.Dir(p)]
if c == nil {
c = map[string]string{}
consts[filepath.Dir(p)] = c
}
for _, m := range goConst.FindAllStringSubmatch(string(text), -1) {
c[m[1]] = m[2]
}
case strings.HasSuffix(name, ".ts"):
for _, m := range tsRead.FindAllStringSubmatch(string(text), -1) {
read(reads, m[1], rel)
}
case name != "module.json":
// A file the module ships: a NAME= line is a word an env-file gives.
for _, m := range envLine.FindAllStringSubmatch(string(text), -1) {
set[m[1]] = true
}
}
return nil
})
if err != nil {
return nil, err
}
for _, p := range goFiles {
text, _ := os.ReadFile(p)
rel, _ := filepath.Rel(dir, p)
for _, m := range goRead.FindAllStringSubmatch(string(text), -1) {
word := m[1]
if word == "" {
word = consts[filepath.Dir(p)][m[2]]
}
if word != "" {
read(reads, word, filepath.ToSlash(rel))
}
}
}
module := filepath.Base(dir)
for word, files := range reads {
if set[word] || Given[word] != "" {
continue
}
if out[module] == nil {
out[module] = map[string][]string{}
}
for f := range files {
out[module][word] = append(out[module][word], f)
}
sort.Strings(out[module][word])
}
}
return out, nil
}
func read(reads map[string]map[string]bool, word, file string) {
if reads[word] == nil {
reads[word] = map[string]bool{}
}
reads[word][file] = true
}
// setBy collects the words a manifest sets: every key of an `env` object and of an `environment`'s `variables`.
func setBy(v any, set map[string]bool) {
switch v := v.(type) {
case map[string]any:
for k, e := range v {
if m, ok := e.(map[string]any); ok && (k == "env" || k == "variables") {
for word := range m {
set[word] = true
}
}
setBy(e, set)
}
case []any:
for _, e := range v {
setBy(e, set)
}
}
}
// Listed is the list's `<module> <WORD>` lines; a line starting with # and a blank line are not entries.
func Listed(path string) (map[string]bool, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
out := map[string]bool{}
s := bufio.NewScanner(f)
for s.Scan() {
line := strings.Join(strings.Fields(s.Text()), " ")
if line == "" || strings.HasPrefix(line, "#") {
continue
}
out[line] = true
}
return out, s.Err()
}
// Judge answers the reads not on the list (refused) and the list's lines no read stands for (stale).
func Judge(found map[string]map[string][]string, listed map[string]bool) (refused, stale []string) {
seen := map[string]bool{}
for module, words := range found {
for word := range words {
line := module + " " + word
seen[line] = true
if !listed[line] {
refused = append(refused, line)
}
}
}
for line := range listed {
if !seen[line] {
stale = append(stale, line)
}
}
sort.Strings(refused)
sort.Strings(stale)
return refused, stale
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
found, err := Check(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
listed, err := Listed(listFile)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
refused, stale := Judge(found, listed)
for _, line := range refused {
module, word, _ := strings.Cut(line, " ")
fmt.Printf("%s reads %s (%s), which its manifest does not set and the mesh gives no process: set it in the "+
"manifest, or read what the mesh gives\n", module, word, strings.Join(found[module][word], ", "))
}
for _, line := range stale {
fmt.Printf("checks/environment/%s names %q, which no code reads unset any more: take it off the list\n", listFile, line)
}
if len(refused) > 0 || len(stale) > 0 {
os.Exit(1)
}
fmt.Printf("environment: every MESH_* word a module reads is set by its manifest or given by the mesh (%d still listed)\n", len(listed))
}
-88
View File
@@ -1,88 +0,0 @@
package main
import (
"os"
"path/filepath"
"reflect"
"testing"
)
// firstCount is how many lines the list held when it was first written: it only shrinks.
const firstCount = 63
func module(t *testing.T, root, name, manifest string, files map[string]string) {
t.Helper()
dir := filepath.Join(root, "modules", name)
for path, text := range map[string]string{"module.json": manifest} {
files[path] = text
}
for path, text := range files {
full := filepath.Join(dir, filepath.FromSlash(path))
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(full, []byte(text), 0o644); err != nil {
t.Fatal(err)
}
}
}
// A word read and set by nothing is found, in Go (a literal, a constant of the same directory) and in TypeScript; one
// the manifest sets (an env, an environment's variables, an env-file line it ships) or the mesh gives every process is
// not; tests, node_modules and dist are not read.
func TestAWordReadThatNothingSetsIsFound(t *testing.T) {
root := t.TempDir()
module(t, root, "reader", `{"module":"reader","build":{"artifacts":[{"name":"tools","env":{"MESH_SET":"x"}}]},`+
`"resources":[{"id":"p","type":"process","environment":{"variables":{"MESH_VARIABLE":"y"}}}]}`, map[string]string{
"cmd/a/main.go": `package main; const reach = "MESH_REACH"; var _ = os.Getenv(reach); var _ = os.Getenv("MESH_SET")`,
"cmd/a/other.go": `package main; var _, _ = os.LookupEnv("MESH_LITERAL"); var _ = os.Getenv("MESH_MODULE")`,
"cmd/a/write.go": `package main; var _ = []string{"MESH_WRITTEN_FOR_A_CHILD=1"}`,
"cmd/a/a_test.go": `package main; var _ = os.Getenv("MESH_IN_A_TEST")`,
"client.ts": `const a = process.env.MESH_TS ?? "d"; const b = env["MESH_BRACKET"]; const c = env.MESH_VARIABLE;`,
"files/env": "MESH_FROM_FILE=${config:x}\n",
"index.ts": `const f = env.MESH_FROM_FILE;`,
"test/x.test.ts": `env.MESH_IN_A_TEST_DIR`,
"node_modules/x.ts": `env.MESH_VENDORED`,
})
module(t, root, "quiet", `{"module":"quiet"}`, map[string]string{"main.go": `package main; var _ = os.Getenv("MESH_NODE")`})
got, err := Check(root)
if err != nil {
t.Fatal(err)
}
want := map[string]map[string][]string{"reader": {
"MESH_REACH": {"cmd/a/main.go"},
"MESH_LITERAL": {"cmd/a/other.go"},
"MESH_TS": {"client.ts"},
"MESH_BRACKET": {"client.ts"},
}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("found %v, want %v", got, want)
}
refused, stale := Judge(got, map[string]bool{"reader MESH_REACH": true, "reader MESH_GONE": true})
if !reflect.DeepEqual(refused, []string{"reader MESH_BRACKET", "reader MESH_LITERAL", "reader MESH_TS"}) ||
!reflect.DeepEqual(stale, []string{"reader MESH_GONE"}) {
t.Errorf("refused %v, stale %v", refused, stale)
}
}
// The catalogue itself: every word a module reads is set or given, but the list's, and the list only shrinks.
func TestTheCatalogueReadsOnlyWordsTheMeshSets(t *testing.T) {
found, err := Check(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
listed, err := Listed(listFile)
if err != nil {
t.Fatal(err)
}
refused, stale := Judge(found, listed)
for _, line := range refused {
t.Errorf("%s: read, set by nothing, and not on checks/environment/unset", line)
}
for _, line := range stale {
t.Errorf("checks/environment/unset names %q, which no code reads unset any more", line)
}
if len(listed) > firstCount {
t.Errorf("checks/environment/unset holds %d lines, more than its first %d: it only shrinks", len(listed), firstCount)
}
}
-5
View File
@@ -1,5 +0,0 @@
# 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.
# Check("../.."): every manifest and the code and files of its module.
modules
-67
View File
@@ -1,67 +0,0 @@
# The MESH_* words a module's code still reads that its manifest does not set and the mesh gives no process,
# `<module> <WORD>` a line, as checks/environment first found them (issue 1966). Each is a contract nobody keeps: fix
# it (the manifest sets it, or the code reads what the mesh gives) and take its line off. A line no code reads unset any
# more is refused, so this list only shrinks; a new read set by nothing is refused, never added here.
baserow MESH_BASEROW_EMAIL
baserow MESH_BASEROW_HOST
baserow MESH_BASEROW_PASSWORD
claude-code MESH_AGENT_ACCOUNT
claude-code MESH_AGENT_HOME
claude-licence-manager MESH_ANTHROPIC_TOKEN_ENDPOINT
claude-licence-manager MESH_ANTHROPIC_USAGE_ENDPOINT
confluence MESH_CONFLUENCE_EMAIL
confluence MESH_CONFLUENCE_TOKEN
confluence MESH_CONFLUENCE_URL
disk-load MESH_DISK_MEASURE_EVERY
gitlab MESH_GITLAB_TOKEN
gitlab MESH_GITLAB_URL
grafana MESH_GRAFANA_PASSWORD
grafana MESH_GRAFANA_TOKEN
grafana MESH_GRAFANA_USER
home-assistant MESH_HOMEASSISTANT_TOKEN
home-assistant MESH_HOMEASSISTANT_WAIT_SECONDS
home-assistant MESH_HOMEASSISTANT_WATCH
icecast MESH_ICECAST_ADMIN_PASSWORD
icecast MESH_ICECAST_ADMIN_USER
influxdb MESH_INFLUXDB_ORG
influxdb MESH_INFLUXDB_TOKEN
jira MESH_JIRA_EMAIL
jira MESH_JIRA_TOKEN
jira MESH_JIRA_URL
letta MESH_LETTA_PASSWORD
mailu MESH_MAILU_API_KEY
mailu MESH_MAILU_DOMAIN
mesh-cli MESH_CONSOLE
mesh-cli MESH_VERB
mesh-files MESH_OWNED_PATHS
mesh-files MESH_SECRET_PLACES
mesh-reads MESH_SECRET_PLACES
minio MESH_MINIO_ROOT_PASSWORD
mongodb MESH_MONGODB_AUTHSOURCE
mongodb MESH_MONGODB_HOST
mongodb MESH_MONGODB_PASSWORD
mongodb MESH_MONGODB_PORT
mongodb MESH_MONGODB_USER
mosquitto MESH_MQTT_ADMIN_USER
mosquitto MESH_MQTT_BROKER_GID
mosquitto MESH_MQTT_BROKER_UID
mosquitto MESH_MQTT_HOST
mosquitto MESH_MQTT_PASSWORD
mosquitto MESH_MQTT_PORT
mssql MESH_MSSQL_HOST
mssql MESH_MSSQL_PASSWORD
mssql MESH_MSSQL_PORT
mssql MESH_MSSQL_READER_PASSWORD
mssql MESH_MSSQL_USER
nextcloud MESH_NEXTCLOUD_ADMIN_PASSWORD
nextcloud MESH_NEXTCLOUD_CONTAINER
nodered MESH_NODERED_TOKEN
nodered MESH_NODERED_WAIT_SECONDS
redis MESH_REDIS_HOST
redis MESH_REDIS_PASSWORD
redis MESH_REDIS_PORT
redis MESH_REDIS_USERNAME
restic MESH_BACKUP_HOUR
unifi MESH_UNIFI_PASSWORD
unifi MESH_UNIFI_SITE
unifi MESH_UNIFI_USERNAME
-128
View File
@@ -1,128 +0,0 @@
// Package failurekit refuses a build module whose tests do not run the build runner's conformance kit (issue 1901, its thorough
// fix): a module that claims a node-build-* seat and serves a test step must hold a test that calls conformance.Run, so
// its parse of the test runner's output is held to the failure contract (a failing test named with its cause, never a
// passing line in its place). A build module with no test step (mesh-build-image builds images and runs no tests) is
// exempt from that, and said so. Every build module, test step or not, must also hold a test that calls
// conformance.LeftRunning with its own start sweep, so a step a previous process left running is ended at its start
// (issue 1890).
package failurekit
import (
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
)
// Judged is what the check says of one build module: Refused says why it fails the check, "" when it passes; Exempt
// says why the kit does not apply.
type Judged struct {
Module, Refused, Exempt string
}
type manifest struct {
Claims []struct {
Name string `json:"name"`
} `json:"claims"`
Tools []json.RawMessage `json:"tools"`
}
// Judge reads every modules/<m>/module.json of the catalogue at root and judges each module that claims a node-build-*
// seat.
func Judge(root string) ([]Judged, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(manifests)
var out []Judged
for _, p := range manifests {
raw, err := os.ReadFile(p)
if err != nil {
return nil, err
}
var m manifest
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", p, err)
}
seat := ""
for _, c := range m.Claims {
if strings.HasPrefix(c.Name, "node-build-") {
seat = c.Name
}
}
if seat == "" {
continue
}
dir := filepath.Dir(p)
j := Judged{Module: filepath.Base(dir)}
var refused []string
test := testTool(m.Tools)
switch {
case test == "":
j.Exempt = fmt.Sprintf("it claims %s and serves no test step, so the failure contract's cases do not apply", seat)
default:
runs, err := runsTheKit(dir, "conformance.Run(")
if err != nil {
return nil, err
}
if !runs {
refused = append(refused, fmt.Sprintf("it claims %s and serves the test step %s, and no test of it calls conformance.Run "+
"(mesh-build-go buildstep/conformance): run the kit with the module's own parse path (issue 1901)", seat, test))
}
}
// Every build module keeps steps, so every one runs the left-running case, test step or not (issue 1890).
sweeps, err := runsTheKit(dir, "conformance.LeftRunning(")
if err != nil {
return nil, err
}
if !sweeps {
refused = append(refused, fmt.Sprintf("it claims %s, and no test of it calls conformance.LeftRunning "+
"(mesh-build-go buildstep/conformance): run the kit's left-running case with the module's own start sweep (issue 1890)", seat))
}
j.Refused = strings.Join(refused, "; ")
out = append(out, j)
}
return out, nil
}
// testTool is the name of the module's test step tool (<prefix>_test), or "".
func testTool(tools []json.RawMessage) string {
for _, raw := range tools {
var name string
if json.Unmarshal(raw, &name) != nil {
var t struct {
Name string `json:"name"`
}
if json.Unmarshal(raw, &t) != nil {
continue
}
name = t.Name
}
if strings.HasSuffix(name, "_test") {
return name
}
}
return ""
}
// runsTheKit says a _test.go file under dir calls the kit's function call (such as "conformance.Run(").
func runsTheKit(dir, call string) (bool, error) {
found := false
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
if err != nil || found || d.IsDir() || !strings.HasSuffix(p, "_test.go") {
return err
}
raw, err := os.ReadFile(p)
if err != nil {
return err
}
found = strings.Contains(string(raw), call) &&
strings.Contains(string(raw), `"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep/conformance"`)
return nil
})
return found, err
}
-77
View File
@@ -1,77 +0,0 @@
package failurekit
import (
"os"
"path/filepath"
"testing"
)
// 1901/AC7 and 1890/AC8: a build module with a test step and no kit is refused, one that runs the kit passes, one with no
// test step is exempt from the failure cases but not from the left-running case, a module that runs only the failure
// cases is refused, and a module that claims no node-build-* seat is not judged.
func TestABuildModuleWithATestStepMustRunTheKit(t *testing.T) {
root := t.TempDir()
const imp = "package main\n\nimport \"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep/conformance\"\n\n"
const sweep = "func TestSweep(t *testing.T) { conformance.LeftRunning(t, nil) }\n"
kit := imp + "func TestKit(t *testing.T) { conformance.Run(t, conformance.GoTest, nil) }\n" + sweep
for path, body := range map[string]string{
"modules/a/module.json": `{"claims":[{"name":"node-build-a"}],"tools":["build_a_fetch","build_a_test"]}`,
"modules/a/cmd/a/check_test.go": "package main\n",
"modules/b/module.json": `{"claims":[{"name":"node-build-b"}],"tools":[{"name":"build_b_test"}]}`,
"modules/b/cmd/b/kit_test.go": kit,
"modules/c/module.json": `{"claims":[{"name":"node-build-c"}],"tools":["build_c_build"]}`,
"modules/c/cmd/c/kit_test.go": imp + sweep,
"modules/f/module.json": `{"claims":[{"name":"node-build-f"}],"tools":["build_f_build"]}`,
"modules/g/module.json": `{"claims":[{"name":"node-build-g"}],"tools":["build_g_test"]}`,
"modules/g/cmd/g/kit_test.go": imp + "func TestKit(t *testing.T) { conformance.Run(t, conformance.GoTest, nil) }\n",
"modules/d/module.json": `{"claims":[{"name":"other"}],"tools":["d_test"]}`,
"modules/e/module.json": `{"claims":[{"name":"node-build-e"}],"tools":["build_e_test"]}`,
"modules/e/cmd/e/kit.go": kit, // not a test file
} {
p := filepath.Join(root, path)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
got, err := Judge(root)
if err != nil {
t.Fatal(err)
}
want := map[string]string{"a": "refused", "b": "passes", "c": "exempt", "e": "refused", "f": "refused", "g": "refused"}
if len(got) != len(want) {
t.Fatalf("judged %+v, want a, b, c, e, f and g", got)
}
for _, j := range got {
is := "passes"
if j.Refused != "" {
is = "refused"
} else if j.Exempt != "" {
is = "exempt"
}
if is != want[j.Module] {
t.Errorf("%s %s (%+v), want it %s", j.Module, is, j, want[j.Module])
}
}
}
// Every build module of this catalogue with a test step runs the kit.
func TestTheCataloguesBuildModulesRunTheKit(t *testing.T) {
got, err := Judge(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
if len(got) == 0 {
t.Fatal("no module claims a node-build-* seat: the check reads the wrong tree")
}
for _, j := range got {
switch {
case j.Refused != "":
t.Errorf("%s: %s", j.Module, j.Refused)
case j.Exempt != "":
t.Logf("%s is exempt: %s", j.Module, j.Exempt)
}
}
}
-3
View File
@@ -1,3 +0,0 @@
module git.novox.be/novox/mesh-catalog/checks/failurekit
go 1.22
-7
View File
@@ -1,7 +0,0 @@
# 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.
# Judge("../.."): every manifest, and the directory of each module claiming a node-build-* seat, walked for a test that
# calls the kit. The build modules are the mesh-build-* directories; a build module named otherwise needs its line here.
modules/*/module.json
modules/mesh-build-*
-292
View File
@@ -1,292 +0,0 @@
// Package gitignored refuses an ignore pattern that hides a module's source (catalogue review F1, and its kind): the
// bare pattern `mesh-build-node` in a module's .gitignore, meant for the binary `go build` leaves, also matched the
// directory cmd/mesh-build-node, so git ignored every new file there and publish.go was never committed. A path under
// a module's cmd/ or internal/ is source: no .gitignore, the module's or the catalogue's, may ignore one, tracked or
// new. A binary is ignored by an anchored pattern (`/mesh-build-node`, `/cmd/mesh-build-node/mesh-build-node`).
package gitignored
import (
"bytes"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
)
// Probe is the file name asked of each source directory: a Go file a change might add there.
const Probe = "added_by_a_change.go"
// Hidden is one source path an ignore pattern hides, and the pattern (its file, line and text, as git names it).
type Hidden struct {
Path, Pattern string
}
// Sources are the source directories of root's modules, relative to root: every directory under modules/*/cmd and
// modules/*/internal (node_modules left out).
func Sources(root string) ([]string, error) {
mods, err := filepath.Glob(filepath.Join(root, "modules", "*"))
if err != nil {
return nil, err
}
var out []string
for _, m := range mods {
for _, top := range []string{"cmd", "internal"} {
base := filepath.Join(m, top)
if st, err := os.Stat(base); err != nil || !st.IsDir() {
continue
}
err := filepath.WalkDir(base, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
return nil
}
if d.Name() == "node_modules" {
return filepath.SkipDir
}
rel, _ := filepath.Rel(root, p)
out = append(out, filepath.ToSlash(rel))
return nil
})
if err != nil {
return nil, err
}
}
}
sort.Strings(out)
return out, nil
}
// isSource is whether a file is source a module's build reads: Go and TypeScript files and a Go module's own files.
// What a build or a test script leaves beside them (a binary, compiled JavaScript) may be ignored.
func isSource(name string) bool {
switch filepath.Ext(name) {
case ".go", ".ts", ".tsx":
return true
}
return name == "go.mod" || name == "go.sum"
}
// Find asks git which of the source paths root's ignore files hide: each source directory, a Go file added in it, and
// every source file there (isSource). It reads the .gitignore files of root's tree through a repository of its own, so it judges a git
// checkout and a plain folder alike, and no one's global ignores count.
func Find(root string, dirs []string) ([]Hidden, error) {
gitDir, err := os.MkdirTemp("", "gitignored-")
if err != nil {
return nil, err
}
defer os.RemoveAll(gitDir)
if out, err := exec.Command("git", "init", "-q", "--bare", gitDir).CombinedOutput(); err != nil {
return nil, fmt.Errorf("git init: %v: %s", err, out)
}
var ask bytes.Buffer
for _, d := range dirs {
ask.WriteString(d + "/\x00" + d + "/" + Probe + "\x00")
entries, err := os.ReadDir(filepath.Join(root, d))
if err != nil {
return nil, err
}
for _, e := range entries {
if !e.IsDir() && isSource(e.Name()) {
ask.WriteString(d + "/" + e.Name() + "\x00")
}
}
}
abs, err := filepath.Abs(root)
if err != nil {
return nil, err
}
cmd := exec.Command("git", "--git-dir="+gitDir, "--work-tree="+abs, "-c", "core.excludesFile=/dev/null",
"check-ignore", "--no-index", "-v", "-z", "--stdin")
cmd.Dir = abs
cmd.Stdin = &ask
out, err := cmd.Output()
if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
return nil, nil // nothing is ignored
}
if err != nil {
return nil, fmt.Errorf("git check-ignore: %w", err)
}
// -v -z answers source, line, pattern and path, each ended by NUL.
f := strings.Split(strings.TrimRight(string(out), "\x00"), "\x00")
var hidden []Hidden
for i := 0; i+3 < len(f); i += 4 {
if f[i+2] == "" || strings.HasPrefix(f[i+2], "!") {
continue // a negation keeps the path
}
hidden = append(hidden, Hidden{Path: f[i+3], Pattern: fmt.Sprintf("%s:%s: %s", f[i], f[i+1], f[i+2])})
}
return hidden, nil
}
// Programs are the paths `go build` writes a compiled program to, for every main package of every Go module under
// root (issue 1577, met twice in one milestone: a module's program committed with `git add -A`): `go build` in the
// package's directory writes it there, and `go build ./<dir>` from the module's root writes it at the root, each named
// after the package's import path. Relative to root, sorted; directories named node_modules, testdata or vendor, and
// .git, are left out, and so is a path that is a directory of the tree.
func Programs(root string) ([]string, error) {
var mods []string
err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() && skipped(d.Name()) && p != root {
return filepath.SkipDir
}
if !d.IsDir() && d.Name() == "go.mod" {
mods = append(mods, filepath.Dir(p))
}
return nil
})
if err != nil {
return nil, err
}
seen := map[string]bool{}
for _, m := range mods {
modPath, err := modulePath(filepath.Join(m, "go.mod"))
if err != nil {
return nil, err
}
err = filepath.WalkDir(m, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
return nil
}
if p != m {
if skipped(d.Name()) {
return filepath.SkipDir
}
if _, err := os.Stat(filepath.Join(p, "go.mod")); err == nil {
return filepath.SkipDir // a module of its own, walked on its own
}
}
isMain, err := mainPackage(p)
if err != nil || !isMain {
return err
}
name := filepath.Base(p)
if p == m {
name = pathBase(modPath)
}
for _, dir := range []string{p, m} {
out := filepath.Join(dir, name)
if st, err := os.Stat(out); err == nil && st.IsDir() {
continue // go build refuses to write over a directory, so no program lands there
}
rel, _ := filepath.Rel(root, out)
seen[filepath.ToSlash(rel)] = true
}
return nil
})
if err != nil {
return nil, err
}
}
out := make([]string, 0, len(seen))
for p := range seen {
out = append(out, p)
}
sort.Strings(out)
return out, nil
}
// Unignored names each of programs (paths from root) that root's ignore files do not ignore: a program a local build
// leaves there that `git add -A` would commit.
func Unignored(root string, programs []string) ([]string, error) {
gitDir, err := os.MkdirTemp("", "gitignored-")
if err != nil {
return nil, err
}
defer os.RemoveAll(gitDir)
if out, err := exec.Command("git", "init", "-q", "--bare", gitDir).CombinedOutput(); err != nil {
return nil, fmt.Errorf("git init: %v: %s", err, out)
}
abs, err := filepath.Abs(root)
if err != nil {
return nil, err
}
cmd := exec.Command("git", "--git-dir="+gitDir, "--work-tree="+abs, "-c", "core.excludesFile=/dev/null",
"check-ignore", "--no-index", "-v", "-n", "-z", "--stdin")
cmd.Dir = abs
cmd.Stdin = strings.NewReader(strings.Join(programs, "\x00") + "\x00")
out, err := cmd.Output()
if ee, ok := err.(*exec.ExitError); !(err == nil || ok && ee.ExitCode() == 1) {
return nil, fmt.Errorf("git check-ignore: %w", err)
}
// -v -n -z answers source, line, pattern and path for every path asked; the source is empty for one no pattern
// matches, and a pattern starting with ! keeps the path.
f := strings.Split(strings.TrimRight(string(out), "\x00"), "\x00")
var open []string
for i := 0; i+3 < len(f); i += 4 {
if f[i+2] == "" || strings.HasPrefix(f[i+2], "!") {
open = append(open, f[i+3])
}
}
return open, nil
}
func skipped(name string) bool {
switch name {
case ".git", "node_modules", "testdata", "vendor":
return true
}
return false
}
// modulePath reads the module line of a go.mod.
func modulePath(goMod string) (string, error) {
b, err := os.ReadFile(goMod)
if err != nil {
return "", err
}
for _, l := range strings.Split(string(b), "\n") {
if f := strings.Fields(l); len(f) >= 2 && f[0] == "module" {
return strings.Trim(f[1], `"`), nil
}
}
return "", fmt.Errorf("%s names no module", goMod)
}
// pathBase is the name `go build` gives a program at a module's root: its path's last element, a major version
// suffix (/v2) left out.
func pathBase(modPath string) string {
parts := strings.Split(modPath, "/")
last := parts[len(parts)-1]
if len(parts) > 1 && len(last) > 1 && last[0] == 'v' && strings.Trim(last[1:], "0123456789") == "" {
last = parts[len(parts)-2]
}
return last
}
// mainPackage is whether dir holds a Go file, not a test, of package main.
func mainPackage(dir string) (bool, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return false, err
}
for _, e := range entries {
n := e.Name()
if e.IsDir() || filepath.Ext(n) != ".go" || strings.HasSuffix(n, "_test.go") {
continue
}
b, err := os.ReadFile(filepath.Join(dir, n))
if err != nil {
return false, err
}
for _, l := range strings.Split(string(b), "\n") {
if f := strings.Fields(l); len(f) >= 2 && f[0] == "package" {
if f[1] == "main" {
return true, nil
}
break
}
}
}
return false, nil
}
-158
View File
@@ -1,158 +0,0 @@
package gitignored
import (
"os"
"path/filepath"
"strings"
"testing"
)
// The kind of catalogue review F1: a bare pattern hides the command directory it shares a name with; an anchored one
// hides the binary alone; a glob that ignores source is refused too; a negation keeps a path.
func TestFindNamesEachPatternThatHidesSource(t *testing.T) {
for name, c := range map[string]struct {
ignore string
hidden []string
}{
"a bare name hides cmd/<name>": {"nfs-server\n", []string{"modules/m/cmd/nfs-server/", "modules/m/cmd/nfs-server/" + Probe, "modules/m/cmd/nfs-server/main.go"}},
"anchored to the binary": {"/nfs-server\n/cmd/nfs-server/nfs-server\n", nil},
"a glob ignoring Go source": {"*.go\n!main.go\n", []string{"modules/m/cmd/" + Probe, "modules/m/cmd/nfs-server/" + Probe,
"modules/m/internal/" + Probe, "modules/m/internal/x/" + Probe, "modules/m/internal/x/x.go"}},
"a pattern over internal/": {"internal/\n", []string{"modules/m/internal/", "modules/m/internal/" + Probe, "modules/m/internal/x/",
"modules/m/internal/x/" + Probe, "modules/m/internal/x/x.go"}},
"nothing but generated JavaScript": {"*.js\n", nil},
} {
root := t.TempDir()
for f, b := range map[string]string{
"modules/m/.gitignore": c.ignore,
"modules/m/cmd/nfs-server/main.go": "package main\n",
"modules/m/cmd/nfs-server/run.js": "",
"modules/m/internal/x/x.go": "package x\n",
"modules/m/node_modules/y/index.go": "",
"modules/other/cmd/other/other.go": "package main\n",
} {
p := filepath.Join(root, f)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(b), 0o644); err != nil {
t.Fatal(err)
}
}
dirs, err := Sources(root)
if err != nil {
t.Fatal(err)
}
got, err := Find(root, dirs)
if err != nil {
t.Fatal(err)
}
var paths []string
for _, h := range got {
paths = append(paths, h.Path)
if !strings.HasPrefix(h.Pattern, "modules/m/.gitignore:") {
t.Errorf("%s: %s is named hidden by %q, want the module's .gitignore", name, h.Path, h.Pattern)
}
}
if strings.Join(paths, ",") != strings.Join(c.hidden, ",") {
t.Errorf("%s: hidden %v, want %v", name, paths, c.hidden)
}
}
}
// The catalogue's own tree: no ignore pattern hides a path under a module's cmd/ or internal/.
func TestNoIgnorePatternHidesAModulesSource(t *testing.T) {
root := filepath.Join("..", "..")
dirs, err := Sources(root)
if err != nil {
t.Fatal(err)
}
if len(dirs) == 0 {
t.Fatal("no module source directory found: the check reads nothing")
}
hidden, err := Find(root, dirs)
if err != nil {
t.Fatal(err)
}
for _, h := range hidden {
t.Errorf("%s is hidden by %s: a module's source is never ignored; anchor the pattern to the binary (/<name>, /cmd/<name>/<name>)", h.Path, h.Pattern)
}
}
// The kind of issue 1577, met twice in one milestone: a module's program `go build` left in its folder was committed.
// Every program a main package builds, in its own directory and at its module's root, is found, and one no ignore file
// names is open; a directory of the same name is no program's place.
func TestEveryProgramABuildLeavesIsIgnored(t *testing.T) {
files := map[string]string{
"modules/m/go.mod": "module example.com/m/v2\n",
"modules/m/main.go": "// a program\npackage main\n",
"modules/m/cmd/m-tools/main.go": "package main\n",
"modules/m/cmd/m-tools/main_test.go": "package main\n",
"modules/m/internal/x/x.go": "package x\n",
"modules/m/internal/x/x_test.go": "package main\n",
"modules/m/node_modules/y/y.go": "package main\n",
"modules/m/tool/go.mod": "module tool\n",
"modules/m/tool/cmd/tool/main.go": "package main\n",
"modules/m/tool/tool/source.go": "package tool\n",
"checks/c/go.mod": "module example.com/checks/c\n",
"checks/c/main.go": "package main\n",
}
for name, c := range map[string]struct {
ignore string
open []string
}{
"nothing ignored": {"", []string{"checks/c/c", "modules/m/cmd/m-tools/m-tools", "modules/m/m", "modules/m/m-tools",
"modules/m/tool/cmd/tool/tool"}},
"each anchored": {"/checks/c/c\n/modules/m/cmd/m-tools/m-tools\n/modules/m/m\n/modules/m/m-tools\n/modules/m/tool/cmd/tool/tool\n", nil},
"one kept by a negation": {"/checks/c/c\n/modules/m/*\n!/modules/m/m\n/modules/m/cmd/m-tools/m-tools\n/modules/m/tool/cmd/tool/tool\n",
[]string{"modules/m/m"}},
} {
root := t.TempDir()
for f, b := range files {
p := filepath.Join(root, f)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(b), 0o644); err != nil {
t.Fatal(err)
}
}
if err := os.WriteFile(filepath.Join(root, ".gitignore"), []byte(c.ignore), 0o644); err != nil {
t.Fatal(err)
}
programs, err := Programs(root)
if err != nil {
t.Fatal(err)
}
want := "checks/c/c,modules/m/cmd/m-tools/m-tools,modules/m/m,modules/m/m-tools,modules/m/tool/cmd/tool/tool"
if got := strings.Join(programs, ","); got != want {
t.Fatalf("%s: programs %s, want %s", name, got, want)
}
open, err := Unignored(root, programs)
if err != nil {
t.Fatal(err)
}
if strings.Join(open, ",") != strings.Join(c.open, ",") {
t.Errorf("%s: open %v, want %v", name, open, c.open)
}
}
}
// The catalogue's own tree: every program a local build leaves is ignored, so `git add -A` never takes one.
func TestNoProgramABuildLeavesIsCommitted(t *testing.T) {
root := filepath.Join("..", "..")
programs, err := Programs(root)
if err != nil {
t.Fatal(err)
}
if len(programs) == 0 {
t.Fatal("no main package found: the check reads nothing")
}
open, err := Unignored(root, programs)
if err != nil {
t.Fatal(err)
}
for _, p := range open {
t.Errorf("%s is the program a local go build leaves, and no ignore file names it: add /%s to the catalogue's .gitignore", p, p)
}
}
-3
View File
@@ -1,3 +0,0 @@
module git.novox.be/novox/mesh-catalog/checks/gitignored
go 1.22
-3
View File
@@ -1,3 +0,0 @@
module git.novox.be/novox/mesh-catalog/checks/gofmt
go 1.22
-58
View File
@@ -1,58 +0,0 @@
// Package gofmt refuses a Go file of the catalogue that gofmt would change (the second of a kind in one milestone:
// buildstep's generated.go and claude-code's config_test.go reached a join unformatted). A check here holds every
// module to it, where a module's own tests hold only themselves.
package gofmt
import (
"bytes"
"go/format"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
)
// Files are the Go files under root, relative to it: every .go file but those in .git, node_modules and a testdata
// directory (a test's fixture may be anything).
func Files(root string) ([]string, error) {
var out []string
err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
switch d.Name() {
case ".git", "node_modules", "testdata":
return filepath.SkipDir
}
return nil
}
if strings.HasSuffix(d.Name(), ".go") && d.Type().IsRegular() {
rel, _ := filepath.Rel(root, p)
out = append(out, filepath.ToSlash(rel))
}
return nil
})
sort.Strings(out)
return out, err
}
// Unformatted are the files gofmt would change, and why for one that does not parse.
func Unformatted(root string, files []string) (map[string]string, error) {
out := map[string]string{}
for _, f := range files {
src, err := os.ReadFile(filepath.Join(root, f))
if err != nil {
return nil, err
}
got, err := format.Source(src)
switch {
case err != nil:
out[f] = "it does not parse: " + err.Error()
case !bytes.Equal(got, src):
out[f] = "gofmt changes it: run gofmt -w " + f
}
}
return out, nil
}
-53
View File
@@ -1,53 +0,0 @@
package gofmt
import (
"os"
"path/filepath"
"testing"
)
func TestUnformattedNamesWhatGofmtChanges(t *testing.T) {
root := t.TempDir()
for f, src := range map[string]string{
"modules/a/a.go": "package a\n",
"modules/b/b.go": "package b\nimport (\n\t\"os\"\n\t\"fmt\"\n)\nvar _, _ = fmt.Sprint, os.Exit\n",
"modules/c/testdata/c.go": "package c",
"modules/d/node_modules/d.go": "package d",
} {
p := filepath.Join(root, f)
_ = os.MkdirAll(filepath.Dir(p), 0o755)
if err := os.WriteFile(p, []byte(src), 0o644); err != nil {
t.Fatal(err)
}
}
files, err := Files(root)
if err != nil {
t.Fatal(err)
}
got, err := Unformatted(root, files)
if err != nil {
t.Fatal(err)
}
if len(files) != 2 || len(got) != 1 || got["modules/b/b.go"] == "" {
t.Errorf("files %v, unformatted %v; want a.go and b.go read, b.go named", files, got)
}
}
// The catalogue's own Go files are as gofmt writes them.
func TestTheCataloguesGoIsFormatted(t *testing.T) {
root := filepath.Join("..", "..")
files, err := Files(root)
if err != nil {
t.Fatal(err)
}
if len(files) == 0 {
t.Fatal("no Go file found: the check reads nothing")
}
got, err := Unformatted(root, files)
if err != nil {
t.Fatal(err)
}
for f, why := range got {
t.Errorf("%s: %s", f, why)
}
}
+1 -2
View File
@@ -2,5 +2,4 @@ module checks/health
go 1.26.0 go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023150321-a2c813a6b468
+2
View File
@@ -0,0 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023150321-a2c813a6b468 h1:UcMee+IV2ii5E4dRZdQUaSaeh9IedTFYumuQbBiB0k0=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023150321-a2c813a6b468/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
+3 -8
View File
@@ -1,8 +1,7 @@
// Package health is the catalogue's count of long-running resources that do not say how they are ready (novox/hq ADR 0240 rule // Package health is the catalogue's count of long-running resources that do not say how they are ready (novox/hq ADR 0240 rule
// 8), kept in health-undeclared and judged by a test over the real tree (ADR 0529 clauses 2 and 4): it replaces the step of // 8), kept in health-undeclared and judged by a test over the real tree (ADR 0529 clauses 2 and 4): it replaces the step of
// merge-check.sh that read it out of the controller's `module check`. Every manifest is decoded with mesh-sdk's catalogue // merge-check.sh that read it out of the controller's `module check`. Every manifest is read through mesh-sdk's catalogue
// package, the one the controller counts with, and judged by nothing here (novox/hq ADR 0578 §3): a resource's `health` is // package, the one the controller counts with.
// in the manifest itself, not in a template's text.
package health package health
import ( import (
@@ -28,11 +27,7 @@ func Count(root string) (int, error) {
} }
n := 0 n := 0
for _, path := range manifests { for _, path := range manifests {
raw, err := os.ReadFile(path) m, err := catalogue.ReadTreeManifest(root, filepath.Dir(path))
if err != nil {
return 0, err
}
m, err := catalogue.Decode(raw)
if err != nil { if err != nil {
return 0, fmt.Errorf("%s: %w", path, err) return 0, fmt.Errorf("%s: %w", path, err)
} }
-6
View File
@@ -1,6 +0,0 @@
# 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.
# every manifest, decoded; and the root's kept count of resources that do not say how they are ready.
modules/*/module.json
health-undeclared
-5
View File
@@ -1,5 +0,0 @@
# 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.
# Check("../.."): every manifest.
modules/*/module.json
-5
View File
@@ -1,5 +0,0 @@
# 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.
# Check("../.."): every manifest, and whether the path each image artifact builds from is there.
modules
-7
View File
@@ -1,7 +0,0 @@
# 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.
# Check("../.."): every manifest, a TypeScript module's package.json and whether its lock is there.
modules/*/module.json
modules/*/package.json
modules/*/package-lock.json
-5
View File
@@ -1,5 +0,0 @@
# 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.
# Check("../.."): every manifest.
modules/*/module.json
-6
View File
@@ -1,6 +0,0 @@
module checks/manifests
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
View File
-100
View File
@@ -1,100 +0,0 @@
// Package manifests reads every module's manifest as the catalogue's tree holds it and refuses what no judge could
// read: a module.json that does not decode, a module.json giving one field twice, and two manifests that name one module.
//
// It judges no manifest (novox/hq ADR 0578 §2): the checker judges every change's manifests with the indexer's judge, the
// one judge, so the field, seat, bus and catalogue rules are said once, there. Until 2026-11 this check parsed each
// manifest with mesh-sdk's strict parser and ran the rules between manifests (issue 774, power's bar item of
// mesh-catalog ca8d6f33); that judgement moved to the checker, and what stays is the two refusals that are about the
// tree, not a manifest's rules.
package manifests
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Problems is what is wrong with the tree's manifests under root/modules, one line each: a module.json whose bytes are
// no manifest (catalogue.Decode: not JSON, or a field of the wrong shape), one giving a field twice (a decoder keeps the
// last and drops the first without a word: a merge of two branches that each added the field writes it so), then a module
// named by two manifests.
func Problems(root string) ([]string, error) {
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
sort.Strings(files)
var out []string
named := map[string]bool{}
for _, f := range files {
raw, err := os.ReadFile(f)
if err != nil {
return nil, err
}
rel, _ := filepath.Rel(root, f)
m, err := catalogue.Decode(raw)
if err != nil {
out = append(out, fmt.Sprintf("%s: %v", rel, err))
continue
}
if twice := givenTwice(raw); twice != "" {
out = append(out, fmt.Sprintf("%s: the field %s is given twice; a judge reads only the last", rel, twice))
continue
}
if named[m.Module] {
out = append(out, fmt.Sprintf("%s: %s was already given; two manifests name one module", rel, m.Module))
continue
}
named[m.Module] = true
}
return out, nil
}
// givenTwice is the first field an object of raw gives twice, as its path (`invokes`, `build.artifacts[0].env`), or empty.
func givenTwice(raw []byte) string {
d := json.NewDecoder(bytes.NewReader(raw))
var walk func(path string) string
walk = func(path string) string {
t, err := d.Token()
if err != nil {
return ""
}
switch t {
case json.Delim('{'):
seen := map[string]bool{}
for d.More() {
k, err := d.Token()
if err != nil {
return ""
}
key, _ := k.(string)
at := key
if path != "" {
at = path + "." + key
}
if seen[key] {
return at
}
seen[key] = true
if twice := walk(at); twice != "" {
return twice
}
}
_, _ = d.Token()
case json.Delim('['):
for i := 0; d.More(); i++ {
if twice := walk(fmt.Sprintf("%s[%d]", path, i)); twice != "" {
return twice
}
}
_, _ = d.Token()
}
return ""
}
return walk("")
}
-98
View File
@@ -1,98 +0,0 @@
package manifests
import (
"os"
"path/filepath"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// The tree's manifests are ones a judge can read, and no module is named twice. A row is a catalogue of its own: a
// manifest that is no JSON, one giving a field twice and two manifests of one module are refused, naming the file; a manifest with a field this
// check does not know is not (it judges nothing: the checker does, ADR 0578 §2). The catalogue itself passes.
func TestEveryManifestDecodesAndNamesOneModule(t *testing.T) {
for _, row := range []struct {
name string
files map[string]string
refused string
}{
{name: "a manifest the judge can read", files: map[string]string{"a": `{"module":"a","version":"1"}`}},
{name: "a field no part knows is the judge's to refuse", files: map[string]string{"a": `{"module":"a","version":"1","unheard":true}`}},
{name: "bytes that are no manifest", files: map[string]string{"a": `{"module":`}, refused: "modules/a/module.json"},
{name: "a field given twice", files: map[string]string{"a": `{"module":"a","version":"1","invokes":["x.y"],"build":{"artifacts":[{}]},"invokes":["z.w"]}`},
refused: "modules/a/module.json: the field invokes is given twice"},
{name: "a field given twice deeper", files: map[string]string{"a": `{"module":"a","version":"1","build":{"artifacts":[{"name":"t","name":"u"}]}}`},
refused: "the field build.artifacts[0].name is given twice"},
{name: "two manifests of one module", files: map[string]string{"a": `{"module":"x","version":"1"}`, "b": `{"module":"x","version":"1"}`},
refused: "two manifests name one module"},
} {
root := t.TempDir()
for dir, manifest := range row.files {
if err := os.MkdirAll(filepath.Join(root, "modules", dir), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "modules", dir, "module.json"), []byte(manifest), 0o644); err != nil {
t.Fatal(err)
}
}
got, err := Problems(root)
if err != nil {
t.Fatal(err)
}
joined := strings.Join(got, "\n")
if (row.refused == "") != (len(got) == 0) || !strings.Contains(joined, row.refused) {
t.Errorf("%s: refused %q, want %q", row.name, joined, row.refused)
}
}
real, err := Problems(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
if len(real) != 0 {
t.Fatalf("the catalogue holds manifests no judge can read:\n%s", strings.Join(real, "\n"))
}
}
// No manifest of this catalogue claims the seat mesh-catalog: the indexer of the delivery holds it, and the module that
// held it before has dropped its claim (ADR 0604 §6). A row is a tree of its own: a manifest claiming it is named.
func TestNoManifestClaimsTheSeatMeshCatalog(t *testing.T) {
claimants := func(root string) []string {
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
t.Fatal(err)
}
var out []string
for _, f := range files {
raw, err := os.ReadFile(f)
if err != nil {
t.Fatal(err)
}
m, err := catalogue.Decode(raw)
if err != nil {
continue // Problems names it
}
for _, c := range m.Claims {
if c.Name == "mesh-catalog" {
out = append(out, m.Module)
}
}
}
return out
}
row := t.TempDir()
if err := os.MkdirAll(filepath.Join(row, "modules", "a"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(row, "modules", "a", "module.json"),
[]byte(`{"module":"a","version":"1","claims":[{"name":"mesh-catalog","scope":"mesh"}]}`), 0o644); err != nil {
t.Fatal(err)
}
if got := claimants(row); len(got) != 1 || got[0] != "a" {
t.Fatalf("a manifest claiming the seat is named, got %v", got)
}
if got := claimants(filepath.Join("..", "..")); len(got) != 0 {
t.Errorf("the catalogue's manifests %v claim the seat mesh-catalog: the indexer of the delivery holds it", got)
}
}
-3
View File
@@ -1,3 +0,0 @@
# What these tests read outside their package, as paths from the repository's root (issue 1936).
# every module's directory (its manifest), decoded.
modules
-3
View File
@@ -1,3 +0,0 @@
module git.novox.be/novox/mesh-catalog/checks/nobinaries
go 1.22
-84
View File
@@ -1,84 +0,0 @@
// Package nobinaries refuses a compiled program anywhere in the repository (issue 1577; first a checks/ binary slipped past a modules/-only rule): it is build output, made by the module's build step,
// and `go build` in a module's folder writes one beside go.mod where `git add -A` takes it.
package nobinaries
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
)
// Kind says what executable format the first bytes of a file are, or "" when they are none.
func Kind(head []byte) string {
switch {
case bytes.HasPrefix(head, []byte("\x7fELF")):
return "ELF"
case bytes.HasPrefix(head, []byte("MZ")):
return "PE"
case bytes.HasPrefix(head, []byte{0xcf, 0xfa, 0xed, 0xfe}), bytes.HasPrefix(head, []byte{0xce, 0xfa, 0xed, 0xfe}),
bytes.HasPrefix(head, []byte{0xfe, 0xed, 0xfa, 0xcf}), bytes.HasPrefix(head, []byte{0xfe, 0xed, 0xfa, 0xce}),
bytes.HasPrefix(head, []byte{0xca, 0xfe, 0xba, 0xbe}) && len(head) >= 8 && head[7] < 0x20 && head[6] == 0 && head[5] == 0 && head[4] == 0:
return "Mach-O"
}
return ""
}
// Files are the files of the catalogue's root that the change holds, anywhere: the tracked ones when the root is a git
// checkout (an ignored binary a local build left is not the change's), else every file there but .git's.
func Files(root string) ([]string, error) {
cmd := exec.Command("git", "-C", root, "ls-files", "-z")
if out, err := cmd.Output(); err == nil && len(out) > 0 {
var files []string
for _, f := range strings.Split(strings.TrimRight(string(out), "\x00"), "\x00") {
if _, err := os.Lstat(filepath.Join(root, f)); err == nil { // a file the change deletes is not there
files = append(files, f)
}
}
return files, nil
}
var files []string
err := filepath.WalkDir(root, func(p string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() && d.Name() == ".git" {
return filepath.SkipDir
}
if d.IsDir() || !d.Type().IsRegular() {
return nil
}
rel, _ := filepath.Rel(root, p)
files = append(files, rel)
return nil
})
return files, err
}
// Find names each file of files (paths from root) that is a compiled program, with its format.
func Find(root string, files []string) ([]string, error) {
var found []string
for _, f := range files {
p := filepath.Join(root, f)
if st, err := os.Lstat(p); err != nil || !st.Mode().IsRegular() {
continue
}
fh, err := os.Open(p)
if err != nil {
return nil, err
}
head := make([]byte, 8)
n, err := io.ReadFull(fh, head)
fh.Close()
if err != nil && err != io.ErrUnexpectedEOF && err != io.EOF {
return nil, err
}
if k := Kind(head[:n]); k != "" {
found = append(found, fmt.Sprintf("%s (%s)", f, k))
}
}
return found, nil
}
-89
View File
@@ -1,89 +0,0 @@
package nobinaries
import (
"os"
"os/exec"
"path/filepath"
"testing"
)
func TestFindNamesEachCompiledProgramAndNothingElse(t *testing.T) {
root := t.TempDir()
files := map[string][]byte{
"modules/a/a": []byte("\x7fELF\x02\x01\x01\x00rest"),
"modules/b/cmd/b/b.exe": []byte("MZ\x90\x00"),
"modules/c/c": {0xcf, 0xfa, 0xed, 0xfe, 7, 0, 0, 1},
"modules/d/main.go": []byte("package main\n"),
"modules/e/short": []byte("\x7f"),
"modules/f/empty": {},
"modules/g/Foo.class": {0xca, 0xfe, 0xba, 0xbe, 0, 0, 0, 52}, // Java, not Mach-O
"checks/words/words": []byte("\x7fELF\x02\x01\x01\x00rest"), // outside modules/ too
}
var names []string
for f, b := range files {
p := filepath.Join(root, f)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, b, 0o644); err != nil {
t.Fatal(err)
}
names = append(names, f)
}
got, err := Find(root, names)
if err != nil {
t.Fatal(err)
}
if len(got) != 4 {
t.Fatalf("found %v; wanted modules/a/a, modules/b/cmd/b/b.exe and modules/c/c and checks/words/words only", got)
}
}
// The catalogue's own tree holds no compiled program: a compiled program is build output, so remove it and add it to .gitignore.
func TestTheCatalogueHoldsNoCompiledProgram(t *testing.T) {
root := filepath.Join("..", "..")
files, err := Files(root)
if err != nil {
t.Fatal(err)
}
found, err := Find(root, files)
if err != nil {
t.Fatal(err)
}
for _, f := range found {
t.Errorf("%s: a compiled program is build output: remove it and add it to .gitignore (issue 1577)", f)
}
}
// A compiled program outside modules/ is refused too (a binary under checks/ slipped past the modules/-only rule): Files holds
// every tracked file of a git checkout and every file of a plain folder, and Find names the binary in both.
func TestAProgramUnderChecksIsRefused(t *testing.T) {
for _, git := range []bool{true, false} {
root := t.TempDir()
p := filepath.Join(root, "checks", "words", "words")
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte("\x7fELF\x02\x01\x01\x00rest"), 0o644); err != nil {
t.Fatal(err)
}
if git {
for _, args := range [][]string{{"init", "-q"}, {"add", "-A"}} {
if out, err := exec.Command("git", append([]string{"-C", root}, args...)...).CombinedOutput(); err != nil {
t.Fatalf("git %v: %v: %s", args, err, out)
}
}
}
files, err := Files(root)
if err != nil {
t.Fatal(err)
}
found, err := Find(root, files)
if err != nil {
t.Fatal(err)
}
if len(found) != 1 {
t.Errorf("git=%v: found %v; wanted checks/words/words (ELF)", git, found)
}
}
}
-1
View File
@@ -1 +0,0 @@
# every tracked file of the repository (git ls-files, no path limit): the whole tree, so this package's verdict is never kept.
-3
View File
@@ -1,3 +0,0 @@
module git.novox.be/novox/mesh-catalog/checks/noplacement
go 1.22
-107
View File
@@ -1,107 +0,0 @@
// Command noplacement refuses what the repository path of placement left behind (novox/hq ADR 0567 §10, §12).
//
// Placement, which module runs on which node, is bus state held by the seat mesh-module-assigner; 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 `particulars` file at the root (the mesh's node names come from the checker's run, §16);
//
// 3. a manifest naming a state, stream, event or bucket of the indexer's placement (`mesh-delivery-indexer.placement…`
// or `mesh-delivery-indexer_placement…`), or the assigner's retired state placement (`mesh-delivery-assigner.placement…`,
// `mesh-module-assigner.placement…`, its views and its read tool), in any string value, such as `reads`;
//
// The controller's assignments, which item 4 once listed readers of, are refused with every other name of the
// controller by checks/controllerdeps (novox/hq ADR 0590 §5).
//
// go run . <catalogue root>
//
// 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 and the retired names of the assigner's state, now
// assignments (novox/hq ADR 0641); a manifest string starting with one names them.
var Prefixes = []string{"mesh-delivery-indexer.placement", "mesh-delivery-indexer_placement",
"mesh-delivery-assigner.placement", "mesh-delivery-assigner_placement", "mesh-delivery-assigner.views.placement",
"mesh-delivery-assigner.assigner_read_placement", "mesh-module-assigner.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-module-assigner (novox/hq ADR 0567 §10)")
}
if _, err := os.Lstat(filepath.Join(root, "particulars")); err == nil {
out = append(out, "particulars: the repository lists no node names; the checker hands them to the check (novox/hq ADR 0567 §16)")
}
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, a name that is retired: the indexer keeps no placement (novox/hq ADR 0567 §12) and the seat mesh-module-assigner's state is assignments (novox/hq ADR 0641)", module, s))
}
}
sort.Strings(out)
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 <catalogue root>")
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 names the retired placement")
}
-59
View File
@@ -1,59 +0,0 @@
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-delivery-assigner.assignments"]}`}, ""},
{"a placement directory", map[string]string{"placement/ace.json": `{}`}, "placement: the repository holds no placement"},
{"a particulars file", map[string]string{"particulars": "ace\n"}, "particulars: the repository lists no node names"},
{"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-module-assigner.assignments"]}`}, ""},
{"the assigner's retired state", map[string]string{"modules/e/module.json": `{"module":"e","reads":["mesh-delivery-assigner.placement"]}`}, `e: names "mesh-delivery-assigner.placement"`},
{"its retired views family", map[string]string{"modules/f/module.json": `{"module":"f","consumes":["mesh-delivery-assigner.views.placement"]}`}, "views.placement"},
{"its retired read tool", map[string]string{"modules/g/module.json": `{"module":"g","invokes":["mesh-delivery-assigner.assigner_read_placement"]}`}, "assigner_read_placement"},
{"the seat's retired verb", map[string]string{"modules/h/module.json": `{"module":"h","invokes":["mesh-module-assigner.placement"]}`}, "mesh-module-assigner.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)
}
}
-6
View File
@@ -1,6 +0,0 @@
# 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
-7
View File
@@ -1,7 +0,0 @@
# 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.
# that none of the old check scripts stands at the root.
?merge-check.sh
?merge-check-typescript.sh
?check-nested-modules.sh
+1 -1
View File
@@ -2,6 +2,6 @@ module git.novox.be/novox/mesh-catalog/checks/owned
go 1.22 go 1.22
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0-00010101000000-000000000000 require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
-5
View File
@@ -1,5 +0,0 @@
# 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.
# Check("../.."): every manifest; its lists are this package's own files.
modules/*/module.json
+7 -22
View File
@@ -2,9 +2,8 @@
// issue 1838): a module is for any mesh, so no manifest, file, code string, identifier, comment, test name or // issue 1838): a module is for any mesh, so no manifest, file, code string, identifier, comment, test name or
// fixture of a covered module names a node of this one. // fixture of a covered module names a node of this one.
// //
// The names are the mesh's own, not the repository's: the checker hands them in the environment value // The names are in the file `particulars` at the catalogue's root, one a line, `#` comments — the one place
// MESH_NODE_NAMES, comma-separated, from the node records it reads (novox/hq ADR 0567 §16). A run with none fails // they may be written. The modules covered are in `covered` at checks/particulars/covered, one a line. A name is matched
// loudly, since it would pass on anything. The modules covered are in `covered` at checks/particulars/covered, one a line. A name is matched
// whole and in any case; a hyphen, an underscore or a CamelCase boundary parts one, so a longer word holding a name is another word. // whole and in any case; a hyphen, an underscore or a CamelCase boundary parts one, so a longer word holding a name is another word.
// The forge is not a node name: its host (git.novox.be) and the organisation path (novox/<repo>) are blanked // The forge is not a node name: its host (git.novox.be) and the organisation path (novox/<repo>) are blanked
// before a line is read. // before a line is read.
@@ -35,28 +34,14 @@ func main() {
if len(os.Args) > 1 { if len(os.Args) > 1 {
root = os.Args[1] root = os.Args[1]
} }
os.Exit(check(root, namesFromEnv())) os.Exit(check(root))
}
// NamesEnv is the environment value that carries the mesh's node names (mesh-sdk delivery.NodeNamesEnv).
const NamesEnv = "MESH_NODE_NAMES"
// namesFromEnv is the names NamesEnv holds, comma-separated, trimmed, empty entries left out.
func namesFromEnv() []string {
var out []string
for _, n := range strings.Split(os.Getenv(NamesEnv), ",") {
if n = strings.TrimSpace(n); n != "" {
out = append(out, n)
}
}
return out
} }
// check runs the check over a catalogue and returns the exit code: 0 clean, 1 a finding, 2 a list unreadable. // check runs the check over a catalogue and returns the exit code: 0 clean, 1 a finding, 2 a list unreadable.
func check(root string, names []string) int { func check(root string) int {
var err error names, err := readList(filepath.Join(root, "particulars"))
if len(names) == 0 { if err == nil && len(names) == 0 {
err = fmt.Errorf("no node names (%s is empty) — the check would pass on anything", NamesEnv) err = fmt.Errorf("particulars lists no name — the check would pass on anything")
} }
var covered []string var covered []string
if err == nil { if err == nil {
+8 -9
View File
@@ -74,24 +74,26 @@ func TestANodeNamedPathIsRefusedThroughRun(t *testing.T) {
} }
} }
func TestCheckReturnsOneOnAFindingAndTwoOnNoNames(t *testing.T) { func TestCheckReturnsOneOnAFindingAndTwoOnAnUnreadableList(t *testing.T) {
root := t.TempDir() root := t.TempDir()
write := func(p, s string) { write := func(p, s string) {
p = filepath.Join(root, p) p = filepath.Join(root, p)
os.MkdirAll(filepath.Dir(p), 0o755) os.MkdirAll(filepath.Dir(p), 0o755)
os.WriteFile(p, []byte(s), 0o644) os.WriteFile(p, []byte(s), 0o644)
} }
write("particulars", "ace\n")
write("checks/particulars/covered", "kernel\n") write("checks/particulars/covered", "kernel\n")
write("modules/kernel/a", "clean\n") write("modules/kernel/a", "clean\n")
if got := check(root, []string{"ace"}); got != 0 { if got := check(root); got != 0 {
t.Errorf("clean = %d, want 0", got) t.Errorf("clean = %d, want 0", got)
} }
write("modules/kernel/a", "on ace\n") write("modules/kernel/a", "on ace\n")
if got := check(root, []string{"ace"}); got != 1 { if got := check(root); got != 1 {
t.Errorf("finding = %d, want 1", got) t.Errorf("finding = %d, want 1", got)
} }
if got := check(root, nil); got != 2 { os.Remove(filepath.Join(root, "particulars"))
t.Errorf("no names = %d, want 2", got) if got := check(root); got != 2 {
t.Errorf("no list = %d, want 2", got)
} }
} }
@@ -115,10 +117,7 @@ func TestOnlyCoveredModulesAreRead(t *testing.T) {
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): no node name of this mesh in a covered module. // The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): no node name of this mesh in a covered module.
func TestTheCoveredModulesNameNoNode(t *testing.T) { func TestTheCoveredModulesNameNoNode(t *testing.T) {
if _, set := os.LookupEnv(NamesEnv); !set { if code := check("../.."); code != 0 {
t.Skip("ADR 0567 §16: this checker hands no node names (before D14 is delivered); the skip goes once it is")
}
if code := check("../..", namesFromEnv()); code != 0 {
t.Fatalf("the check of the real catalogue exits %d (see what it printed above)", code) t.Fatalf("the check of the real catalogue exits %d (see what it printed above)", code)
} }
} }
-5
View File
@@ -1,5 +0,0 @@
# 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.
# check("../.."): every file of the modules the covered list names. The node names come from the run (MESH_NODE_NAMES).
modules
+1 -1
View File
@@ -2,6 +2,6 @@ module git.novox.be/novox/mesh-catalog/checks/paths
go 1.22 go 1.22
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0-00010101000000-000000000000 require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
+1 -2
View File
@@ -251,8 +251,7 @@ func main() {
} }
var grown []string var grown []string
if baseDir == "" { if baseDir == "" {
fmt.Fprintln(os.Stderr, "NOT COMPARED: no base branch's lists were given (-base <dir>), so additions to them were not judged; that is a failure, not a pass (issue 1295)") fmt.Println("NOT COMPARED: no base branch's lists were given, so additions to them were not judged")
os.Exit(2)
} else { } else {
baseShared, _, err1 := list(filepath.Join(baseDir, "shared")) baseShared, _, err1 := list(filepath.Join(baseDir, "shared"))
baseKnown, _, err2 := list(filepath.Join(baseDir, "known")) baseKnown, _, err2 := list(filepath.Join(baseDir, "known"))
-5
View File
@@ -1,5 +0,0 @@
# 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.
# Check("../.."): every manifest and every file of a module.
modules
+2 -2
View File
@@ -32,8 +32,8 @@ func TestNoModuleNamesAnothersState(t *testing.T) {
} }
} }
// And neither list gains a line the trunk's copies at the merge base do not hold, unless the line says why after `# allow:`. Fails, saying // And neither list gains a line the trunk's copies at the merge base do not hold, unless the line says why after `# allow:`. Skipped,
// NOT COMPARED, where no trunk is beside (issue 1295). // and said, where no trunk is beside.
func TestTheListsOfPathsHoldNoLineTheTrunksDoNot(t *testing.T) { func TestTheListsOfPathsHoldNoLineTheTrunksDoNot(t *testing.T) {
sharedPath, okShared := trunk.Copy(t, "checks/paths/shared") sharedPath, okShared := trunk.Copy(t, "checks/paths/shared")
knownPath, okKnown := trunk.Copy(t, "checks/paths/known") knownPath, okKnown := trunk.Copy(t, "checks/paths/known")
-3
View File
@@ -1,3 +0,0 @@
module checks/planhands
go 1.26.0
-48
View File
@@ -1,48 +0,0 @@
// Package planhands refuses a module's line that takes a plan existing for its module handed to the provisioner (issue
// 774). Whether a plan hands its module is mesh-sdk's one rule, catalogue.Plan.Hands: since the seat mesh-provisioner keys a
// provision its provider's helper answers for a module not handed, a plan naming provisions alone exists for modules
// whose files and own secrets stay their node's (on 2026-10-30 the vault read every such plan as handed and answered
// twenty modules' own secrets to nobody, and the provisioner client refused each as a plan it could not act on). A line
// that tests whether a plan exists either asks Hands on the same line, or says "not handing": it reads only who binds
// whom, or that there is no plan at all.
package planhands
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// existence is a test of whether an assignment's or a plan's `provisioning` is there: nil, or an empty raw value.
var existence = regexp.MustCompile(`Provisioning\s*[!=]=\s*nil|len\([^)]*Provisioning\)\s*[!=]=\s*0`)
// Problems is each such line under root's modules/ that neither asks Hands nor says "not handing", as
// `path:line: text`, sorted. Test files are left out: a test may build a plan by hand.
func Problems(root string) ([]string, error) {
var out []string
err := filepath.WalkDir(filepath.Join(root, "modules"), func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() || !strings.HasSuffix(p, ".go") || strings.HasSuffix(p, "_test.go") {
return nil
}
raw, err := os.ReadFile(p)
if err != nil {
return err
}
rel, _ := filepath.Rel(root, p)
for i, line := range strings.Split(string(raw), "\n") {
if existence.MatchString(line) && !strings.Contains(line, ".Hands()") && !strings.Contains(line, "not handing") {
out = append(out, fmt.Sprintf("%s:%d: %s", rel, i+1, strings.TrimSpace(line)))
}
}
return nil
})
sort.Strings(out)
return out, err
}
-45
View File
@@ -1,45 +0,0 @@
package planhands
import (
"os"
"path/filepath"
"strings"
"testing"
)
// A line that takes a plan existing for its module handed is refused and named; one that asks Hands, one marked "not
// handing" and any test file pass; and the real catalogue is clean.
func TestNoModuleTakesAPlanExistingForItsModuleHanded(t *testing.T) {
root := t.TempDir()
for f, body := range map[string]string{
"modules/vault/own.go": "if a.Provisioning != nil {\n",
"modules/client/plan.go": "if len(a.Provisioning) == 0 {\n",
"modules/vault/rule.go": "return a.Provisioning != nil && a.Provisioning.Hands(), nil\n",
"modules/client/binds.go": "if a.Provisioning != nil { // binds, not handing\n",
"modules/vault/own_test.go": "if a.Provisioning == nil {\n",
"modules/client/notgo/plan.md": "if a.Provisioning == nil {\n",
} {
p := filepath.Join(root, f)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
got, err := Problems(root)
if err != nil {
t.Fatal(err)
}
if len(got) != 2 || !strings.HasPrefix(got[0], "modules/client/plan.go:1:") || !strings.HasPrefix(got[1], "modules/vault/own.go:1:") {
t.Fatalf("refused %v, wanted the client's and the vault's existence tests alone", got)
}
real, err := Problems(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
if len(real) > 0 {
t.Errorf("a module takes a plan existing for its module handed; ask catalogue.Plan.Hands, or mark a line that reads "+
"only who binds whom \"not handing\":\n%s", strings.Join(real, "\n"))
}
}
-3
View File
@@ -1,3 +0,0 @@
# What these tests read outside their package, as paths from the repository's root (issue 1936).
# every module whole (a directory is read whole, so a module's Go source at any depth enters the digest), for a line that takes a plan existing for its module handed.
modules
+1 -1
View File
@@ -2,6 +2,6 @@ module git.novox.be/novox/mesh-catalog/checks/predefined
go 1.22 go 1.22
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0-00010101000000-000000000000 require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
-6
View File
@@ -1,6 +0,0 @@
# 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's list of predefined keys and every manifest that names one.
predefined.json
modules/*/module.json
-16
View File
@@ -1,16 +0,0 @@
module checks/readable
go 1.26.0
require (
git.novox.be/novox/mesh-catalog/modules/disk-load/diskreadings v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-catalog/modules/restic/backupgood v0.0.0-00010101000000-000000000000
)
replace git.novox.be/novox/mesh-catalog/modules/disk-load/diskreadings => ../../modules/disk-load/diskreadings
replace git.novox.be/novox/mesh-catalog/modules/restic/backupgood => ../../modules/restic/backupgood
require (
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 // indirect
)
View File
-335
View File
@@ -1,335 +0,0 @@
// Command readable refuses a catalogue where a module reads another module's state that the owner does not announce
// readable (novox/hq ADR 0495 clause 3, issue 1790): the controller grants a read only from the owner's announcement, so
// such a read is refused on the live mesh, said only in the controller's journal. The sweep of 2026-10-30 (issue 774)
// found three: gitea's `bound` (read by mesh-provisioner, so no forge account was bound), disk-load's `readings` (read by
// restic, nfs-server and mounts) and restic's `good` (read by the self-check). The merge gate's check of reads against
// the owner's manifest went with clause 5, and announcetest holds a module to its own list, not to its readers.
//
// An owner announces from its code. A Go module announces when its code imports mesh-sdk's go/announce: every
// announce.State literal there is read, its Name a string, a constant of the package, or a constant of an owner's package this
// check knows (Known). A TypeScript module announces when its code says the event `states` or calls the SDK's keeps:
// every `{ name: "...", readable: true }` there is read. An owner that announces nothing from code is read as before
// on the mesh, and is not judged; neither is an owner outside the catalogue (another repository's).
//
// go run . <catalog root>
//
// Exits 1 on a finding.
package main
import (
"encoding/json"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"git.novox.be/novox/mesh-catalog/modules/disk-load/diskreadings"
"git.novox.be/novox/mesh-catalog/modules/restic/backupgood"
)
// Known are the constants of the owners' published packages an announcement may name a state by, by the selector the
// code writes: disk-load's readings and restic's last good night (ADR 0634 §4: a reading's layout is its owner's).
var Known = map[string]string{
"backupgood.GoodState": backupgood.GoodState,
"diskreadings.ReadingsState": diskreadings.ReadingsState,
}
const announcePkg = `"git.novox.be/novox/mesh-sdk/go/announce"`
// Announced is what one owner's code announces: each state, readable or not.
type Announced map[string]bool
// Findings is every refusal in the catalogue at root, sorted.
func Findings(root string) ([]string, error) {
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
dirs := map[string]string{}
reads := map[string][]string{}
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Reads []json.RawMessage `json:"reads"`
}
if err := json.Unmarshal(raw, &m); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
dirs[m.Module] = filepath.Dir(path)
for _, r := range m.Reads {
var s string
if json.Unmarshal(r, &s) == nil {
reads[m.Module] = append(reads[m.Module], s)
}
}
}
var out []string
owners := map[string]Announced{}
for reader, rs := range reads {
for _, r := range rs {
owner, state, ok := strings.Cut(r, ".")
dir, inCatalogue := dirs[owner]
if !ok || !inCatalogue || owner == reader {
continue
}
a, seen := owners[owner]
if !seen {
var problems []string
a, problems, err = AnnouncedBy(dir)
if err != nil {
return nil, err
}
owners[owner] = a
for _, p := range problems {
out = append(out, owner+": "+p)
}
}
if a == nil {
continue // announces nothing from code: read as before
}
readable, keeps := a[state]
switch {
case !keeps:
out = append(out, fmt.Sprintf("%s reads %s.%s, which %s's code does not announce: the read is refused on the mesh", reader, owner, state, owner))
case !readable:
out = append(out, fmt.Sprintf("%s reads %s.%s, which %s's code announces not readable: the read is refused on the mesh; announce it readable", reader, owner, state, owner))
}
}
}
sort.Strings(out)
return dedupe(out), nil
}
func dedupe(in []string) []string {
var out []string
for i, s := range in {
if i == 0 || s != in[i-1] {
out = append(out, s)
}
}
return out
}
var (
tsAnnounces = regexp.MustCompile(`emit\(\s*"states"|\bkeeps\(`)
tsState = regexp.MustCompile(`\{\s*name:\s*"([a-z0-9][a-z0-9-]*)"\s*(,\s*readable:\s*(true|false)\s*)?\}`)
)
// AnnouncedBy is what the module in dir announces from its code, nil when it announces nothing; problems are
// announcements whose state's name cannot be read.
func AnnouncedBy(dir string) (Announced, []string, error) {
var out Announced
var problems []string
pkgs := map[string][]*ast.File{}
fset := token.NewFileSet()
err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
if n := d.Name(); path != dir && (n == "node_modules" || n == "dist" || n == "test" || n == "testdata" || strings.HasPrefix(n, ".")) {
return filepath.SkipDir
}
return nil
}
name := d.Name()
switch {
case strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go"):
f, err := parser.ParseFile(fset, path, nil, 0)
if err != nil {
return err
}
pkgs[filepath.Dir(path)] = append(pkgs[filepath.Dir(path)], f)
case strings.HasSuffix(name, ".ts") && !strings.HasSuffix(name, ".d.ts") && !strings.HasSuffix(name, ".test.ts"):
raw, err := os.ReadFile(path)
if err != nil {
return err
}
if !tsAnnounces.Match(raw) && !strings.Contains(string(raw), "KEPT") {
return nil
}
for _, m := range tsState.FindAllStringSubmatch(string(raw), -1) {
if out == nil {
out = Announced{}
}
out[m[1]] = out[m[1]] || m[3] == "true"
}
}
return nil
})
if err != nil {
return nil, nil, err
}
for _, files := range pkgs {
imports := false
consts := map[string]ast.Expr{}
for _, f := range files {
for _, im := range f.Imports {
if im.Path.Value == announcePkg && (im.Name == nil || im.Name.Name == "announce") {
imports = true
}
}
for _, decl := range f.Decls {
g, ok := decl.(*ast.GenDecl)
if !ok || g.Tok != token.CONST {
continue
}
for _, spec := range g.Specs {
v := spec.(*ast.ValueSpec)
for i, n := range v.Names {
if i < len(v.Values) {
consts[n.Name] = v.Values[i]
}
}
}
}
}
if !imports {
continue
}
if out == nil {
out = Announced{}
}
for _, f := range files {
ast.Inspect(f, func(n ast.Node) bool {
lit, ok := n.(*ast.CompositeLit)
if !ok {
return true
}
states := []*ast.CompositeLit{}
switch {
case isAnnounceState(lit.Type):
states = append(states, lit)
case isStateSlice(lit.Type):
for _, e := range lit.Elts {
if c, ok := e.(*ast.CompositeLit); ok && (c.Type == nil || isAnnounceState(c.Type)) {
states = append(states, c)
}
}
default:
return true
}
for _, s := range states {
name, readable, problem := stateOf(s, consts)
if problem != "" {
problems = append(problems, fmt.Sprintf("%s: %s", fset.Position(s.Pos()), problem))
continue
}
out[name] = out[name] || readable
}
return false
})
}
}
return out, problems, nil
}
func isAnnounceState(e ast.Expr) bool {
s, ok := e.(*ast.SelectorExpr)
if !ok {
return false
}
x, ok := s.X.(*ast.Ident)
return ok && x.Name == "announce" && s.Sel.Name == "State"
}
func isStateSlice(e ast.Expr) bool {
a, ok := e.(*ast.ArrayType)
return ok && isAnnounceState(a.Elt)
}
// stateOf is one announce.State literal's name and whether it is readable (Readable true, or Readers named).
func stateOf(lit *ast.CompositeLit, consts map[string]ast.Expr) (string, bool, string) {
var name string
var readable bool
named := false
for _, e := range lit.Elts {
kv, ok := e.(*ast.KeyValueExpr)
if !ok {
return "", false, "an announce.State without field names cannot be read; name its fields"
}
key, _ := kv.Key.(*ast.Ident)
if key == nil {
continue
}
switch key.Name {
case "Name":
v, ok := resolve(kv.Value, consts, 0)
if !ok {
return "", false, fmt.Sprintf("the state's name %s cannot be read: name it by a string, a constant of the package, or a mesh-sdk constant in checks/readable's Known", exprString(kv.Value))
}
name, named = v, true
case "Readable":
if id, ok := kv.Value.(*ast.Ident); ok {
readable = readable || id.Name == "true"
}
case "Readers":
readable = true
}
}
if !named {
return "", false, "an announce.State without a Name"
}
return name, readable, ""
}
func resolve(e ast.Expr, consts map[string]ast.Expr, depth int) (string, bool) {
if depth > 8 {
return "", false
}
switch v := e.(type) {
case *ast.BasicLit:
if v.Kind == token.STRING {
s, err := strconv.Unquote(v.Value)
return s, err == nil
}
case *ast.Ident:
if c, ok := consts[v.Name]; ok {
return resolve(c, consts, depth+1)
}
case *ast.SelectorExpr:
s, ok := Known[exprString(v)]
return s, ok
}
return "", false
}
func exprString(e ast.Expr) string {
switch v := e.(type) {
case *ast.Ident:
return v.Name
case *ast.SelectorExpr:
return exprString(v.X) + "." + v.Sel.Name
case *ast.BasicLit:
return v.Value
}
return fmt.Sprintf("%T", e)
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
found, err := Findings(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
for _, f := range found {
fmt.Println(f)
}
if len(found) > 0 {
os.Exit(1)
}
}
-90
View File
@@ -1,90 +0,0 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
// A read of another module's state is refused here when the owner's code announces that state not readable, or not at
// all (the sweep of 2026-10-30, issue 774); an owner that announces nothing from code, or lives in another repository,
// is not judged.
func TestFindings(t *testing.T) {
const goOwner = "package main\n\nimport \"git.novox.be/novox/mesh-sdk/go/announce\"\n\n"
for _, row := range []struct {
name string
files map[string]string
want string // empty: clean
}{
{"go, readable by a constant", map[string]string{
"modules/r/module.json": `{"module":"r","reads":["o.s"]}`,
"modules/o/module.json": `{"module":"o"}`,
"modules/o/cmd/o/main.go": goOwner + "const S = \"s\"\n\nvar _ = announce.State{Name: S, Readable: true}\n",
"modules/o/cmd/o/main_test.go": goOwner + "var _ = announce.State{Name: \"s\"}\n"}, ""},
{"go, not readable", map[string]string{
"modules/r/module.json": `{"module":"r","reads":["o.s"]}`,
"modules/o/module.json": `{"module":"o"}`,
"modules/o/cmd/o/main.go": goOwner + "var Kept = []announce.State{{Name: \"t\", Readable: true}, {Name: \"s\"}}\n"},
"r reads o.s, which o's code announces not readable"},
{"go, a known mesh-sdk name and readers", map[string]string{
"modules/r/module.json": `{"module":"r","reads":["o.good","o.s"]}`,
"modules/o/module.json": `{"module":"o"}`,
"modules/o/cmd/o/main.go": goOwner + "var _ = []announce.State{{Name: backupgood.GoodState, Readable: true}, {Name: \"s\", Readers: []string{\"r\"}}}\n"}, ""},
{"go, a name that cannot be read", map[string]string{
"modules/r/module.json": `{"module":"r","reads":["o.s"]}`,
"modules/o/module.json": `{"module":"o"}`,
"modules/o/cmd/o/main.go": goOwner + "var _ = announce.State{Name: other.S, Readable: true}\n"},
"the state's name other.S cannot be read"},
{"go, a state not announced", map[string]string{
"modules/r/module.json": `{"module":"r","reads":["o.s"]}`,
"modules/o/module.json": `{"module":"o"}`,
"modules/o/cmd/o/main.go": goOwner + "var _ = announce.State{Name: \"t\"}\n"},
"r reads o.s, which o's code does not announce"},
{"typescript, not readable", map[string]string{
"modules/r/module.json": `{"module":"r","reads":["o.bound"]}`,
"modules/o/module.json": `{"module":"o"}`,
"modules/o/kept.ts": `export const KEPT = [{ name: "announced" }, { name: "bound" }];`},
"r reads o.bound, which o's code announces not readable"},
{"typescript, readable", map[string]string{
"modules/r/module.json": `{"module":"r","reads":["o.bound"]}`,
"modules/o/module.json": `{"module":"o"}`,
"modules/o/index.ts": `await keeps([{ name: "bound", readable: true }]);`}, ""},
{"an owner that announces nothing, and one outside the catalogue", map[string]string{
"modules/r/module.json": `{"module":"r","reads":["o.s","mesh-controller.lease"]}`,
"modules/o/module.json": `{"module":"o"}`,
"modules/o/cmd/o/main.go": "package main\n\nvar _ = 1\n"}, ""},
} {
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)
}
joined := strings.Join(got, "\n")
if row.want == "" && len(got) != 0 || row.want != "" && !strings.Contains(joined, row.want) {
t.Errorf("findings %q, want %q", joined, row.want)
}
})
}
}
// The catalogue itself: every state a module reads of another module that announces from code is announced readable.
func TestTheCatalogueReadsOnlyWhatItsOwnersAnnounceReadable(t *testing.T) {
got, err := Findings(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
for _, f := range got {
t.Error(f)
}
}
-6
View File
@@ -1,6 +0,0 @@
# What these tests read outside their package, as paths from the repository's root (issue 1936).
# every manifest, for what it reads; and every module's Go and TypeScript code, for the states it announces.
modules/*/module.json
modules/*/cmd
modules/*/*.go
modules/*/*.ts
-3
View File
@@ -1,3 +0,0 @@
module git.novox.be/novox/mesh-catalog/checks/removedpackages
go 1.22
-166
View File
@@ -1,166 +0,0 @@
// Command removedpackages refuses a Go source of the catalogue that imports what mesh-sdk gave up in the release that
// narrowed it (novox/hq ADR 0593 §1): a package that left for its owner, and the build runner's names that left
// mesh-sdk's buildstep for mesh-build-go's package.
//
// A module compiles against the mesh-sdk version it pins, so a source that still names a removed package builds until the
// pin moves, and then breaks in a delivery of its own; and a later change could bring the import back with the old pin.
// This check refuses it at once, from the source alone:
//
// - an import of a package in Removed (or one under it);
// - in a file importing mesh-sdk's buildstep (under any name), a selector naming anything that is not in
// BuildstepContract: what is left of that package is what the delivery parts read, and the runner is
// mesh-build-go's package (git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep).
//
// It reads the non-test and test Go files under modules/ and checks/ (testdata, vendor and node_modules left out).
//
// go run . <catalogue root>
//
// Exits 1 on a finding. The lists are the release's: a package that goes later is added here with the change.
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
)
const sdk = "git.novox.be/novox/mesh-sdk/go"
// Removed are the packages of mesh-sdk that left it, each with where its logic is now.
var Removed = map[string]string{
"buildcpu": "the build runner's package, git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep/buildcpu",
"buildstep/conformance": "the build runner's package, git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep/conformance",
"delivery/buildrun": "the builder's package (mesh-delivery-builder), imported by its repository's modules",
"compose": "the node-engine, which alone composes (ADR 0593 §6)",
"compose/frombus": "the node-engine, which alone composes (ADR 0593 §6)",
"compose/fromruntime": "the node-engine, which alone composes (ADR 0593 §6)",
"compose/judge": "the assigner's own package",
"takein": "the node-engine, which alone composes (ADR 0593 §6)",
"validate": "the node-engine, which alone composes (ADR 0593 §6)",
"facts": "nothing: the controller they served is ended (ADR 0590)",
"example": "nothing: no module imports it (ADR 0593 §12)",
"packageguard": "nothing: a test helper of mesh-sdk's own",
}
// BuildstepContract are the names mesh-sdk's buildstep keeps: the failure contract, the fetch request, the stale-step sweep,
// the step id and the module tree shape the delivery parts read.
var BuildstepContract = map[string]bool{
"AskWhat": true, "Boot": true, "By": true, "CapacitySetting": true, "Clip": true, "ConsoleFailure": true, "CutLine": true,
"EndOne": true, "EndStale": true, "EndStaleAtStart": true, "EndStaleRound": true, "Failure": true, "FetchRequest": true,
"FetchRequestProperties": true, "Hidden": true, "Launchers": true, "LauncherSuffix": true, "LineBytes": true,
"ModuleTree": true, "NewID": true, "OutputBytes": true, "Owner": true, "PackagesArg": true, "ParseFetchRequest": true,
"PassingLine": true, "RestartReason": true, "Scan": true, "ScanOf": true, "StaleAttempts": true, "StaleRetry": true,
"StaleServed": true,
}
// Findings is every refusal in the Go sources under root, sorted.
func Findings(root string) ([]string, error) {
var out []string
for _, top := range []string{"modules", "checks"} {
err := filepath.WalkDir(filepath.Join(root, top), func(path string, d fs.DirEntry, err error) error {
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if d.IsDir() {
switch d.Name() {
case "testdata", "vendor", "node_modules", ".mesh-build":
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(path, ".go") {
return nil
}
rel, _ := filepath.Rel(root, path)
found, err := fileFindings(path, filepath.ToSlash(rel))
if err != nil {
return err
}
out = append(out, found...)
return nil
})
if err != nil {
return nil, err
}
}
sort.Strings(out)
return out, nil
}
func fileFindings(path, rel string) ([]string, error) {
file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.SkipObjectResolution)
if err != nil {
return nil, fmt.Errorf("%s: %w", rel, err)
}
var out []string
buildstep := ""
for _, imp := range file.Imports {
p, err := strconv.Unquote(imp.Path.Value)
if err != nil {
continue
}
sub, ok := strings.CutPrefix(p, sdk+"/")
if !ok {
continue
}
for pkg, to := range Removed {
if sub == pkg || strings.HasPrefix(sub, pkg+"/") {
out = append(out, fmt.Sprintf("%s: imports %s, which left mesh-sdk; it is %s (novox/hq ADR 0593 §1)", rel, p, to))
break
}
}
if sub == "buildstep" {
buildstep = "buildstep"
if imp.Name != nil {
buildstep = imp.Name.Name
}
}
}
if buildstep == "" || buildstep == "_" || buildstep == "." {
return out, nil
}
seen := map[string]bool{}
ast.Inspect(file, func(n ast.Node) bool {
sel, ok := n.(*ast.SelectorExpr)
if !ok {
return true
}
id, ok := sel.X.(*ast.Ident)
if !ok || id.Name != buildstep || !sel.Sel.IsExported() || BuildstepContract[sel.Sel.Name] || seen[sel.Sel.Name] {
return true
}
seen[sel.Sel.Name] = true
out = append(out, fmt.Sprintf("%s: uses %s.%s, which is the build runner's, no longer mesh-sdk's buildstep; import git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep (novox/hq ADR 0593 §1)", rel, buildstep, sel.Sel.Name))
return true
})
return out, nil
}
func main() {
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "usage: removedpackages <catalogue root>")
os.Exit(2)
}
found, err := Findings(os.Args[1])
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
for _, f := range found {
fmt.Println(f)
}
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("no source imports a package mesh-sdk gave up")
}
-57
View File
@@ -1,57 +0,0 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
// ADR 0593 §1: a source that imports a package mesh-sdk gave up, or names the build runner through mesh-sdk's buildstep, is
// refused naming where it went; the contract that stays, the runner's own package and a testdata copy are not.
func TestFindingsRefuseWhatMeshSDKGaveUp(t *testing.T) {
const head = "package x\n\nimport (\n"
for _, row := range []struct {
name, file, src string
want string // empty: clean
}{
{"clean", "modules/a/x.go", head + "\t\"git.novox.be/novox/mesh-sdk/go/delivery\"\n)\n\nvar _ = delivery.StepsState\n", ""},
{"a removed package", "modules/a/x.go", head + "\t\"git.novox.be/novox/mesh-sdk/go/compose\"\n)\n\nvar _ = compose.Resolve\n", "imports git.novox.be/novox/mesh-sdk/go/compose, which left mesh-sdk; it is the node-engine"},
{"under a removed package", "modules/a/x_test.go", head + "\t\"git.novox.be/novox/mesh-sdk/go/compose/frombus\"\n)\n", "the node-engine"},
{"buildcpu", "checks/c/x.go", head + "\t\"git.novox.be/novox/mesh-sdk/go/buildcpu\"\n)\n", "buildstep/buildcpu"},
{"the runner through mesh-sdk's buildstep", "modules/a/x.go", head + "\t\"git.novox.be/novox/mesh-sdk/go/buildstep\"\n)\n\nvar _ = buildstep.RunAsk\n", "uses buildstep.RunAsk, which is the build runner's"},
{"the runner through an alias", "modules/a/x.go", head + "\tsdkstep \"git.novox.be/novox/mesh-sdk/go/buildstep\"\n)\n\nvar _ = sdkstep.Puller{}\n", "uses sdkstep.Puller"},
{"the contract of buildstep", "modules/a/x.go", head + "\t\"git.novox.be/novox/mesh-sdk/go/buildstep\"\n)\n\nvar _ = buildstep.NewID\nvar _ buildstep.Failure\n", ""},
{"the runner's own package", "modules/a/x.go", head + "\t\"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep\"\n)\n\nvar _ = buildstep.RunAsk\n", ""},
{"a copy in testdata", "modules/a/testdata/x.go", head + "\t\"git.novox.be/novox/mesh-sdk/go/compose\"\n)\n", ""},
} {
t.Run(row.name, func(t *testing.T) {
root := t.TempDir()
p := filepath.Join(root, row.file)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(row.src), 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 TestTheCatalogueImportsNothingMeshSDKGaveUp(t *testing.T) {
got, err := Findings(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
for _, f := range got {
t.Error(f)
}
}
-6
View File
@@ -1,6 +0,0 @@
# 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.
# every Go source of the modules and of the checks, whose imports are read.
modules
checks
-3
View File
@@ -1,3 +0,0 @@
module git.novox.be/novox/mesh-catalog/checks/sdkjudge
go 1.22
-183
View File
@@ -1,183 +0,0 @@
// Package sdkjudge refuses mesh-sdk a manifest judge (novox/hq ADR 0578 §4): a package every module compiles in carries
// the contract only, so it holds no function that gathers a manifest's problems and no table of what a manifest may
// declare. The judge is a package of mesh-delivery that only the indexer and the checker use, and a reader decodes a
// manifest and takes the indexer's verdict (ADR 0578 §3).
//
// It reads mesh-sdk's source as the checker places it beside this check (MESH_CHECK_BESIDE, the repository mesh-sdk: at its
// trunk, or at the head of the group's pull request when the group holds one), so a change adding either to mesh-sdk in a
// delivery group with a catalogue change is refused here. A change to mesh-sdk alone is checked by mesh-sdk's own check,
// which does not run this package: the catalogue's check sees the SDK only beside a catalogue change.
//
// What it refuses, in the non-test Go files of mesh-sdk's go/ tree:
// - a function or method named for problems (a name ending in Problems or Problem) that takes a Manifest or a Shelf, and
// a function named ParseManifest, ParseManifestWith, ParseManifestForInputs, ValidateManifest... or ValidateShelf...: the
// judge's entry points;
// - in the package catalogue, a package-level variable named for what is allowed, permitted, removable, reserved or
// forbidden whose value is a map or a list: an allow-list table.
//
// The composer's refusals of composition itself (what a machine can be composed with) are not a judgement of a manifest
// (ADR 0578 §4, clause 5; issue 1585 tracks moving them): they are named in composition, each with why, and the list only
// shrinks, since a name that no longer exists is refused too.
package sdkjudge
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// composition is every function or table the check lets stay in mesh-sdk, as "path-from-go/:name", and why. It names no
// manifest judgement: each is the composer's refusal of composition itself (ADR 0578 clause 5).
var composition = map[string]string{
"catalogue/collections.go:itemTemplateProblem": "composition: filling an item's template",
}
// Problems is what mesh-sdk's Go tree under root (its go/ directory) holds of a manifest judge, one line each, but for
// what composition lets stay (the map the caller gives; the test of the real tree gives the package's), and the
// entries of composition it no longer holds.
func Problems(root string, composition map[string]string) ([]string, error) {
var out []string
seen := map[string]bool{}
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
if n := d.Name(); n == "testdata" || n == "vendor" || strings.HasPrefix(n, ".") {
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
rel, _ := filepath.Rel(root, path)
file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.SkipObjectResolution)
if err != nil {
return fmt.Errorf("%s: %w", rel, err)
}
inCatalogue := filepath.Base(filepath.Dir(path)) == "catalogue"
for _, d := range file.Decls {
switch d := d.(type) {
case *ast.FuncDecl:
if why := judgeFunc(d); why != "" {
out = append(out, note(composition, seen, rel, d.Name.Name, why))
}
case *ast.GenDecl:
if !inCatalogue || d.Tok != token.VAR {
continue
}
for _, spec := range d.Specs {
v, _ := spec.(*ast.ValueSpec)
if v == nil {
continue
}
for i, name := range v.Names {
if i < len(v.Values) && allowName.MatchString(name.Name) && isTable(v.Values[i]) {
out = append(out, note(composition, seen, rel, name.Name,
"a table of what a manifest may declare (an allow-list)"))
}
}
}
}
}
return nil
})
if err != nil {
return nil, err
}
var kept []string
for _, line := range out {
if line != "" {
kept = append(kept, line)
}
}
for key, why := range composition {
if !seen[key] {
kept = append(kept, fmt.Sprintf("composition lists %s (%s), which mesh-sdk no longer holds: take it off the list", key, why))
}
}
sort.Strings(kept)
return kept, nil
}
// note is the line refusing name in rel for why, or "" when composition lets it stay.
func note(composition map[string]string, seen map[string]bool, rel, name, why string) string {
key := rel + ":" + name
if _, ok := composition[key]; ok {
seen[key] = true
return ""
}
return fmt.Sprintf("%s: %s is %s; mesh-sdk carries the contract only, the judge is the indexer's and the checker's (ADR 0578 §4)", rel, name, why)
}
// allowName is what names a table of what is allowed.
var allowName = regexp.MustCompile(`(?i)allow|permit|removable|reserved|forbid|denied`)
// isTable says whether an expression is a map or a list literal of more than one entry.
func isTable(e ast.Expr) bool {
c, ok := e.(*ast.CompositeLit)
if !ok || len(c.Elts) < 2 {
return false
}
switch c.Type.(type) {
case *ast.MapType, *ast.ArrayType:
return true
}
return false
}
// judgeFunc says why a function is a manifest judge, or "".
func judgeFunc(f *ast.FuncDecl) string {
name := f.Name.Name
if judgeEntry.MatchString(name) {
return "a function that judges a manifest"
}
if !strings.HasSuffix(name, "Problems") && !strings.HasSuffix(name, "Problem") {
return ""
}
if takes(f.Recv) || takes(f.Type.Params) {
return "a function that gathers a manifest's problems"
}
return ""
}
// judgeEntry is the names of the judge's entry points: the strict parsers and the validators of a manifest or a shelf.
var judgeEntry = regexp.MustCompile(`^(ParseManifest(With|ForInputs)?|Validate(Manifest|Shelf)\w*)$`)
// takes says whether a list of parameters names a Manifest or a Shelf anywhere in a type.
func takes(fields *ast.FieldList) bool {
if fields == nil {
return false
}
found := false
for _, f := range fields.List {
ast.Inspect(f.Type, func(n ast.Node) bool {
if id, ok := n.(*ast.Ident); ok && (id.Name == "Manifest" || id.Name == "Shelf") {
found = true
}
return !found
})
}
return found
}
// BesideDir is where the checker places mesh-sdk's clone, as a directory under MESH_CHECK_BESIDE; its go/ tree is judged.
func BesideDir() (string, error) {
beside := os.Getenv("MESH_CHECK_BESIDE")
if beside == "" {
return "", fmt.Errorf("NOT CHECKED: MESH_CHECK_BESIDE is not set, so mesh-sdk's source is not beside this check; the checker places the SDK there (besideRules)")
}
dir := filepath.Join(beside, "mesh-sdk", "go")
if _, err := os.Stat(dir); err != nil {
return "", fmt.Errorf("NOT CHECKED: mesh-sdk is not beside this check in MESH_CHECK_BESIDE=%s: %w", beside, err)
}
return dir, nil
}
-66
View File
@@ -1,66 +0,0 @@
package sdkjudge
import (
"os"
"path/filepath"
"strings"
"testing"
)
// mesh-sdk carries the contract only (ADR 0578 §4). A row is a source tree of its own: a function gathering a manifest's
// problems, a manifest parser that judges and a table of what a manifest may declare are refused, named; a Problems
// function over something that is no manifest, a test file, a table outside the catalogue package and a function that
// merely decodes are not. Then the SDK beside this check is judged.
func TestMeshSDKCarriesNoManifestJudge(t *testing.T) {
for _, row := range []struct {
name, dir, file, src, refused string
}{
{name: "a function gathering a manifest's problems", dir: "catalogue", refused: "StreamProblems is a function that gathers a manifest's problems",
src: "package catalogue\ntype Manifest struct{}\nfunc StreamProblems(m Manifest) []string { return nil }\n"},
{name: "a method over a manifest", dir: "catalogue", refused: "healthProblems is a function that gathers",
src: "package catalogue\ntype Manifest struct{}\nfunc (m Manifest) healthProblems() []string { return nil }\n"},
{name: "a function over the shelf", dir: "catalogue", refused: "CatalogueProblems is a function that gathers",
src: "package catalogue\ntype Shelf map[string]int\nfunc CatalogueProblems(s Shelf) []string { return nil }\n"},
{name: "a parser that judges", dir: "catalogue", refused: "ParseManifestWith is a function that judges a manifest",
src: "package catalogue\nfunc ParseManifestWith(raw []byte) error { return nil }\n"},
{name: "a table of what may be removed", dir: "catalogue", refused: "removable is a table of what a manifest may declare",
src: "package catalogue\nvar removable = map[string]bool{\"A\": true, \"B\": true}\n"},
{name: "a key parser", dir: "delivery", src: "package delivery\nfunc ParseManifestsKey(k string) error { return nil }\n"},
{name: "a decoder", dir: "catalogue", src: "package catalogue\ntype Manifest struct{}\nfunc Decode(raw []byte) (Manifest, error) { return Manifest{}, nil }\n"},
{name: "problems of something else", dir: "asks", src: "package asks\ntype Condition struct{}\nfunc DecodeProblems(c Condition) []string { return nil }\n"},
{name: "a table beside the catalogue", dir: "asks", src: "package asks\nvar allowed = []string{\"a\", \"b\"}\n"},
{name: "a test file", dir: "catalogue", file: "x_test.go",
src: "package catalogue\ntype Manifest struct{}\nfunc StreamProblems(m Manifest) []string { return nil }\n"},
} {
root := t.TempDir()
dir := filepath.Join(root, row.dir)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if row.file == "" {
row.file = "x.go"
}
if err := os.WriteFile(filepath.Join(dir, row.file), []byte(row.src), 0o644); err != nil {
t.Fatal(err)
}
got, err := Problems(root, nil)
if err != nil {
t.Fatal(err)
}
joined := strings.Join(got, "\n")
if (row.refused == "") != (len(got) == 0) || !strings.Contains(joined, row.refused) {
t.Errorf("%s: refused %q, want %q", row.name, joined, row.refused)
}
}
dir, err := BesideDir()
if err != nil {
t.Fatal(err)
}
got, err := Problems(dir, composition)
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Fatalf("mesh-sdk holds a manifest judge:\n%s", strings.Join(got, "\n"))
}
}
-4
View File
@@ -1,4 +0,0 @@
# 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.
# None: the tests read mesh-sdk's source, the clone beside this repository (MESH_CHECK_BESIDE), which is no path of it.
+1 -1
View File
@@ -2,6 +2,6 @@ module git.novox.be/novox/mesh-catalog/checks/secrets
go 1.22 go 1.22
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0-00010101000000-000000000000 require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
-5
View File
@@ -1,5 +0,0 @@
# 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.
# Check("../.."): every manifest and the files it names.
modules
-5
View File
@@ -1,5 +0,0 @@
# 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.
# Check("../.."): every manifest.
modules/*/module.json
+1 -1
View File
@@ -2,6 +2,6 @@ module git.novox.be/novox/mesh-catalog/checks/templates
go 1.22 go 1.22
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0-00010101000000-000000000000 require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
-5
View File
@@ -1,5 +0,0 @@
# 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.
# Check("../.."): every manifest and its templates.
modules
-5
View File
@@ -1,5 +0,0 @@
# 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.
# Check("../.."): every manifest.
modules/*/module.json
+18 -14
View File
@@ -3,14 +3,14 @@
// //
// The checker fetches the change's own repository at its base commit beside the change, as it fetches a sibling, and says where // The checker fetches the change's own repository at its base commit beside the change, as it fetches a sibling, and says where
// in MESH_CHECK_BESIDE; the copy is under the repository's name (the same convention the other repositories' tests use). With // in MESH_CHECK_BESIDE; the copy is under the repository's name (the same convention the other repositories' tests use). With
// MESH_CHECK_BESIDE unset, or set without the catalogue's copy in it, the test fails saying NOT COMPARED WITH THE TRUNK and // MESH_CHECK_BESIDE unset, or set without the catalogue's copy in it, the comparison is skipped and the test says NOT COMPARED
// why: the checker places the change's own repository since R4g (issue 1295), so a missing copy is a fault, never a pass. A trunk that holds no such file had // WITH THE TRUNK, naming why: the checker that runs before R4g's delivery places siblings but not the change's own repository,
// so R4g's own check cannot hold the copy (issue 1295 notes that a missing copy becomes a failure once that checker is gone). A trunk that holds no such file had
// none: the list's first copy, which nothing was added to. // none: the list's first copy, which nothing was added to.
package trunk package trunk
import ( import (
"errors" "errors"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@@ -23,11 +23,13 @@ const Repository = "mesh-catalog"
const BesideEnv = "MESH_CHECK_BESIDE" const BesideEnv = "MESH_CHECK_BESIDE"
// Copy is the trunk's copy of the file at rel (a path from the catalogue's root), written to a file of the test's own: its path, // Copy is the trunk's copy of the file at rel (a path from the catalogue's root), written to a file of the test's own: its path,
// and whether the trunk holds one (false: its first copy). It fails the test when no copy of the catalogue is beside. // and whether the trunk holds one (false: its first copy). It skips the test, saying so, when no copy of the catalogue is beside.
func Copy(t testing.TB, rel string) (string, bool) { func Copy(t testing.TB, rel string) (string, bool) {
t.Helper() t.Helper()
raw, found, err := Read(rel) raw, found, skip, err := Read(rel)
switch { switch {
case skip != "":
t.Skip(skip)
case err != nil: case err != nil:
t.Fatal(err) t.Fatal(err)
case !found: case !found:
@@ -41,25 +43,27 @@ func Copy(t testing.TB, rel string) (string, bool) {
return path, true return path, true
} }
// Read is the trunk's copy of rel and whether the trunk holds one. err is a trunk not beside the check (MESH_CHECK_BESIDE unset, // Read is the trunk's copy of rel and whether the trunk holds one. skip is the words to skip the comparison with, when no trunk is
// or without the catalogue's directory), or a copy unreadable: a comparison that cannot be made is never a pass. // named; err is a trunk named and not there, or unreadable.
func Read(rel string) (content []byte, found bool, err error) { func Read(rel string) (content []byte, found bool, skip string, err error) {
beside := os.Getenv(BesideEnv) beside := os.Getenv(BesideEnv)
if beside == "" { if beside == "" {
return nil, false, fmt.Errorf("NOT COMPARED WITH THE TRUNK: %s is not set, so the trunk's copy of %s is not beside this run; the checker places the change's own repository there (issue 1295)", BesideEnv, rel) return nil, false, "NOT COMPARED WITH THE TRUNK: " + BesideEnv + " is not set, so the trunk's copy of " + rel +
" is not beside this run; the checker places it", nil
} }
dir := filepath.Join(beside, Repository) dir := filepath.Join(beside, Repository)
if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) { if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) {
return nil, false, fmt.Errorf("NOT COMPARED WITH THE TRUNK: %s is not beside this check in %s=%s, so %s is not judged against the trunk's copy; the checker places the change's own repository there at its trunk (issue 1295)", Repository, BesideEnv, beside, rel) return nil, false, "NOT COMPARED WITH THE TRUNK: " + Repository + " is not beside this check in " + BesideEnv + "=" + beside +
", so " + rel + " is not judged against the trunk's copy (the checker before R4g places no copy of the change's own repository)", nil
} else if err != nil { } else if err != nil {
return nil, false, err return nil, false, "", err
} }
raw, err := os.ReadFile(filepath.Join(dir, rel)) raw, err := os.ReadFile(filepath.Join(dir, rel))
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
return nil, false, nil return nil, false, "", nil
} }
if err != nil { if err != nil {
return nil, false, err return nil, false, "", err
} }
return raw, true, nil return raw, true, "", nil
} }
+14 -14
View File
@@ -17,27 +17,27 @@ func TestTheCloneBesideTheCheckIsTheTrunk(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
t.Setenv(BesideEnv, beside) t.Setenv(BesideEnv, beside)
raw, found, err := Read("checks/x/known") raw, found, skip, err := Read("checks/x/known")
if err != nil || !found || string(raw) != "a\nb\n" { if err != nil || skip != "" || !found || string(raw) != "a\nb\n" {
t.Fatalf("read %q found %v err %v", raw, found, err) t.Fatalf("read %q found %v skip %q err %v", raw, found, skip, err)
} }
if _, found, err := Read("checks/x/absent"); err != nil || found { if _, found, skip, err := Read("checks/x/absent"); err != nil || skip != "" || found {
t.Fatalf("a file the trunk does not hold: found %v err %v; wanted its first copy", found, err) t.Fatalf("a file the trunk does not hold: found %v skip %q err %v; wanted its first copy", found, skip, err)
} }
} }
func TestWithNothingBesideTheComparisonFailsAndSaysNotMade(t *testing.T) { func TestWithNothingBesideTheComparisonIsSkippedAndSaidNotMade(t *testing.T) {
t.Setenv(BesideEnv, "") t.Setenv(BesideEnv, "")
_, found, err := Read("checks/x/known") _, found, skip, err := Read("checks/x/known")
if found || err == nil || !strings.Contains(err.Error(), "NOT COMPARED") { if found || err != nil || len(skip) < 12 || skip[:12] != "NOT COMPARED" {
t.Fatalf("found %v, err %v; wanted a failure saying NOT COMPARED", found, err) t.Fatalf("found %v, skip %q, err %v; wanted NOT COMPARED", found, skip, err)
} }
} }
func TestACloneBesideWithoutTheCatalogueFailsAndSaysNotMade(t *testing.T) { func TestACloneBesideWithoutTheCatalogueIsSkippedAndSaidNotMade(t *testing.T) {
t.Setenv(BesideEnv, t.TempDir()) // the siblings are there, the catalogue's own base is not t.Setenv(BesideEnv, t.TempDir()) // the siblings are there, the catalogue's own base is not (the checker before R4g)
_, found, err := Read("checks/x/known") _, found, skip, err := Read("checks/x/known")
if found || err == nil || !strings.Contains(err.Error(), "NOT COMPARED") || !strings.Contains(err.Error(), "not beside") { if found || err != nil || len(skip) < 12 || skip[:12] != "NOT COMPARED" || !strings.Contains(skip, "not beside") {
t.Fatalf("found %v, err %v; wanted a failure naming the missing copy", found, err) t.Fatalf("found %v, skip %q, err %v; wanted NOT COMPARED naming the missing copy", found, skip, err)
} }
} }
+1 -45
View File
@@ -4,9 +4,7 @@
// own code that is not a test (a tool's description, the notes and errors it answers with) and every // own code that is not a test (a tool's description, the notes and errors it answers with) and every
// description in its manifest. Comments are not read: an agent never sees them. And no daemon's line // description in its manifest. Comments are not read: an agent never sees them. And no daemon's line
// (novox/hq ADR 0272, lines.go): the copy in retired-lines, the lines a person would otherwise be told to // (novox/hq ADR 0272, lines.go): the copy in retired-lines, the lines a person would otherwise be told to
// type of the node-engine's or the controller's binary, where the operator's command is `nox`. And no address of the // type of the node-engine's or the controller's binary, where the operator's command is `nox`.
// controller (novox/hq ADR 0590 §5): it is not placed, so a text naming one of its verbs, states or buckets sends its
// reader nowhere.
// //
// A word is matched whole and in any case, a space in it matching any run of white space, and its plural // A word is matched whole and in any case, a space in it matching any run of white space, and its plural
// (`s` or `es` on its last part) is matched as the word. A vendor word is // (`s` or `es` on its last part) is matched as the word. A vendor word is
@@ -91,45 +89,11 @@ func run(root string) int {
len(findings)) len(findings))
return 1 return 1
} }
// And no address of the controller (novox/hq ADR 0590 §5): it is not placed, so a text that sends an agent or the
// operator to one of its verbs, states or buckets sends them nowhere.
if findings := checkAddresses(texts); len(findings) > 0 {
for _, f := range findings {
fmt.Println(f)
}
fmt.Printf("words: %d address(es) of the controller in what a module shows — name the seat that holds the job now (novox/hq ADR 0590)\n",
len(findings))
return 1
}
fmt.Printf("words: %d retired words and %d daemons' lines, none in the %d strings of %d files\n", len(words), len(lines), fmt.Printf("words: %d retired words and %d daemons' lines, none in the %d strings of %d files\n", len(words), len(lines),
len(texts), files) len(texts), files)
return 0 return 0
} }
// controllerAddress is an address of the controller: its seat's verb or state (`mesh-controller.<name>`, with or without
// `seat:`) or one of its buckets (`mesh-controller_<bucket>`). The module's name in prose is no address.
var controllerAddress = regexp.MustCompile(`(^|[^A-Za-z0-9_-])(seat:)?mesh-controller[._][A-Za-z0-9<]`)
// checkAddresses is every text that names an address of the controller (novox/hq ADR 0590 §5).
func checkAddresses(texts []text) []string {
var out []string
for _, t := range texts {
if !strings.Contains(t.s, "mesh-controller") {
continue
}
if m := controllerAddress.FindStringIndex(t.s); m != nil {
line := t.line
if line > 0 {
line += strings.Count(t.s[:m[0]], "\n")
}
out = append(out, fmt.Sprintf("%s:%d: names an address of the controller, which is not placed — in %q", t.file, line,
excerpt(t.s, m[0], m[1])))
}
}
sort.Strings(out)
return out
}
// collect reads every text a module can show under root: the strings of its own code that is not a test, and every string of // collect reads every text a module can show under root: the strings of its own code that is not a test, and every string of
// its manifests. // its manifests.
func collect(root string) ([]text, int, error) { func collect(root string) ([]text, int, error) {
@@ -242,9 +206,6 @@ func excerpt(s string, from, to int) string {
return strings.Join(strings.Fields(s[a:b]), " ") return strings.Join(strings.Fields(s[a:b]), " ")
} }
// modulePath matches a string that is only a module or import path ("git.example.org/org/repo/go/pkg").
var modulePath = regexp.MustCompile(`^[a-z0-9.-]+\.[a-z]+/[A-Za-z0-9_./-]+$`)
// goStrings returns a Go file's string literals, adjacent ones joined across `+` as the program joins them. // goStrings returns a Go file's string literals, adjacent ones joined across `+` as the program joins them.
func goStrings(path, rel string) ([]text, error) { func goStrings(path, rel string) ([]text, error) {
src, err := os.ReadFile(path) src, err := os.ReadFile(path)
@@ -270,11 +231,6 @@ func goStrings(path, rel string) ([]text, error) {
if err != nil { if err != nil {
v = lit v = lit
} }
if modulePath.MatchString(v) {
// An import path or another module path names code, not words an agent reads.
cur, joining = nil, false
continue
}
if cur != nil && joining { if cur != nil && joining {
cur.s += v cur.s += v
} else { } else {
-22
View File
@@ -95,25 +95,3 @@ func TestTheCatalogueHoldsNoRetiredWordAndNoDaemonsLine(t *testing.T) {
t.Fatalf("the check of the real catalogue exits %d (see what it printed above)", code) t.Fatalf("the check of the real catalogue exits %d (see what it printed above)", code)
} }
} }
// ADR 0590 §5 (issue 1126 B10): no text a module shows names an address of the controller, which is not placed; prose
// naming it passes.
func TestCheckAddressesRefusesTheControllersAddresses(t *testing.T) {
for _, c := range []struct {
s string
want bool
}{
{"push this machine (mesh-controller.push)", true},
{"ask seat:mesh-controller.nodes", true},
{"the bucket mesh-controller_assignments", true},
{"`mesh-controller.<verb>`", true},
{"the mesh-controller module is gone.", false},
{"located in mesh-controller#246", false},
{"ask mesh-module-assigner.assignments", false},
} {
got := checkAddresses([]text{{file: "m/a.go", line: 1, s: c.s}})
if (len(got) == 1) != c.want {
t.Errorf("checkAddresses(%q) = %q, want a finding: %v", c.s, got, c.want)
}
}
}
-7
View File
@@ -1,7 +0,0 @@
# 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.
# run("../.."): the root's lists of retired words and lines, and every module's texts.
retired-words
retired-lines
modules
+1 -1
View File
@@ -1,4 +1,4 @@
56 58
The long-running resources of this catalogue that do not say how they are ready (`health`, novox/hq ADR 0240 The long-running resources of this catalogue that do not say how they are ready (`health`, novox/hq ADR 0240
rule 8), as `module check` counts them. It may only go down: checks/health fails a change that raises it, and rule 8), as `module check` counts them. It may only go down: checks/health fails a change that raises it, and
one that lowers it writes the new number on the first line. From 2026-11-18, or once it is 0, a long-running one that lowers it writes the new number on the first line. From 2026-11-18, or once it is 0, a long-running
+2 -11
View File
@@ -1,14 +1,5 @@
module adwaita module adwaita
go 1.26.0 go 1.22
require git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 require git.novox.be/novox/mesh-sdk/go v0.1.7
require (
github.com/klauspost/compress v1.20.1 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
+2 -12
View File
@@ -1,12 +1,2 @@
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= git.novox.be/novox/mesh-sdk/go v0.1.7 h1:C0sTQmtTiyYH7bnqZb7PusXnqA37gKuT7Nqjn9gG47w=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= git.novox.be/novox/mesh-sdk/go v0.1.7/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
@@ -1,9 +0,0 @@
# 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 copies of this package in every module that carries one.
modules/xorg/internal/desktop/*.go
modules/lemurs/internal/desktop/*.go
modules/i3/internal/desktop/*.go
modules/xterm/internal/desktop/*.go
modules/adwaita/internal/desktop/*.go
+28 -32
View File
@@ -6,34 +6,38 @@ directory and the file the mesh writes there from its setting `older-store-names
## Why a module beside the store ## Why a module beside the store
The store is the `distribution` module, the registry seats' holder. These tools became a module of their own when the The store is the `distribution` module. A module that provides the artifact store cannot also build an
indexer's judge refused a store holder that builds an artifact; that rule is gone (issue 1966), and the sweep stays its artifact: building publishes to the store, so the store would be needed to create itself, and the
own module, holding the seat `mesh-artifact-sweep`. It requires no provision: what it holds, releases and removes it controller's module check refuses that manifest. So the store's tools are a second module, as the
asks the registry seats `oci-artifact-registry` and `container-image-registry` (see *The sweep through the registry controller's refusal says to do. **Assign it to the machine that holds the store**: its tools read the
seats*), and what the store holds it reads through the same seats' `list` (novox/hq ADR 0632 §4): it reads neither the store's files through the store's own container, and anywhere else they answer that the container is not
store's files nor its read side, so it runs on any machine. on that machine.
## Tools ## Tools
| tool | | what | | tool | | what |
|---|---|---| |---|---|---|
| `artifact_store_repositories` | r | every repository: tags and what each names, how many manifests (tagged or not), size | | `artifact_store_repositories` | r | every repository: tags and what each names, how many manifests (tagged or not), size |
| `artifact_store_usage` | r | the store's size, each manifest's as its seat says it, and the largest repositories | | `artifact_store_usage` | r | the store's size, the largest repositories, bytes shared between repositories, bytes no manifest marks (what the nightly collector frees next) |
| `artifact_store_references` | r | what the controller's records and the catalogue's definitions say of each manifest, counted and sized by state and repository; each manifest listed when one repository is asked; what the records keep that the store does not hold | | `artifact_store_references` | r | what the controller's records and the catalogue's definitions say of each manifest, counted and sized by state and repository; each manifest listed when one repository is asked; what the records keep that the store does not hold |
| `artifact_store_collect` | a | a dry run unless `dry_run` is false: what the controller would let go of, and the bytes the nightly collector would free then and now. A real run needs `why` and asks the controller's `collect` | | `artifact_store_collect` | a | a dry run unless `dry_run` is false: what the controller would let go of, and the bytes the nightly collector would free then and now. A real run needs `why` and asks the controller's `collect` |
### How the store is read ### How the store is read
The store's read side lists repositories and tags, but not a manifest no tag names, and the mesh pins every The store's door lists repositories and tags, but not a manifest no tag names, and the mesh pins every
machine by digest, so that is most of them. So the bundle asks the two registry seats' `list` machine by digest, so that is most of them. So the bundle reads the store's own files through its own
(`container-image-registry` for images and indexes, `oci-artifact-registry` for bundles and archives), which the container (`docker exec mesh-registry`, busybox `find`, `stat` and `cat`), and only reads them: every
store's holder answers from the store's own files: each manifest's digest, whether it is an index and the manifests it blob with its size, every manifest each repository holds, what each tag names, and each manifest's
names, the tags naming it, its size and whether a hold keeps it (novox/hq ADR 0632 §4). content, four hundred to an exec. What it read it keeps (a manifest is named by its content, so it never
changes), and the next call reads only what is new. A manifest that could not be read is counted and
said; what it marks beyond itself is then unknown, so sizes may read low and freed bytes high, and the
answer says so.
A manifest's size is its own content and every blob it names, as its seat says it, so a layer two manifests share docker runs as the tool runner's account; a socket that refuses it is asked again through `sudo -n`,
is counted in each, and the bytes the store's nightly collector frees are the sizes of the manifests that go, an never with a prompt, as the container runtime's own tools do.
upper bound. The address the bundle says the store is at comes from `${seat:container-image-registry:reach}`, the
image seat holder's listen (ADR 0632 §1), and is used for the catalogue's definitions only, never to reach the store. A manifest *marks* its own content, its configuration and its layers, and an index marks the manifests
it lists. The store's collector removes every blob no manifest marks, so these numbers are its own.
### The states of a manifest ### The states of a manifest
@@ -57,7 +61,7 @@ record names leaves its platform manifests unrecorded with it.
**What the definitions pin** (hq issue 323). A definition may pin an image of the store the mesh did not **What the definitions pin** (hq issue 323). A definition may pin an image of the store the mesh did not
make: pushed before the mesh built it, and pinned under the store's current name or a name it had before. make: pushed before the mesh built it, and pinned under the store's current name or a name it had before.
No record of the controller names it, yet a machine runs it, so `artifact_store_references` reads every No record of the controller names it, yet a machine runs it, so `artifact_store_references` reads every
definition the catalogue holds (the seat mesh-catalog's verbs `modules`, then `module` for each, eight at definition the catalogue holds (`mesh-catalog.catalog_modules`, then `catalog_module` for each, eight at
a time and within twenty seconds) and finds every `<name>/<repository>@sha256:…` in any of its strings, a time and within twenty seconds) and finds every `<name>/<repository>@sha256:…` in any of its strings,
under each of the store's names: `artifact-store://`, the store's address as the controller answers it, under each of the store's names: `artifact-store://`, the store's address as the controller answers it,
and the names it had before. Those are the mesh's setting `older-store-names` (space-separated, `none` by and the names it had before. Those are the mesh's setting `older-store-names` (space-separated, `none` by
@@ -95,27 +99,19 @@ manifest that stays and would lose a platform it names — to be read before a r
go test ./... go test ./...
``` ```
Against a fake of the two registry seats' `list`: the store read through it (an index and the manifests it names, a Against a fake container that prints what the two scripts would: the listing parsed (a repository name
bundle's holder, a hold, the seat's sizes), every state, what the records keep that the store lacks, bytes freed as the with slashes, a cut listing refused), manifests read with one absent or unreadable said as unread, a
sizes of what goes, a dry run never confirming, a real run without `why` refused before anything is asked, digest never becoming a path outside the blobs, the mark (an index, a shared blob, a blob nothing marks),
every state, what the records keep that the store lacks, bytes freed counting a blob shared with a kept
manifest as kept, a dry run never confirming, a real run without `why` refused before anything is asked,
the fall-back when the references let go of cannot be carried, a manifest a definition pins under the the fall-back when the references let go of cannot be carried, a manifest a definition pins under the
store's current or an older name, a definition not read said first, and that the store's current or an older name, a definition not read said first, escalation through `sudo -n`, and that the
tools served are exactly the manifest's `tools` and its `invokes` exactly the two controller verbs and the tools served are exactly the manifest's `tools` and its `invokes` exactly the two controller verbs and the
catalogue's two tools. catalogue's two tools.
Any ask other than a seat's `list` fails the read tools' tests. Any command other than the two read-only scripts fails the test.
## Files it does not know ## Files it does not know
Each directory or archive this module declares says how it treats a file it did not write (ADR 0422, ADR 0416). Each directory or archive this module declares says how it treats a file it did not write (ADR 0422, ADR 0416).
- `${dir:state}`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2). - `${dir:state}`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
### The sweep through the registry seats
The sweep writes nothing to the store's door (CAT-AS1, ADR 0617 §2): it asks the registry seats the store's holder
serves. Each kept bundle or archive is held on `oci-artifact-registry` and each kept copy of an upstream base on
`container-image-registry` (`hold`, with why it is kept) before anything goes; each eligible one is released, then
removed for `retention` on its seat (`release`, `remove`). It reads a reference in both forms, `artifact-store://<path>`
and `<seat>/<module>/<artifact>@<digest>`, as one artifact, and says artifacts-gone in the form the build named it. A
call a seat refuses stops the sweep and writes the finding `sweep-refused` in the module's findings; the next sweep no
seat refuses clears it (`sweepseats.go`).
@@ -23,11 +23,10 @@ import (
// manifest's. // manifest's.
const NamesFileEnv = "MESH_ARTIFACT_STORE_NAMES_FILE" const NamesFileEnv = "MESH_ARTIFACT_STORE_NAMES_FILE"
// CatalogueModules and CatalogueModule are the verbs of the seat mesh-catalog these definitions are read through // CatalogueModules and CatalogueModule are the catalogue's tools these definitions are read through.
// (ADR 0604 §3); the answers are catalogue.CatalogModules and catalogue.CatalogOne.
const ( const (
CatalogueModules = "seat:mesh-catalog.modules" CatalogueModules = "mesh-catalog.catalog_modules"
CatalogueModule = "seat:mesh-catalog.module" CatalogueModule = "mesh-catalog.catalog_module"
) )
// DefinitionsTimeout bounds reading every definition, below the runtime's thirty-second call limit; what // DefinitionsTimeout bounds reading every definition, below the runtime's thirty-second call limit; what
@@ -144,21 +143,21 @@ func PinsIn(manifest json.RawMessage, names StoreNames) []pinned {
return out return out
} }
// Definitions is what the catalog's definitions pin in the store, and what of them could not be read. // Definitions is what the catalogue's definitions pin in the store, and what of them could not be read.
type Definitions struct { type Definitions struct {
// Pins are by Key(repository, digest). // Pins are by Key(repository, digest).
Pins map[string][]Pin Pins map[string][]Pin
// Read is how many definitions were read; Unread says, per module, why its definition was not. // Read is how many definitions were read; Unread says, per module, why its definition was not.
Read int Read int
Unread map[string]string Unread map[string]string
// NotAsked says why the catalog could not be asked at all, when it could not. // NotAsked says why the catalogue could not be asked at all, when it could not.
NotAsked string NotAsked string
// NamesNotRead says why the store's older names could not be read, when they could not. // NamesNotRead says why the store's older names could not be read, when they could not.
NamesNotRead string NamesNotRead string
Names StoreNames Names StoreNames
} }
// Catalogue reads the definitions through the catalog's tools, and keeps each definition it read by its // Catalogue reads the definitions through the catalogue's tools, and keeps each definition it read by its
// module and commit, which never change together. // module and commit, which never change together.
type Catalogue struct { type Catalogue struct {
Ask Asking Ask Asking
@@ -172,21 +171,21 @@ type catalogueEntry struct {
Commit string `json:"commit"` Commit string `json:"commit"`
} }
// Read asks the catalog for every module and reads each definition, bounded by DefinitionsTimeout. // Read asks the catalogue for every module and reads each definition, bounded by DefinitionsTimeout.
func (c *Catalogue) Read(ctx context.Context, names StoreNames) *Definitions { func (c *Catalogue) Read(ctx context.Context, names StoreNames) *Definitions {
defs := &Definitions{Pins: map[string][]Pin{}, Unread: map[string]string{}, Names: names} defs := &Definitions{Pins: map[string][]Pin{}, Unread: map[string]string{}, Names: names}
if c == nil || c.Ask == nil { if c == nil || c.Ask == nil {
defs.NotAsked = "this bundle has no way to ask the catalog" defs.NotAsked = "this bundle has no way to ask the catalogue"
return defs return defs
} }
raw, err := c.Ask(CatalogueModules, map[string]any{}) raw, err := c.Ask(CatalogueModules, map[string]any{})
if err != nil { if err != nil {
defs.NotAsked = "the catalog's modules could not be asked: " + err.Error() defs.NotAsked = "the catalogue's modules could not be asked: " + err.Error()
return defs return defs
} }
answer, output, ok := answerOf(raw) answer, output, ok := answerOf(raw)
if !ok { if !ok {
defs.NotAsked = "the catalog refused its modules: " + lastLine(output) defs.NotAsked = "the catalogue refused its modules: " + lastLine(output)
return defs return defs
} }
var list struct { var list struct {
@@ -194,7 +193,7 @@ func (c *Catalogue) Read(ctx context.Context, names StoreNames) *Definitions {
Error string `json:"error"` Error string `json:"error"`
} }
if err := json.Unmarshal(answer, &list); err != nil || list.Error != "" || list.Modules == nil { if err := json.Unmarshal(answer, &list); err != nil || list.Error != "" || list.Modules == nil {
defs.NotAsked = "the catalog's modules answer is not readable: " + firstOf(list.Error, errText(err), lastLine(output)) defs.NotAsked = "the catalogue's modules answer is not readable: " + firstOf(list.Error, errText(err), lastLine(output))
return defs return defs
} }
c.mu.Lock() c.mu.Lock()
@@ -280,8 +279,11 @@ func (c *Catalogue) definition(m catalogueEntry, names StoreNames, nameKey strin
return pins, "" return pins, ""
} }
} }
// The verb takes no commit: it answers the trunk's module, and the answer names the commit it is at. args := map[string]any{"module": m.Module}
raw, err := c.Ask(CatalogueModule, map[string]any{"module": m.Module}) if m.Commit != "" {
args["commit"] = m.Commit
}
raw, err := c.Ask(CatalogueModule, args)
if err != nil { if err != nil {
return nil, "could not be asked: " + err.Error() return nil, "could not be asked: " + err.Error()
} }
@@ -291,7 +293,6 @@ func (c *Catalogue) definition(m catalogueEntry, names StoreNames, nameKey strin
} }
var one struct { var one struct {
Module *struct { Module *struct {
Commit string `json:"commit"`
Manifest json.RawMessage `json:"manifest"` Manifest json.RawMessage `json:"manifest"`
} `json:"module"` } `json:"module"`
Error string `json:"error"` Error string `json:"error"`
@@ -300,11 +301,9 @@ func (c *Catalogue) definition(m catalogueEntry, names StoreNames, nameKey strin
return nil, "not readable: " + firstOf(one.Error, errText(err), "no manifest in the answer") return nil, "not readable: " + firstOf(one.Error, errText(err), "no manifest in the answer")
} }
pins := PinsIn(one.Module.Manifest, names) pins := PinsIn(one.Module.Manifest, names)
// Kept by the commit the answer names, which may be newer than the list's: a definition and its commit never if m.Commit != "" {
// change together.
if at := firstOf(one.Module.Commit, m.Commit); at != "" {
c.mu.Lock() c.mu.Lock()
c.kept[m.Module+"@"+at+"|"+nameKey] = pins c.kept[key] = pins
c.mu.Unlock() c.mu.Unlock()
} }
return pins, "" return pins, ""
@@ -152,8 +152,8 @@ func TestOlderNamesNotReadAreSaidFirst(t *testing.T) {
// references is one artifact_store_references tool, kept across calls as the runtime keeps the bundle. // references is one artifact_store_references tool, kept across calls as the runtime keeps the bundle.
func references(t *testing.T, a *asked) func(map[string]any) (any, error) { func references(t *testing.T, a *asked) func(map[string]any) (any, error) {
f := world() f := world()
s := &Store{Ask: f.ask(t)} s := &Store{Container: "mesh-registry", Run: f.run(t)}
for _, tl := range Tools(s, recordsAsked{a}, &Catalogue{Ask: a.ask}) { for _, tl := range Tools(s, Controller{Ask: a.ask}, &Catalogue{Ask: a.ask}) {
if tl.Name == "artifact_store_references" { if tl.Name == "artifact_store_references" {
return tl.Run return tl.Run
} }
@@ -211,7 +211,7 @@ func TestReferencesReadTheDefinitionsUnderTheStoresOlderNames(t *testing.T) {
} }
} }
if before != 2 || after != 2 { if before != 2 || after != 2 {
t.Errorf("asked the seat's module %d times, then %d: each definition at its commit is read once", before, after) t.Errorf("asked catalog_module %d times, then %d: each definition at its commit is read once", before, after)
} }
} }
@@ -1,168 +0,0 @@
package main
// The artifact store, through its seats, and the bus, as the holder reaches them through this module's runtime.
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"sync/atomic"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/views"
)
// Gone is the answer when the store does not hold it: the outcome wanted, already true.
var Gone = errors.New("the store does not hold it")
// ErrNotOurs is a reference this sweep will not address: not the mesh's own, or naming nothing the store holds by digest.
//
// **A fact about the record, not about the store** (novox/hq issue 226). The two deserve opposite responses (skip one and
// go on, abandon the sweep for the other), and collapsing them into "an error" is how a cautious loop became one that did
// nothing while reporting the right number.
var ErrNotOurs = errors.New("not a reference into the mesh's artifact store")
// door is the store as its seats' list answers it (ADR 0632 §4), at the address this machine reaches it by: it reads
// only; what the sweep holds and removes it asks the registry seats too (sweepseats.go).
type door struct {
address string
store *Store
}
func (d *door) Address() string { return d.address }
// Holds is whether the store holds what a reference names: an image's manifest, a bundle's or an archive's blob. Gone when
// its seat lists none (ADR 0526 clause 3: a registration by hand is refused at bytes that are not there).
func (d *door) Holds(_ context.Context, r string) error {
_, err := d.store.listed(r)
return err
}
// IsIndex is whether a reference is an index the store holds, as its seat lists it. Gone when it does not hold it.
func (d *door) IsIndex(_ context.Context, r string) (bool, error) {
e, err := d.store.listed(r)
return e.Index, err
}
// DoorAt reads the store's address (host:port) from the file the manifest fills from the seat container-image-registry's
// reach, each time, so a store that moved is said where it is now; the store is read through s.
func DoorAt(file string, s *Store) func() (Door, error) {
return func() (Door, error) {
if file == "" {
return nil, fmt.Errorf("this bundle was given no file naming the artifact store's address")
}
raw, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("the artifact store's address: %w", err)
}
address := strings.TrimSpace(string(raw))
if address == "" {
return nil, fmt.Errorf("the artifact store's address is empty in %s", file)
}
return &door{address: address, store: s}, nil
}
}
// runtimeBus is the holder's bus through the runtime: its reads, its findings and its seat's event.
type runtimeBus struct{ views *views.Reader }
// followed says whether the state is the indexer's or the assigner's, which the holder asks once and follows by events
// (ADR 0615) rather than reads.
func (b runtimeBus) followed(state string) bool { return b.views != nil && b.views.Follows(state) }
func (b runtimeBus) Keys(state string) ([]string, error) {
if b.followed(state) {
return b.views.Keys(state)
}
return stdio.State(state).Keys()
}
func (b runtimeBus) Get(state, key string) ([]byte, bool, error) {
if b.followed(state) {
return b.views.Get(state, key)
}
e, err := stdio.State(state).Get(key)
if err != nil || e == nil {
return nil, false, err
}
return e.Value, true, nil
}
func (runtimeBus) History(state, key string) ([]stdio.StateRevision, error) {
return stdio.State(state).History(key)
}
func (runtimeBus) LogSince(read string, at time.Time) ([]stdio.LogEntry, error) {
var out []stdio.LogEntry
page, err := stdio.Log(read).Since(at, "", 0)
for err == nil {
out = append(out, page.Entries...)
if page.Next == 0 {
return out, nil
}
page, err = stdio.Log(read).Read(page.Next, "", 0)
}
return nil, err
}
func (runtimeBus) Put(state, key string, value any) error {
_, err := stdio.State(state).Put(key, value)
return err
}
func (runtimeBus) Delete(state, key string) error { return stdio.State(state).Delete(key) }
func (runtimeBus) Publish(subject string, body any) error {
return stdio.SeatPublish(subject, body, "")
}
// follow watches the builder's done keys and the wants: a build's end sweeps and
// observes again; a want changed observes again. Event-driven: nothing reads them on a timer.
func follow(h *Holder, v *views.Reader) {
<-stdio.Served()
watch := func(state string, handle func(stdio.StateChange) error) {
for {
if err := stdio.State(state).Watch("", handle); err == nil {
return
} else {
h.logf("watching %s: %v; asked again in 10s", state, err)
}
time.Sleep(10 * time.Second)
}
}
observe := func() {
if err := h.Observe(context.Background()); err != nil {
h.logf("image-missing could not be observed: %v", err)
}
}
watch(BuildsRead, func(c stdio.StateChange) error {
if c.Current || c.Op != stdio.OpPut {
return nil
}
var b struct {
State string `json:"state"`
}
if json.Unmarshal(c.Value, &b) == nil && b.State == "done" {
go func() { h.AfterBuild(c.Key); observe() }()
}
return nil
})
// The wants are the assigner's: the first view then its events (ADR 0615). What Watch hands over before it returns
// is the first view, not a change.
var current atomic.Bool
current.Store(true)
for {
err := v.Watch(WantsRead, func(string, []byte, bool) {
if !current.Load() {
go observe()
}
})
if err == nil {
break
}
h.logf("watching %s: %v; asked again in 10s", WantsRead, err)
time.Sleep(10 * time.Second)
}
current.Store(false)
observe()
}
@@ -1,39 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"sort"
sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep"
)
// The holders whose states this module reads, one constant each (a reader names its holder once; ADR 0634 §4).
const (
assignerModule = "mesh-delivery-assigner"
indexerModule = "mesh-delivery-indexer"
)
// The seat mesh-artifact-sweep types the answer of its verb held and the body of its event artifacts-gone; this module
// holds the seat, so it answers and says them in those types.
type (
// HeldAnswer is the answer of the seat's verb held.
HeldAnswer = sweep.HeldAnswer
// ArtifactsGone is the body of the seat's event artifacts-gone.
ArtifactsGone = sweep.ArtifactsGone
)
// GoneSubject is where the seat says artifacts-gone.
const GoneSubject = sweep.SubjectArtifactsGone
// EncodeArtifactsGone is the event's body, the references sorted; it refuses one naming no module, no fingerprint or no
// gone reference.
func EncodeArtifactsGone(e ArtifactsGone) ([]byte, error) {
e.Event = sweep.EventArtifactsGone
e.Gone = append([]string(nil), e.Gone...)
sort.Strings(e.Gone)
if e.Module == "" || e.Fingerprint == "" || len(e.Gone) == 0 {
return nil, fmt.Errorf("%s names a module, a fingerprint and at least one gone reference", sweep.EventArtifactsGone)
}
return json.Marshal(e)
}

Some files were not shown because too many files have changed in this diff Show More