mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group feat/health-the-field delivering: 1 of 3 delivered
mesh/delivery held for a person: merged, and the controller opened no walk for it within 10m0s — nothing it holds follows that branch, or the merge was…
A module could say nothing about what ready means for what it runs, so a web application with its port open and its requests hanging passed everything for eleven hours (issue 145). A long-running resource now carries `health` — the image's own check adopted by name, http, tcp, exec, unit or a module's own tool, with its timing — refused near its author when it names a port or an address, an endpoint the module does not declare, a tool it does not serve, a tool check alone, or a timing outside the record's bounds. It is composed with the endpoint as the port this machine published it on, and sent only to a node-engine whose statement says it reads it: an older one would refuse the whole declaration. The engine is granted its own machine's instance of each health tool. `module check` warns of every long-running resource without `health`, counts them for the catalogue, and refuses them from 2026-11-18. A check's findings stay out of a condition's summary. The node-engine's validator is vendored at its Phase B commit, so what is composed is judged by the words the engine takes.
231 lines
8.3 KiB
Go
231 lines
8.3 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
)
|
|
|
|
// moduleCheck judges manifests where they are written, with no mesh (novox/hq ADR 0037, issue 148).
|
|
//
|
|
// **The same functions registration runs, and nothing the command line adds** (ADR 0035): the strict
|
|
// parse with every per-manifest problem, then the rules no single manifest can be judged against,
|
|
// over exactly the manifests given. Somebody describing their own application in their own
|
|
// repository runs this before pushing and finds out there, rather than when a running mesh refuses
|
|
// the registration or, later, when a machine applies something that resolved and should not have.
|
|
//
|
|
// **What it cannot know without a store, it says.** The mesh's own seat set is the store's (ADR
|
|
// 0122); this binary carries a compiled copy that the store overrides when loaded, so a claim on a
|
|
// mesh seat is judged fully only at registration. A seat another module declares is unknown unless
|
|
// that module's manifest is passed too. Both are printed as a note, not as a problem — a check that
|
|
// refused what it could not see would teach people to ignore it.
|
|
//
|
|
// **And every identity against every bound it meets** (novox/hq ADR 0225, issue 263): each module's
|
|
// identity, on a machine whose name is `longestMachine` characters, against the bound of every
|
|
// provision it wants that a manifest given here offers. An overflow is refused in the pull request
|
|
// that introduces it — a new requirement, a lowered bound, a longer slug — instead of on the
|
|
// provider's machine when a real machine's name first meets the module's.
|
|
func moduleCheck(paths []string, out io.Writer) error {
|
|
return moduleCheckFor(paths, catalogue.DefaultLongestMachine, out)
|
|
}
|
|
|
|
func moduleCheckFor(paths []string, longestMachine int, out io.Writer) error {
|
|
if len(paths) == 0 {
|
|
return errors.New("module check <manifest.json>... — one file per module; pass every " +
|
|
"manifest of a repository together so the rules between them are checked too")
|
|
}
|
|
shelf := catalogue.Shelf{}
|
|
faulted := map[string]bool{}
|
|
failed := 0
|
|
for _, path := range paths {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
fmt.Fprintf(out, "%s: %v\n", path, err)
|
|
failed++
|
|
continue
|
|
}
|
|
m, err := catalogue.ParseManifest(raw)
|
|
if err != nil {
|
|
fmt.Fprintf(out, "%s: %v\n", path, err)
|
|
failed++
|
|
continue
|
|
}
|
|
if first, twice := shelf[m.Module]; twice {
|
|
_ = first
|
|
fmt.Fprintf(out, "%s: %s was already given; two manifests name one module\n", path, m.Module)
|
|
failed++
|
|
continue
|
|
}
|
|
// A definition names no installation (novox/hq ADR 0112, ADR 0155): judged here, in the
|
|
// catalogue-wide test, and at registration, which refuses in the same words.
|
|
if named := catalogue.InstallationProblems(m); len(named) > 0 {
|
|
for _, p := range named {
|
|
fmt.Fprintf(out, "%s: %s\n", path, p)
|
|
}
|
|
failed += len(named)
|
|
faulted[m.Module] = true
|
|
}
|
|
shelf[m.Module] = m
|
|
}
|
|
|
|
// Between the manifests: a seat declared twice, a use of a seat nothing declares, a claim on
|
|
// a seat that does not exist. Run only over what parsed, because a problem inside one manifest
|
|
// has already been said and would be said again here in a worse form.
|
|
problems := catalogue.CatalogueProblems(shelf)
|
|
sort.Strings(problems)
|
|
for _, p := range problems {
|
|
fmt.Fprintln(out, p)
|
|
}
|
|
failed += len(problems)
|
|
|
|
// Between the manifests too: an identity against the bounds of the provisions it wants, which
|
|
// only the provider's manifest states.
|
|
identities := catalogue.IdentityProblems(shelf, longestMachine)
|
|
sort.Strings(identities)
|
|
for _, p := range identities {
|
|
fmt.Fprintln(out, p)
|
|
}
|
|
failed += len(identities)
|
|
|
|
// And the data each module keeps (novox/hq ADR 0233): a provider that grants says what it keeps for
|
|
// its consumers, a directory a container writes is declared, and no backup line is written by hand.
|
|
data := catalogue.DataProblems(shelf)
|
|
sort.Strings(data)
|
|
for _, p := range data {
|
|
fmt.Fprintln(out, p)
|
|
}
|
|
failed += len(data)
|
|
|
|
var names []string
|
|
for name := range shelf {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
|
|
// **Every long-running resource says how it is ready** (novox/hq ADR 0240 rule 8): warned until the
|
|
// date, refused from it. The count is the catalogue's: its merge check keeps the number and lets a
|
|
// change lower it, never raise it.
|
|
undeclared := 0
|
|
required := !checkNow().Before(catalogue.HealthRequiredFrom)
|
|
for _, name := range names {
|
|
missing := catalogue.Undeclared(shelf[name])
|
|
undeclared += len(missing)
|
|
if len(missing) == 0 {
|
|
continue
|
|
}
|
|
if required {
|
|
for _, id := range missing {
|
|
fmt.Fprintf(out, "%s: %s stays up and does not say how it is ready: a long-running resource declares "+
|
|
"health since %s (novox/hq ADR 0240 rule 8)\n", name, id, catalogue.HealthRequiredFrom.Format("2006-01-02"))
|
|
}
|
|
failed += len(missing)
|
|
faulted[name] = true
|
|
}
|
|
}
|
|
|
|
for _, name := range names {
|
|
m := shelf[name]
|
|
if faulted[name] {
|
|
continue
|
|
}
|
|
fmt.Fprintf(out, "%s: ok", name)
|
|
if n := len(m.Tools); n > 0 {
|
|
fmt.Fprintf(out, ", %d tool(s)", n)
|
|
}
|
|
if len(m.Invokes) > 0 {
|
|
fmt.Fprintf(out, ", invokes %s", joinInvokes(m.Invokes))
|
|
}
|
|
// The state it keeps and reads (novox/hq ADR 0201), so a reviewer sees what lands on the bus.
|
|
if len(m.State) > 0 {
|
|
kept := make([]string, 0, len(m.State))
|
|
for _, s := range m.State {
|
|
kept = append(kept, s.Name)
|
|
}
|
|
fmt.Fprintf(out, ", keeps state %s", strings.Join(kept, ", "))
|
|
}
|
|
if len(m.Reads) > 0 {
|
|
fmt.Fprintf(out, ", reads %s", strings.Join(m.Reads, ", "))
|
|
}
|
|
// The data it keeps, by class, so a reviewer sees what the mesh will protect and how.
|
|
if items := m.DataItems(); len(items) > 0 {
|
|
kept := make([]string, 0, len(items))
|
|
for _, it := range items {
|
|
kept = append(kept, it.ID+" ("+it.Class+")")
|
|
}
|
|
fmt.Fprintf(out, ", keeps %s", strings.Join(kept, ", "))
|
|
}
|
|
// How what it runs is ready (ADR 0240): each declared check, and what is judged by liveness alone.
|
|
var checks []string
|
|
for _, r := range m.Resources {
|
|
if h, has, _ := catalogue.ReadHealth(r); has {
|
|
checks = append(checks, fmt.Sprintf("%v by %s", r["id"], catalogue.HealthWords(h)))
|
|
}
|
|
}
|
|
if len(checks) > 0 {
|
|
fmt.Fprintf(out, ", ready: %s", strings.Join(checks, "; "))
|
|
}
|
|
if missing := catalogue.Undeclared(m); len(missing) > 0 {
|
|
fmt.Fprintf(out, "; WARNING: %s stay(s) up and say(s) not how it is ready — judged by liveness alone, "+
|
|
"refused from %s (ADR 0240 rule 8)", strings.Join(missing, ", "), catalogue.HealthRequiredFrom.Format("2006-01-02"))
|
|
}
|
|
fmt.Fprintln(out)
|
|
}
|
|
// The count the catalogue keeps (ADR 0240 rule 8), in a line its merge check reads.
|
|
fmt.Fprintf(out, "%s %d\n", UndeclaredHealthLine, undeclared)
|
|
if failed > 0 {
|
|
return fmt.Errorf("%d problem(s) in %d manifest(s)", failed, len(paths))
|
|
}
|
|
fmt.Fprintf(out, "%d manifest(s) checked. Judged against the seats this binary carries; a claim on "+
|
|
"one of the mesh's own seats is judged fully at registration, and a seat declared by a "+
|
|
"module not given here reads as unknown. Identities judged on a %d-character machine name, "+
|
|
"against the bounds of the providers given here\n", len(paths), longestMachine)
|
|
return nil
|
|
}
|
|
|
|
// UndeclaredHealthLine starts the line `module check` says the count of long-running resources without
|
|
// `health` in, over the manifests given: the catalogue's merge check compares it with the number it keeps.
|
|
const UndeclaredHealthLine = "long-running resources without health:"
|
|
|
|
// checkNow is the clock `module check` judges the date by; a test sets it.
|
|
var checkNow = time.Now
|
|
|
|
func joinInvokes(invokes []string) string {
|
|
if len(invokes) == 1 && invokes[0] == "*" {
|
|
return "every tool"
|
|
}
|
|
s := ""
|
|
for i, t := range invokes {
|
|
if i > 0 {
|
|
s += ", "
|
|
}
|
|
s += t
|
|
}
|
|
return s
|
|
}
|
|
|
|
// manifestsUnder lists every module.json below a directory, for `module check <dir>`.
|
|
func manifestsUnder(dir string) ([]string, error) {
|
|
var found []string
|
|
err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() && (d.Name() == "node_modules" || d.Name() == ".git" || d.Name() == "dist") {
|
|
return filepath.SkipDir
|
|
}
|
|
if !d.IsDir() && d.Name() == "module.json" {
|
|
found = append(found, path)
|
|
}
|
|
return nil
|
|
})
|
|
sort.Strings(found)
|
|
return found, err
|
|
}
|