Merge pull request 'R4g+R5 (hq): its checks as Go tests; design 47, the glossary and playbooks say what R4g builds (design 47)' (#470) from feat/r4g into main
Reviewed-on: #470
This commit was merged in pull request #470.
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# Checks
|
||||
|
||||
Since ADR 0527 the same four checks are also Go tests, in this folder (`*_test.go`) with `go.mod` at the root: run
|
||||
`go test ./...` from the root. Each check is a function over a file tree that returns its problems, tested on hq's tree
|
||||
and on a deliberately broken tree built in memory. The Python files and `merge-check.sh` run beside them until the
|
||||
delivery of R4g is done and hq's trunk requires `mesh/check` (ADR 0527 §3); then they go (issue 1296).
|
||||
|
||||
```
|
||||
python3 00-META/checks/records.py structure: links, no record written here, citations in the form "ADR NNNN"
|
||||
python3 00-META/checks/index.py the reading order's topics are written, and no list of records is stored
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
// The development cycle, checked (cycle.py until ADR 0527; ADR 0080). Everything here reads frontmatter only,
|
||||
// because a status this repository holds lives in frontmatter and nowhere else. The decision records and research
|
||||
// efforts live in the knowledge base (ADR 0290), whose rules judge them.
|
||||
|
||||
var designStatuses = map[string]bool{"proposed": true, "designed": true, "in-progress": true, "implemented": true, "abandoned": true}
|
||||
|
||||
// The keys an issue report carried while its status lived here. The issue tracker holds them now (ADR 0275), and
|
||||
// `issue-tracker.move` enforces the cycle's rules at the write. A report carrying one is a status in two places.
|
||||
var trackerKeys = []string{"status", "located-in", "fixed-by", "amended-design", "replay", "replay-none", "resolved"}
|
||||
|
||||
// checkDesigns: every design parses, carries `layer:` matching its folder and a known `status:`; a to-be design
|
||||
// names a decision, and once in-progress or implemented names its owning code. As-is designs need no decisions.
|
||||
func checkDesigns(fsys fs.FS) []string {
|
||||
var out []string
|
||||
for _, layer := range []struct{ folder, name string }{{"00-as-is", "as-is"}, {"01-to-be", "to-be"}} {
|
||||
entries, _ := fs.ReadDir(fsys, "03-DESIGN/"+layer.folder)
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") || e.Name() == "README.md" {
|
||||
continue
|
||||
}
|
||||
p := path.Join("03-DESIGN", layer.folder, e.Name())
|
||||
front := frontmatter(readFile(fsys, p))
|
||||
if front == nil {
|
||||
out = append(out, "cycle: "+p+": no frontmatter")
|
||||
continue
|
||||
}
|
||||
if front.scalar("layer") != layer.name {
|
||||
out = append(out, fmt.Sprintf("cycle: %s: layer is %q; this directory is %s", p, front.scalar("layer"), layer.name))
|
||||
}
|
||||
status := front.scalar("status")
|
||||
if !designStatuses[status] {
|
||||
var known []string
|
||||
for s := range designStatuses {
|
||||
known = append(known, s)
|
||||
}
|
||||
sort.Strings(known)
|
||||
out = append(out, fmt.Sprintf("cycle: %s: status %q is not one of %v", p, status, known))
|
||||
}
|
||||
if layer.name == "to-be" {
|
||||
if len(front.list("decisions")) == 0 {
|
||||
out = append(out, "cycle: "+p+": names no decisions -- no design without a decision")
|
||||
}
|
||||
if (status == "in-progress" || status == "implemented") && len(front.list("code")) == 0 {
|
||||
out = append(out, fmt.Sprintf("cycle: %s: status %s but code: names no owner -- no development without a design that says where", p, status))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// checkIssues: no two issue folders share a number (issue 155: the number is how a record is cited), every report
|
||||
// has frontmatter with `opened:`, and none carries a key the tracker holds.
|
||||
func checkIssues(fsys fs.FS) []string {
|
||||
var out []string
|
||||
entries, _ := fs.ReadDir(fsys, "04-ISSUES")
|
||||
seen := map[string]string{}
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
number := strings.SplitN(e.Name(), "-", 2)[0]
|
||||
if number == "" || strings.Trim(number, "0123456789") != "" {
|
||||
continue
|
||||
}
|
||||
if first, ok := seen[number]; ok {
|
||||
out = append(out, fmt.Sprintf("cycle: 04-ISSUES/%s: is numbered %s, and so is %s -- an issue number is how it is cited, and two records "+
|
||||
"answering to one means a citation that resolves to whichever the reader opened. Take the next free number across main AND every open pull request", e.Name(), number, first))
|
||||
} else {
|
||||
seen[number] = e.Name()
|
||||
}
|
||||
}
|
||||
for _, e := range entries {
|
||||
p := path.Join("04-ISSUES", e.Name(), "00-report.md")
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
if _, err := fs.Stat(fsys, p); err != nil {
|
||||
continue
|
||||
}
|
||||
front := frontmatter(readFile(fsys, p))
|
||||
if front == nil {
|
||||
out = append(out, "cycle: "+p+": no frontmatter")
|
||||
continue
|
||||
}
|
||||
if front.scalar("opened") == "" {
|
||||
out = append(out, "cycle: "+p+": opened: says nothing -- the day the issue was opened, YYYY-MM-DD")
|
||||
continue
|
||||
}
|
||||
var carried []string
|
||||
for _, k := range trackerKeys {
|
||||
if _, ok := front[k]; ok {
|
||||
carried = append(carried, "`"+k+":`")
|
||||
}
|
||||
}
|
||||
if len(carried) > 0 {
|
||||
out = append(out, fmt.Sprintf("cycle: %s: carries %s, but an issue's status, owner, fix and replay live in the issue tracker (ADR 0275): "+
|
||||
"move it with `issue-tracker.move`, and keep no copy here", p, strings.Join(carried, ", ")))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestCycleDesignsHoldTheChain(t *testing.T) { expectNone(t, checkDesigns(rootFS())) }
|
||||
func TestCycleIssuesHoldTheChain(t *testing.T) { expectNone(t, checkIssues(rootFS())) }
|
||||
|
||||
func design(layer, status, extra string) *fstest.MapFile {
|
||||
return doc("---\nlayer: " + layer + "\nstatus: " + status + "\n" + extra + "---\nbody\n")
|
||||
}
|
||||
|
||||
func TestCycleDesignsCatchesEachBreak(t *testing.T) {
|
||||
broken := fstest.MapFS{
|
||||
"03-DESIGN/00-as-is/a-nofront.md": doc("no frontmatter"),
|
||||
"03-DESIGN/00-as-is/b-wronglayer.md": design("to-be", "implemented", ""),
|
||||
"03-DESIGN/00-as-is/c-badstatus.md": design("as-is", "done", ""),
|
||||
"03-DESIGN/01-to-be/d-nodecision.md": design("to-be", "designed", ""),
|
||||
"03-DESIGN/01-to-be/e-nocode.md": design("to-be", "in-progress", "decisions: [ADR 0001]\n"),
|
||||
"03-DESIGN/01-to-be/README.md": doc("not a design"),
|
||||
}
|
||||
got := checkDesigns(broken)
|
||||
expectProblem(t, got, "a-nofront.md: no frontmatter")
|
||||
expectProblem(t, got, `b-wronglayer.md: layer is "to-be"; this directory is as-is`)
|
||||
expectProblem(t, got, `c-badstatus.md: status "done" is not one of`)
|
||||
expectProblem(t, got, "d-nodecision.md: names no decisions")
|
||||
expectProblem(t, got, "e-nocode.md: status in-progress but code: names no owner")
|
||||
if len(got) != 5 {
|
||||
t.Errorf("exactly the five breaks, as-is needs no decisions and a README is no design: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCycleDesignsAcceptsASoundPair(t *testing.T) {
|
||||
sound := fstest.MapFS{
|
||||
"03-DESIGN/00-as-is/a.md": design("as-is", "implemented", ""),
|
||||
"03-DESIGN/01-to-be/b.md": design("to-be", "in-progress", "decisions: [ADR 0001]\ncode: [mesh-sdk]\n"),
|
||||
}
|
||||
expectNone(t, checkDesigns(sound))
|
||||
}
|
||||
|
||||
func TestCycleIssuesCatchesEachBreak(t *testing.T) {
|
||||
report := func(front string) *fstest.MapFile { return doc("---\n" + front + "---\nbody\n") }
|
||||
broken := fstest.MapFS{
|
||||
"04-ISSUES/100-a/00-report.md": report("opened: 2026-10-01\n"),
|
||||
"04-ISSUES/100-b/00-report.md": report("opened: 2026-10-01\n"),
|
||||
"04-ISSUES/101-noopened/00-report.md": report("title: x\n"),
|
||||
"04-ISSUES/102-tracker/00-report.md": report("opened: 2026-10-01\nstatus: open\nreplay: none\n"),
|
||||
"04-ISSUES/103-nofront/00-report.md": doc("plain"),
|
||||
"04-ISSUES/notes/readme.md": doc("not an issue"),
|
||||
}
|
||||
got := checkIssues(broken)
|
||||
expectProblem(t, got, "04-ISSUES/100-b: is numbered 100, and so is 100-a")
|
||||
expectProblem(t, got, "101-noopened/00-report.md: opened: says nothing")
|
||||
expectProblem(t, got, "102-tracker/00-report.md: carries `status:`, `replay:`")
|
||||
expectProblem(t, got, "103-nofront/00-report.md: no frontmatter")
|
||||
if len(got) != 4 {
|
||||
t.Errorf("exactly the four breaks: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
// The reading order of the decision records (index.py until ADR 0527): its topics are written in
|
||||
// 02-DECISIONS/README.md, its records are not. The knowledge base reads these lines and refuses a record whose
|
||||
// topic is not one of them, so the form is a contract; a topic's records are `knowledge-base.holds` with that
|
||||
// topic, never stored here (ADR 0248, issue 297).
|
||||
|
||||
const readmeOfDecisions = "02-DECISIONS/README.md"
|
||||
|
||||
// A topic line: `1. **Label** — `topic: name`. ...`
|
||||
var topicLineRx = regexp.MustCompile(`(?m)^\d+\.\s+\*\*([^*]+)\*\*\s+—\s+` + "`topic:\\s*([^`]+)`")
|
||||
|
||||
// What a stored list looks like: the old markers, or a line of the list index.py used to print.
|
||||
var storedLists = []struct {
|
||||
rx *regexp.Regexp
|
||||
what string
|
||||
}{
|
||||
{regexp.MustCompile(`<!--\s*index:(start|end)\s*-->`), "an index marker"},
|
||||
{regexp.MustCompile(`(?m)^- \*\*\d{4}\*\* — \[`), "a line of a generated list"},
|
||||
}
|
||||
|
||||
func readingOrder(text string) []string {
|
||||
var topics []string
|
||||
for _, m := range topicLineRx.FindAllStringSubmatch(text, -1) {
|
||||
topics = append(topics, strings.TrimSpace(m[2]))
|
||||
}
|
||||
return topics
|
||||
}
|
||||
|
||||
func checkIndex(fsys fs.FS) []string {
|
||||
text := readFile(fsys, readmeOfDecisions)
|
||||
topics := readingOrder(text)
|
||||
var out []string
|
||||
if len(topics) == 0 {
|
||||
out = append(out, "index: "+readmeOfDecisions+" writes no reading order: no line of the form '1. **Label** — `topic: name`. ...'; "+
|
||||
"the knowledge base refuses every record without it")
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, topic := range topics {
|
||||
if seen[topic] {
|
||||
out = append(out, fmt.Sprintf("index: %s names a topic twice in its reading order: %s", readmeOfDecisions, topic))
|
||||
break
|
||||
}
|
||||
seen[topic] = true
|
||||
}
|
||||
for _, s := range storedLists {
|
||||
if s.rx.MatchString(text) {
|
||||
out = append(out, fmt.Sprintf("index: %s stores a list of records (%s). The records live in the knowledge base, and a topic's list is "+
|
||||
"`knowledge-base.holds` with that topic", readmeOfDecisions, s.what))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestIndexReadingOrderIsWrittenAndStoresNoList(t *testing.T) {
|
||||
expectNone(t, checkIndex(rootFS()))
|
||||
if n := len(readingOrder(readFile(rootFS(), readmeOfDecisions))); n == 0 {
|
||||
t.Error("the reading order has no topic")
|
||||
}
|
||||
}
|
||||
|
||||
const sound = "1. **One** — `topic: one`. x\n2. **Two** — `topic: two`. y\n"
|
||||
|
||||
func TestIndexCatchesAMissingReadingOrder(t *testing.T) {
|
||||
expectProblem(t, checkIndex(fstest.MapFS{readmeOfDecisions: doc("nothing written here\n")}), "writes no reading order")
|
||||
}
|
||||
|
||||
func TestIndexCatchesADuplicateTopic(t *testing.T) {
|
||||
broken := fstest.MapFS{readmeOfDecisions: doc(sound + "3. **Again** — `topic: one`. z\n")}
|
||||
expectProblem(t, checkIndex(broken), "names a topic twice")
|
||||
}
|
||||
|
||||
func TestIndexCatchesAStoredList(t *testing.T) {
|
||||
for what, stored := range map[string]string{
|
||||
"an index marker": "<!-- index:start -->\n",
|
||||
"a line of a generated list": "- **0001** — [a title](x.md)\n",
|
||||
} {
|
||||
expectProblem(t, checkIndex(fstest.MapFS{readmeOfDecisions: doc(sound + stored)}), what)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexAcceptsASoundReadme(t *testing.T) {
|
||||
expectNone(t, checkIndex(fstest.MapFS{readmeOfDecisions: doc(sound)}))
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
// Structural checks over hq's own documents (records.py until ADR 0527). The decision records and research
|
||||
// efforts live in the knowledge base (ADR 0290, issue 426); what stays here judges what this repository still
|
||||
// holds, and keeps the records from coming back.
|
||||
|
||||
var (
|
||||
linkRx = regexp.MustCompile(`\[[^\]]*\]\(([^)]+)\)`)
|
||||
// A decision record is named "ADR NNNN", four digits, and a research effort "research NNN", three.
|
||||
decisionEntryRx = regexp.MustCompile(`^ADR \d{4}$`)
|
||||
adrMentionRx = regexp.MustCompile(`\bADRs?\s+(\d+)\b`)
|
||||
researchMentionRx = regexp.MustCompile(`\b[Rr]esearch\s+(\d+)\b`)
|
||||
codeSpanRx = regexp.MustCompile("`[^`]*`")
|
||||
recordFolders = []string{"02-DECISIONS", "01-RESEARCH"}
|
||||
recordFolderPoints = map[string]bool{"02-DECISIONS/README.md": true, "01-RESEARCH/README.md": true}
|
||||
)
|
||||
|
||||
// checkLinks: every relative link resolves to something that exists.
|
||||
func checkLinks(fsys fs.FS) []string {
|
||||
var out []string
|
||||
for _, p := range markdownFiles(fsys) {
|
||||
for n, line := range strings.Split(readFile(fsys, p), "\n") {
|
||||
for _, m := range linkRx.FindAllStringSubmatch(line, -1) {
|
||||
target := m[1]
|
||||
if strings.HasPrefix(target, "http:") || strings.HasPrefix(target, "https:") || strings.HasPrefix(target, "mailto:") {
|
||||
continue
|
||||
}
|
||||
target = strings.TrimSpace(strings.SplitN(target, "#", 2)[0])
|
||||
if target == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := fs.Stat(fsys, path.Join(path.Dir(p), target)); err != nil {
|
||||
out = append(out, fmt.Sprintf("links: %s:%d: link does not resolve: %s", p, n+1, target))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// checkNoRecords: the two folders the records left hold their pointer README and nothing else (ADR 0290 rule 9).
|
||||
func checkNoRecords(fsys fs.FS) []string {
|
||||
var out []string
|
||||
for _, folder := range recordFolders {
|
||||
if _, err := fs.Stat(fsys, folder); err != nil {
|
||||
out = append(out, "no-records: "+folder+": the folder is gone; it keeps a README that points at the knowledge base")
|
||||
continue
|
||||
}
|
||||
_ = fs.WalkDir(fsys, folder, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil || p == folder || recordFolderPoints[p] {
|
||||
return nil
|
||||
}
|
||||
out = append(out, "no-records: "+p+": a decision record or research effort lives in the knowledge base, never here: "+
|
||||
"write it with knowledge-base.reserve and knowledge-base.propose (playbooks 01 and 02)")
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// checkDecisionsEntries: a design's `decisions:` names each record as "ADR NNNN".
|
||||
func checkDecisionsEntries(fsys fs.FS) []string {
|
||||
var out []string
|
||||
for _, p := range markdownFiles(fsys) {
|
||||
if !strings.HasPrefix(p, "03-DESIGN/") {
|
||||
continue
|
||||
}
|
||||
for _, e := range frontmatter(readFile(fsys, p)).list("decisions") {
|
||||
if !decisionEntryRx.MatchString(unquote(e)) {
|
||||
out = append(out, fmt.Sprintf("decisions-form: %s: decisions: entry %q is not 'ADR NNNN' (four digits)", p, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// checkMentions: in running text a decision record is "ADR NNNN" and a research effort "research NNN"; a code
|
||||
// span is an example and is not read.
|
||||
func checkMentions(fsys fs.FS) []string {
|
||||
var out []string
|
||||
for _, p := range markdownFiles(fsys) {
|
||||
for n, line := range strings.Split(readFile(fsys, p), "\n") {
|
||||
line = codeSpanRx.ReplaceAllString(line, "")
|
||||
for _, m := range adrMentionRx.FindAllStringSubmatch(line, -1) {
|
||||
if len(m[1]) != 4 {
|
||||
out = append(out, fmt.Sprintf("mention-form: %s:%d: %q: a decision record is named 'ADR NNNN', four digits", p, n+1, m[0]))
|
||||
}
|
||||
}
|
||||
for _, m := range researchMentionRx.FindAllStringSubmatch(line, -1) {
|
||||
if len(m[1]) != 3 {
|
||||
out = append(out, fmt.Sprintf("mention-form: %s:%d: %q: a research effort is named 'research NNN', three digits", p, n+1, m[0]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var codeEntryRx = regexp.MustCompile(`\s*\(.*\)$`)
|
||||
|
||||
// checkStatusAgainstCode: a to-be design naming specific code (a path after the repository) may not still be
|
||||
// `designed`. Deliberately weak: it cannot tell whether the prose is true, only that a document stopped claiming
|
||||
// to be unbuilt once it points at something. `code: [mesh-controller]` is a plan and stays `designed`.
|
||||
func checkStatusAgainstCode(fsys fs.FS) []string {
|
||||
var out []string
|
||||
for _, p := range markdownFiles(fsys) {
|
||||
if !strings.HasPrefix(p, "03-DESIGN/01-to-be/") || strings.HasSuffix(p, "README.md") {
|
||||
continue
|
||||
}
|
||||
front := frontmatter(readFile(fsys, p))
|
||||
if front.scalar("status") != "designed" {
|
||||
continue
|
||||
}
|
||||
for _, e := range front.list("code") {
|
||||
named := strings.SplitN(strings.TrimSpace(codeEntryRx.ReplaceAllString(e, "")), " ", 2)
|
||||
if len(named) > 1 {
|
||||
out = append(out, fmt.Sprintf("status-vs-code: %s: `designed`, but names %q in %s. Naming a file claims it implements this "+
|
||||
"- use `in-progress`, or `implemented` once it is defensible from that repository's main branch.", p, strings.TrimSpace(named[1]), named[0]))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func doc(s string) *fstest.MapFile { return &fstest.MapFile{Data: []byte(s)} }
|
||||
|
||||
// The real tree.
|
||||
|
||||
func TestRecordsLinksResolve(t *testing.T) { expectNone(t, checkLinks(rootFS())) }
|
||||
func TestRecordsFoldersHoldOnlyPointers(t *testing.T) { expectNone(t, checkNoRecords(rootFS())) }
|
||||
func TestRecordsDecisionsEntriesForm(t *testing.T) { expectNone(t, checkDecisionsEntries(rootFS())) }
|
||||
func TestRecordsMentionForm(t *testing.T) { expectNone(t, checkMentions(rootFS())) }
|
||||
func TestRecordsStatusAgainstCode(t *testing.T) { expectNone(t, checkStatusAgainstCode(rootFS())) }
|
||||
|
||||
// The checks themselves, each against a broken fixture, and against a sound one.
|
||||
|
||||
func TestRecordsLinksCatchesABrokenLink(t *testing.T) {
|
||||
broken := fstest.MapFS{
|
||||
"a/one.md": doc("see [two](two.md) and [gone](gone.md#x) and [web](https://example.com/x)\n"),
|
||||
"a/two.md": doc("[up](../a/one.md#top) [anchor](#here)\n"),
|
||||
}
|
||||
got := checkLinks(broken)
|
||||
expectProblem(t, got, "a/one.md:1: link does not resolve: gone.md")
|
||||
if len(got) != 1 {
|
||||
t.Errorf("only the broken link is a problem, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordsNoRecordsCatchesARecordPutBack(t *testing.T) {
|
||||
broken := fstest.MapFS{
|
||||
"02-DECISIONS/README.md": doc("pointer"),
|
||||
"02-DECISIONS/0001-x.md": doc("a record"),
|
||||
"01-RESEARCH/README.md": doc("pointer"),
|
||||
"01-RESEARCH/045/00-x.md": doc("an effort"),
|
||||
}
|
||||
got := checkNoRecords(broken)
|
||||
expectProblem(t, got, "02-DECISIONS/0001-x.md")
|
||||
expectProblem(t, got, "01-RESEARCH/045")
|
||||
expectProblem(t, checkNoRecords(fstest.MapFS{"02-DECISIONS/README.md": doc("p")}), "01-RESEARCH: the folder is gone")
|
||||
}
|
||||
|
||||
func TestRecordsDecisionsFormCatchesAShortEntry(t *testing.T) {
|
||||
broken := fstest.MapFS{
|
||||
"03-DESIGN/01-to-be/1.md": doc("---\nlayer: to-be\ndecisions:\n - ADR 0290\n - ADR 290\n---\n"),
|
||||
"00-META/x.md": doc("---\ndecisions: [ADR 5]\n---\n"),
|
||||
}
|
||||
got := checkDecisionsEntries(broken)
|
||||
expectProblem(t, got, `entry "ADR 290"`)
|
||||
if len(got) != 1 {
|
||||
t.Errorf("only a design's entries are read, and ADR 0290 is sound: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordsMentionFormCatchesShortNumbers(t *testing.T) {
|
||||
broken := fstest.MapFS{"x.md": doc("fine ADR 0290 and research 045\nbad ADR 12 and research 45\nexample `ADR 7` and `research 9`\n")}
|
||||
got := checkMentions(broken)
|
||||
expectProblem(t, got, "x.md:2: \"ADR 12\"")
|
||||
expectProblem(t, got, "x.md:2: \"research 45\"")
|
||||
if len(got) != 2 {
|
||||
t.Errorf("code spans and sound forms are not problems: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordsStatusAgainstCodeCatchesADesignedDesignNamingAFile(t *testing.T) {
|
||||
broken := fstest.MapFS{
|
||||
"03-DESIGN/01-to-be/9.md": doc("---\nstatus: designed\ncode:\n - mesh-controller internal/x.go (the thing)\n---\n"),
|
||||
"03-DESIGN/01-to-be/8.md": doc("---\nstatus: designed\ncode: [mesh-controller]\n---\n"),
|
||||
"03-DESIGN/01-to-be/7.md": doc("---\nstatus: in-progress\ncode: [mesh-controller internal/x.go]\n---\n"),
|
||||
}
|
||||
got := checkStatusAgainstCode(broken)
|
||||
expectProblem(t, got, "03-DESIGN/01-to-be/9.md: `designed`, but names \"internal/x.go\" in mesh-controller")
|
||||
if len(got) != 1 {
|
||||
t.Errorf("a plan without a path and an in-progress design are sound: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
func TestMarkdownFilesSkipsToolFolders(t *testing.T) {
|
||||
fsys := fstest.MapFS{
|
||||
"a.md": {Data: []byte("x")},
|
||||
"b/c.md": {Data: []byte("x")},
|
||||
"b/c.txt": {Data: []byte("x")},
|
||||
".git/d.md": {Data: []byte("x")},
|
||||
".claude/e.md": {Data: []byte("x")},
|
||||
".work/f/g.md": {Data: []byte("x")},
|
||||
}
|
||||
got := markdownFiles(fsys)
|
||||
want := []string{"a.md", "b/c.md"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("markdownFiles = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrontmatterReadsScalarsAndLists(t *testing.T) {
|
||||
f := frontmatter("---\nlayer: to-be\ndecisions:\n - ADR 0001\n - ADR 0002\ncode: []\ncodes: [a, b]\n---\nbody\n")
|
||||
if f.scalar("layer") != "to-be" {
|
||||
t.Errorf("layer = %q", f.scalar("layer"))
|
||||
}
|
||||
if !reflect.DeepEqual(f.list("decisions"), []string{"ADR 0001", "ADR 0002"}) {
|
||||
t.Errorf("decisions = %q", f.list("decisions"))
|
||||
}
|
||||
if len(f.list("code")) != 0 || !reflect.DeepEqual(f.list("codes"), []string{"a", "b"}) {
|
||||
t.Errorf("code = %q, codes = %q", f.list("code"), f.list("codes"))
|
||||
}
|
||||
if frontmatter("no frontmatter") != nil || frontmatter("---\nunclosed") != nil {
|
||||
t.Error("a document without a closed block has no frontmatter")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// Package checks holds hq's four checks as Go tests (ADR 0527): records, index, cycle and words.
|
||||
//
|
||||
// The package has tests only. Each check is a function over an fs.FS that returns its problems, so
|
||||
// the test of the real tree reads the repository's root, and the test of the check itself reads a
|
||||
// deliberately broken tree built in memory and must see the problem named (every check here failed on
|
||||
// a broken fixture before it passed). The shared reading of Markdown and frontmatter is in this file.
|
||||
package checks
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// rootFS is hq's tree: the checks run from 00-META/checks, two folders under the root.
|
||||
func rootFS() fs.FS { return os.DirFS("../..") }
|
||||
|
||||
// skipDirs are folders no check reads: git's, the agent tooling's and a session's scratch folder.
|
||||
var skipDirs = map[string]bool{".git": true, ".claude": true, ".work": true}
|
||||
|
||||
// markdownFiles lists every Markdown file of the tree, sorted, in slash paths from the root.
|
||||
func markdownFiles(fsys fs.FS) []string {
|
||||
var out []string
|
||||
_ = fs.WalkDir(fsys, ".", func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if d.IsDir() {
|
||||
if p != "." && skipDirs[d.Name()] {
|
||||
return fs.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if strings.HasSuffix(p, ".md") {
|
||||
out = append(out, p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func readFile(fsys fs.FS, p string) string {
|
||||
b, err := fs.ReadFile(fsys, p)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Front is a document's frontmatter: a scalar field holds a string, a list field a []string.
|
||||
type Front map[string]any
|
||||
|
||||
func (f Front) scalar(key string) string {
|
||||
s, _ := f[key].(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func (f Front) list(key string) []string {
|
||||
switch v := f[key].(type) {
|
||||
case []string:
|
||||
return v
|
||||
case string:
|
||||
if strings.TrimSpace(v) != "" {
|
||||
return []string{v}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
fmItem = regexp.MustCompile(`^\s+-\s*(.+?)\s*$`)
|
||||
fmPair = regexp.MustCompile(`^([A-Za-z_-]+):\s*(.*)$`)
|
||||
)
|
||||
|
||||
// frontmatter reads the block between the first two --- lines. It is not a YAML parser on purpose: a
|
||||
// dependency to read a few scalar fields and one list would cost more than it returns. It returns nil
|
||||
// when the document has no frontmatter.
|
||||
func frontmatter(text string) Front {
|
||||
if !strings.HasPrefix(text, "---\n") {
|
||||
return nil
|
||||
}
|
||||
end := strings.Index(text[4:], "\n---")
|
||||
if end == -1 {
|
||||
return nil
|
||||
}
|
||||
out := Front{}
|
||||
key := ""
|
||||
for _, line := range strings.Split(text[4:4+end], "\n") {
|
||||
if m := fmItem.FindStringSubmatch(line); m != nil && key != "" {
|
||||
list, _ := out[key].([]string)
|
||||
out[key] = append(list, m[1])
|
||||
continue
|
||||
}
|
||||
m := fmPair.FindStringSubmatch(line)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
key = m[1]
|
||||
value := strings.TrimSpace(m[2])
|
||||
switch {
|
||||
case strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]"):
|
||||
list := []string{}
|
||||
for _, v := range strings.Split(value[1:len(value)-1], ",") {
|
||||
if v = strings.TrimSpace(v); v != "" {
|
||||
list = append(list, v)
|
||||
}
|
||||
}
|
||||
out[key] = list
|
||||
case value == "":
|
||||
out[key] = []string{}
|
||||
default:
|
||||
out[key] = value
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func unquote(v string) string {
|
||||
v = strings.TrimSpace(v)
|
||||
if len(v) >= 2 && v[0] == v[len(v)-1] && (v[0] == '"' || v[0] == '\'') {
|
||||
return v[1 : len(v)-1]
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// lineOf is the 1-based line of byte offset i in text.
|
||||
func lineOf(text string, i int) int { return strings.Count(text[:i], "\n") + 1 }
|
||||
|
||||
// expectNone fails the test with every problem a check found in the real tree.
|
||||
func expectNone(t *testing.T, problems []string) {
|
||||
t.Helper()
|
||||
for _, p := range problems {
|
||||
t.Error(p)
|
||||
}
|
||||
}
|
||||
|
||||
// expectProblem fails unless some problem contains want: the broken fixture must be caught, by name.
|
||||
func expectProblem(t *testing.T, problems []string, want string) {
|
||||
t.Helper()
|
||||
for _, p := range problems {
|
||||
if strings.Contains(p, want) {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("no problem mentions %q; got %q", want, problems)
|
||||
}
|
||||
@@ -8,3 +8,15 @@ which keeps a record's words as it was imported.
|
||||
|
||||
| Document | Until | Why |
|
||||
|---|---|---|
|
||||
| `04-ISSUES/285-the-merge-gate-compared-broken-to-broken-and-passed/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/300-a-new-module-is-not-built-at-its-merge/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/302-the-labs-go-replays-were-never-checked-before-merge/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/305-a-recheck-left-the-old-verdict-standing/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/336-a-send-held-for-the-bus-step-raises-nothing/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/336-a-send-held-for-the-bus-step-raises-nothing/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/338-a-module-built-from-a-shared-repository-moves-on-every-merge-to-it/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/341-a-merge-check-judges-the-head-not-what-merging-it-makes/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/353-the-controller-asked-before-the-bus-held-its-grant/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/362-two-merges-seconds-apart-left-their-delivery-waiting-for-a-person/01-diagnosis.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
| `04-ISSUES/364-the-gate-refuses-a-module-that-uses-a-seat-another-module-declares/00-report.md` | 2027-04-30 | history: the issue's narrative keeps the word of its day, "merge gate" (issue 1303); it is not rewritten |
|
||||
|
||||
@@ -0,0 +1,756 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// One word per thing, checked (words.py until ADR 0527; ADR 0244). The glossary (00-META/glossary.md) is the
|
||||
// authority on the mesh's words. It names the words it retired on one fixed kind of line, so a program can read them:
|
||||
//
|
||||
// *Not:* ~~control plane~~, ~~overlay~~ (hq)
|
||||
//
|
||||
// A struck word with no scope is retired everywhere; `(hq)` retires it in this repository's prose only; `(tools)`
|
||||
// in the descriptions of the mesh's tools only. And it names code that still carries an old name until it is renamed:
|
||||
//
|
||||
// *Identifier until renamed:* `mesh-host` - the repository, binary and service unit
|
||||
//
|
||||
// An identifier is allowed inside a code span and nowhere else.
|
||||
//
|
||||
// What is enforced:
|
||||
//
|
||||
// retired no retired word of scope hq (or none), and no identifier, in running prose of a checked document.
|
||||
// Running prose is what is left once code blocks, code spans, block quotes, text in quotation marks,
|
||||
// struck-through text, link targets, HTML comments and frontmatter are taken out. A word's plural is the word.
|
||||
// unique no head word heads two entries, and none is also struck through on a *Not:* line.
|
||||
// lines no daemon's line in a checked document (ADR 0272, its insight of 2026-10-09): the operator types `nox`.
|
||||
// copy with MESH_CATALOG_DIR set to a checkout of the catalogue, its copies of the retired words and the
|
||||
// daemons' lines equal this glossary's and this list (skipped, with its reason, when unset).
|
||||
//
|
||||
// Checked documents: 00-META/, 03-DESIGN/ (both layers), AGENTS.md and README.md, always; an issue opened on or after
|
||||
// wordsFrom (on or after linesFrom for the lines). A document that cannot be reworded in the change that found it is
|
||||
// named, with a date and a reason, in words-allowed.md beside this file; an entry past its date fails like the word
|
||||
// itself, and so does an entry for a document that no longer needs it.
|
||||
|
||||
const (
|
||||
glossaryFile = "00-META/glossary.md"
|
||||
allowedFile = "00-META/checks/words-allowed.md"
|
||||
// The day the rule began (ADR 0244): issues opened from it on are held to it.
|
||||
wordsFrom = "2026-10-07"
|
||||
// The day the daemons' lines rule began.
|
||||
linesFrom = "2026-10-09"
|
||||
)
|
||||
|
||||
var wordsAlways = []string{"00-META/", "03-DESIGN/", "AGENTS.md", "README.md"}
|
||||
|
||||
// daemonLines are the daemons' lines (ADR 0272): each daemon's commands. "alone" is a command that is a line by
|
||||
// itself; "with an argument" one that is a line only when written as one ("the mesh-controller module" is prose;
|
||||
// `mesh-controller module check ...` is a line). The node-engine's install-time subcommands are not lines here.
|
||||
var daemonLines = []struct {
|
||||
daemon, alone, withArgument string
|
||||
}{
|
||||
{"mesh-controller",
|
||||
"adopt assign broker cancel cleanup collect conditions converge doctor durations hand-act hand-acts healers identity kill licence " +
|
||||
"migrate operator pause pin plan prepare push rebuild rehearse replay resume retire rollout rotate secret settings status token " +
|
||||
"unassign unpin upgrade",
|
||||
"artifacts ask builds bus collection data delivery images mirrors module node overlay plans queue seat seats take version"},
|
||||
{"mesh-host", "apply bundle enrol enroll hand-over help inventory key overlay owned profile reconcile run version", ""},
|
||||
{"nox-mesh-host", "hand-over", ""},
|
||||
}
|
||||
|
||||
var (
|
||||
notLineRx = regexp.MustCompile(`(?m)^\s*(?:[-*]\s+)?\*Not:\*(.*)$`)
|
||||
struckRx = regexp.MustCompile(`~~([^~]+)~~(?:\s*\((hq|tools)\))?`)
|
||||
identLineRx = regexp.MustCompile(`(?m)^\s*(?:[-*]\s+)?\*Identifier until renamed:\*(.*)$`)
|
||||
spanRx = regexp.MustCompile("`([^`]+)`")
|
||||
headRx = regexp.MustCompile(`(?m)^\s*-\s+\*\*([^*]+)\*\*`)
|
||||
openedRx = regexp.MustCompile(`(?m)^opened:\s*(\S+)`)
|
||||
)
|
||||
|
||||
type retiredWord struct{ word, scope string }
|
||||
|
||||
// parseGlossary reads the retired words with their scope, the identifiers and the head words from the glossary.
|
||||
func parseGlossary(text string) (retired []retiredWord, identifiers, heads []string) {
|
||||
for _, m := range notLineRx.FindAllStringSubmatch(text, -1) {
|
||||
for _, s := range struckRx.FindAllStringSubmatch(m[1], -1) {
|
||||
scope := s[2]
|
||||
if scope == "" {
|
||||
scope = "all"
|
||||
}
|
||||
for _, one := range strings.Split(s[1], " / ") {
|
||||
retired = append(retired, retiredWord{strings.TrimSpace(one), scope})
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, m := range identLineRx.FindAllStringSubmatch(text, -1) {
|
||||
for _, s := range spanRx.FindAllStringSubmatch(m[1], -1) {
|
||||
identifiers = append(identifiers, strings.TrimSpace(s[1]))
|
||||
}
|
||||
}
|
||||
for _, m := range headRx.FindAllStringSubmatch(text, -1) {
|
||||
for _, h := range strings.Split(m[1], " / ") {
|
||||
heads = append(heads, strings.TrimSpace(h))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func isWordRune(r rune) bool {
|
||||
return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-'
|
||||
}
|
||||
|
||||
// findBounded returns the start offsets of the matches of rx that are whole words: no word character or hyphen
|
||||
// before or after (the lookarounds of the Python check, which RE2 has not).
|
||||
func findBounded(rx *regexp.Regexp, text string) []int {
|
||||
var out []int
|
||||
pos := 0
|
||||
for pos <= len(text) {
|
||||
loc := rx.FindStringIndex(text[pos:])
|
||||
if loc == nil {
|
||||
break
|
||||
}
|
||||
start, end := pos+loc[0], pos+loc[1]
|
||||
before, after := true, true
|
||||
if start > 0 {
|
||||
r := []rune(text[max(0, start-4):start])
|
||||
before = !isWordRune(r[len(r)-1])
|
||||
}
|
||||
if end < len(text) {
|
||||
r := []rune(text[end:min(len(text), end+4)])
|
||||
after = !isWordRune(r[0])
|
||||
}
|
||||
if before && after {
|
||||
out = append(out, start)
|
||||
pos = end
|
||||
if end == start {
|
||||
pos++
|
||||
}
|
||||
} else {
|
||||
pos = start + 1
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// wordRegexp matches a word whole, case-insensitively; a space in the word matches a space, a line break or a hyphen.
|
||||
// The plural is the word too: its last part may end in s or es ("alerts", "control planes").
|
||||
func wordRegexp(word string) *regexp.Regexp {
|
||||
var parts []string
|
||||
for _, p := range strings.Fields(word) {
|
||||
parts = append(parts, regexp.QuoteMeta(p))
|
||||
}
|
||||
return regexp.MustCompile(`(?i)` + strings.Join(parts, `[\s-]+`) + `(?:e?s)?`)
|
||||
}
|
||||
|
||||
// blank replaces every byte of s but the line breaks, so offsets and line numbers hold.
|
||||
func blank(s string) string {
|
||||
b := []byte(s)
|
||||
for i, c := range b {
|
||||
if c != '\n' {
|
||||
b[i] = ' '
|
||||
}
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func blankFrontmatter(text string) string {
|
||||
if strings.HasPrefix(text, "---\n") {
|
||||
if end := strings.Index(text[4:], "\n---"); end != -1 {
|
||||
end += 4
|
||||
return blank(text[:end+4]) + text[end+4:]
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func blankAll(text string, steps []*regexp.Regexp) string {
|
||||
for _, rx := range steps {
|
||||
text = rx.ReplaceAllStringFunc(text, blank)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func compileAll(patterns ...string) []*regexp.Regexp {
|
||||
var out []*regexp.Regexp
|
||||
for _, p := range patterns {
|
||||
out = append(out, regexp.MustCompile(p))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// What is not the document's own words, in the order it is taken out.
|
||||
const (
|
||||
stepComment = `(?s)<!--.*?-->`
|
||||
stepBlockQuote = `(?m)^\s*>.*$`
|
||||
stepLinkTarget = `\]\([^)\s]*\)`
|
||||
stepStruck = `~~[^~\n]+~~`
|
||||
stepQuoted = `"[^"\n]*"`
|
||||
stepCurlyDouble = `\x{201c}[^\x{201d}]*\x{201d}`
|
||||
stepCurlySingle = `\x{2018}[^\x{2019}\n]*\x{2019}`
|
||||
)
|
||||
|
||||
var (
|
||||
proseSteps = compileAll("(?ms)^\\s*```.*?^\\s*```", stepComment, stepBlockQuote, "`[^`\\n]+`", stepLinkTarget, stepStruck,
|
||||
stepQuoted, stepCurlyDouble, stepCurlySingle)
|
||||
// In the glossary the *Not:* and *Identifier until renamed:* lines name the retired words, so they are taken out first.
|
||||
glossaryProseSteps = append(compileAll(notLineRx.String(), identLineRx.String()), proseSteps...)
|
||||
// The daemons' lines are looked for in the document's own words: a code block and a code span are kept, since a
|
||||
// line to type is written in one.
|
||||
ownWordsSteps = compileAll(stepComment, stepBlockQuote, stepLinkTarget, stepStruck, stepQuoted, stepCurlyDouble, stepCurlySingle)
|
||||
)
|
||||
|
||||
// prose is the text with everything that is not running prose blanked, keeping offsets and line numbers.
|
||||
func prose(text string, glossaryDoc bool) string {
|
||||
text = blankFrontmatter(text)
|
||||
if glossaryDoc {
|
||||
return blankAll(text, glossaryProseSteps)
|
||||
}
|
||||
return blankAll(text, proseSteps)
|
||||
}
|
||||
|
||||
func ownWords(text string) string { return blankAll(blankFrontmatter(text), ownWordsSteps) }
|
||||
|
||||
// issueDocuments lists the issue reports' files opened on or after since.
|
||||
func issueDocuments(fsys fs.FS, since string) []string {
|
||||
var out []string
|
||||
entries, _ := fs.ReadDir(fsys, "04-ISSUES")
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
head := readFile(fsys, path.Join("04-ISSUES", e.Name(), "00-report.md"))
|
||||
front := ""
|
||||
if strings.HasPrefix(head, "---\n") {
|
||||
if end := strings.Index(head[4:], "\n---"); end != -1 {
|
||||
if m := openedRx.FindStringSubmatch(head[4 : 4+end]); m != nil {
|
||||
front = m[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
if front == "" || front < since {
|
||||
continue
|
||||
}
|
||||
files, _ := fs.ReadDir(fsys, path.Join("04-ISSUES", e.Name()))
|
||||
for _, f := range files {
|
||||
if strings.HasSuffix(f.Name(), ".md") {
|
||||
out = append(out, path.Join("04-ISSUES", e.Name(), f.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// perDocument runs fn over the documents on all processors (the unit level is short) and joins the problems in
|
||||
// the documents' order.
|
||||
func perDocument(docs []string, fn func(doc string) []string) []string {
|
||||
results := make([][]string, len(docs))
|
||||
var wg sync.WaitGroup
|
||||
sem := make(chan struct{}, runtime.NumCPU())
|
||||
for i, d := range docs {
|
||||
wg.Add(1)
|
||||
sem <- struct{}{}
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
results[i] = fn(d)
|
||||
<-sem
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
var out []string
|
||||
for _, r := range results {
|
||||
out = append(out, r...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func checkedDocuments(fsys fs.FS, since string) []string {
|
||||
var docs []string
|
||||
for _, entry := range wordsAlways {
|
||||
if strings.HasSuffix(entry, "/") {
|
||||
_ = fs.WalkDir(fsys, strings.TrimSuffix(entry, "/"), func(p string, d fs.DirEntry, err error) error {
|
||||
if err == nil && !d.IsDir() && strings.HasSuffix(p, ".md") {
|
||||
docs = append(docs, p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
} else if _, err := fs.Stat(fsys, entry); err == nil {
|
||||
docs = append(docs, entry)
|
||||
}
|
||||
}
|
||||
return append(docs, issueDocuments(fsys, since)...)
|
||||
}
|
||||
|
||||
var dateRx = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
|
||||
|
||||
// parseAllowed reads words-allowed.md: one row per document, | `document` | until | why |. Until is a date, after
|
||||
// which the allowance fails like the words themselves. The clock is the caller's: today is YYYY-MM-DD.
|
||||
func parseAllowed(text, today string) (allowed map[string]string, problems []string) {
|
||||
allowed = map[string]string{}
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
cells := strings.Split(strings.Trim(strings.TrimSpace(line), "|"), "|")
|
||||
if len(cells) != 3 {
|
||||
continue
|
||||
}
|
||||
for i := range cells {
|
||||
cells[i] = strings.TrimSpace(cells[i])
|
||||
}
|
||||
if !strings.HasPrefix(cells[0], "`") {
|
||||
continue
|
||||
}
|
||||
doc, until, why := strings.Trim(cells[0], "`"), cells[1], cells[2]
|
||||
if why == "" {
|
||||
problems = append(problems, fmt.Sprintf("%s: the allowance for %s gives no reason", allowedFile, doc))
|
||||
}
|
||||
if !dateRx.MatchString(until) {
|
||||
problems = append(problems, fmt.Sprintf("%s: the allowance for %s has no date", allowedFile, doc))
|
||||
continue
|
||||
}
|
||||
if until < today {
|
||||
problems = append(problems, fmt.Sprintf("%s: the allowance for %s ran out on %s — reword it", allowedFile, doc, until))
|
||||
continue
|
||||
}
|
||||
allowed[doc] = until
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func checkRetired(fsys fs.FS, retired []retiredWord, identifiers []string, today string) []string {
|
||||
allowance, out := parseAllowed(readFile(fsys, allowedFile), today)
|
||||
type matcher struct {
|
||||
word string
|
||||
rx *regexp.Regexp
|
||||
why string
|
||||
}
|
||||
var words []matcher
|
||||
for _, r := range retired {
|
||||
if r.scope == "all" || r.scope == "hq" {
|
||||
words = append(words, matcher{r.word, wordRegexp(r.word), "retired"})
|
||||
}
|
||||
}
|
||||
for _, i := range identifiers {
|
||||
words = append(words, matcher{i, wordRegexp(i), "an identifier, allowed only in a code span"})
|
||||
}
|
||||
var mu sync.Mutex
|
||||
used := map[string]bool{}
|
||||
out = append(out, perDocument(checkedDocuments(fsys, wordsFrom), func(d string) []string {
|
||||
var found []string
|
||||
text := prose(readFile(fsys, d), d == glossaryFile)
|
||||
lower := strings.ToLower(text)
|
||||
for _, w := range words {
|
||||
// Every match holds the word's first part literally, so a text without it is not searched (the unit level is short).
|
||||
if !strings.Contains(lower, strings.ToLower(strings.Fields(w.word)[0])) {
|
||||
continue
|
||||
}
|
||||
for _, start := range findBounded(w.rx, text) {
|
||||
if _, ok := allowance[d]; ok {
|
||||
mu.Lock()
|
||||
used[d] = true
|
||||
mu.Unlock()
|
||||
continue
|
||||
}
|
||||
m := w.rx.FindString(text[start:])
|
||||
found = append(found, fmt.Sprintf("%s:%d: %q is %s (glossary)", d, lineOf(text, start), strings.ReplaceAll(m, "\n", " "), w.why))
|
||||
}
|
||||
}
|
||||
return found
|
||||
})...)
|
||||
var unused []string
|
||||
for d := range allowance {
|
||||
if !used[d] {
|
||||
unused = append(unused, d)
|
||||
}
|
||||
}
|
||||
sort.Strings(unused)
|
||||
for _, d := range unused {
|
||||
out = append(out, fmt.Sprintf("%s: %s no longer uses a retired word — remove its allowance", allowedFile, d))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func checkUnique(retired []retiredWord, heads []string) []string {
|
||||
var out []string
|
||||
seen := map[string]bool{}
|
||||
for _, h := range heads {
|
||||
k := strings.ToLower(h)
|
||||
if seen[k] {
|
||||
out = append(out, fmt.Sprintf("%s: %q heads two entries", glossaryFile, h))
|
||||
}
|
||||
seen[k] = true
|
||||
}
|
||||
for _, r := range retired {
|
||||
if seen[strings.ToLower(r.word)] {
|
||||
out = append(out, fmt.Sprintf("%s: %q is a head word and also retired", glossaryFile, r.word))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// linesList is the daemons' lines as the catalogue's copy lists them: "<daemon> <command>", with " …" when an
|
||||
// argument must follow.
|
||||
func linesList() []string {
|
||||
var out []string
|
||||
for _, d := range daemonLines {
|
||||
for _, c := range strings.Fields(d.alone) {
|
||||
out = append(out, d.daemon+" "+c)
|
||||
}
|
||||
for _, c := range strings.Fields(d.withArgument) {
|
||||
out = append(out, d.daemon+" "+c+" …")
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func checkLines(fsys fs.FS) []string {
|
||||
type pat struct {
|
||||
daemon, command string
|
||||
rx *regexp.Regexp
|
||||
bounded bool
|
||||
}
|
||||
var pats []pat
|
||||
for _, d := range daemonLines {
|
||||
dq := regexp.QuoteMeta(d.daemon)
|
||||
for _, c := range strings.Fields(d.alone) {
|
||||
pats = append(pats, pat{d.daemon, c, regexp.MustCompile(dq + `\s+` + regexp.QuoteMeta(c)), true})
|
||||
}
|
||||
for _, c := range strings.Fields(d.withArgument) {
|
||||
// A line only when written as one: at the start of a line or a code span, a prompt allowed, an argument after.
|
||||
pats = append(pats, pat{d.daemon, c, regexp.MustCompile("(?m)(?:^|`)[ \\t]*(?:\\$[ \\t]*)?(" + dq + `\s+` + regexp.QuoteMeta(c) + "\\s+[^\\s`])"), false})
|
||||
}
|
||||
}
|
||||
return perDocument(checkedDocuments(fsys, linesFrom), func(d string) []string {
|
||||
var out []string
|
||||
text := ownWords(readFile(fsys, d))
|
||||
for _, p := range pats {
|
||||
if !strings.Contains(text, p.daemon) {
|
||||
continue
|
||||
}
|
||||
var starts []int
|
||||
if p.bounded {
|
||||
starts = findBounded(p.rx, text)
|
||||
} else {
|
||||
for _, loc := range p.rx.FindAllStringSubmatchIndex(text, -1) {
|
||||
starts = append(starts, loc[2])
|
||||
}
|
||||
}
|
||||
for _, s := range starts {
|
||||
out = append(out, fmt.Sprintf("%s:%d: `%s %s` is a daemon's line — the operator types `nox …` (ADR 0272)", d, lineOf(text, s), p.daemon, p.command))
|
||||
}
|
||||
}
|
||||
return out
|
||||
})
|
||||
}
|
||||
|
||||
func toolsList(retired []retiredWord) []string {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
for _, r := range retired {
|
||||
if (r.scope == "all" || r.scope == "tools") && !seen[r.word] {
|
||||
seen[r.word] = true
|
||||
out = append(out, r.word)
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return strings.ToLower(out[i]) < strings.ToLower(out[j]) })
|
||||
return out
|
||||
}
|
||||
|
||||
func readList(file string) ([]string, error) {
|
||||
b, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []string
|
||||
for _, l := range strings.Split(string(b), "\n") {
|
||||
if l = strings.TrimSpace(l); l != "" && !strings.HasPrefix(l, "#") {
|
||||
out = append(out, l)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// checkCopy compares the catalogue's copies of the retired words and the daemons' lines with this glossary's. A
|
||||
// difference names what the copy should list, so regenerating it is copying that from the failure.
|
||||
func checkCopy(catalogue string, retired []retiredWord) []string {
|
||||
var out []string
|
||||
copyWords, err := readList(path.Join(catalogue, "retired-words"))
|
||||
if err != nil {
|
||||
return []string{"the catalogue's copy of the retired words cannot be read: " + err.Error()}
|
||||
}
|
||||
sort.Slice(copyWords, func(i, j int) bool { return strings.ToLower(copyWords[i]) < strings.ToLower(copyWords[j]) })
|
||||
want := toolsList(retired)
|
||||
if strings.Join(copyWords, "\n") != strings.Join(want, "\n") {
|
||||
out = append(out, "the catalogue's retired-words differs from the glossary: it should list "+strings.Join(want, ", "))
|
||||
}
|
||||
copyLines, err := readList(path.Join(catalogue, "retired-lines"))
|
||||
if err != nil {
|
||||
return append(out, "the catalogue's copy of the daemons' lines cannot be read: "+err.Error())
|
||||
}
|
||||
sort.Strings(copyLines)
|
||||
if strings.Join(copyLines, "\n") != strings.Join(linesList(), "\n") {
|
||||
out = append(out, "the catalogue's retired-lines differs from this check's list: it should list, one per line:\n"+strings.Join(linesList(), "\n"))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// The real tree.
|
||||
|
||||
func today() string { return time.Now().Format("2006-01-02") }
|
||||
|
||||
func TestWordsGlossaryNamesRetiredWords(t *testing.T) {
|
||||
retired, identifiers, heads := parseGlossary(readFile(rootFS(), glossaryFile))
|
||||
if len(retired) == 0 {
|
||||
t.Fatal("the glossary names no retired word on a *Not:* line — the check would pass on anything")
|
||||
}
|
||||
if len(identifiers) == 0 || len(heads) == 0 {
|
||||
t.Errorf("the glossary names %d identifiers and %d head words; both are expected", len(identifiers), len(heads))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWordsEachHeadWordHeadsOneEntry(t *testing.T) {
|
||||
retired, _, heads := parseGlossary(readFile(rootFS(), glossaryFile))
|
||||
expectNone(t, checkUnique(retired, heads))
|
||||
}
|
||||
|
||||
func TestWordsNoRetiredWordInRunningProse(t *testing.T) {
|
||||
retired, identifiers, _ := parseGlossary(readFile(rootFS(), glossaryFile))
|
||||
expectNone(t, checkRetired(rootFS(), retired, identifiers, today()))
|
||||
}
|
||||
|
||||
func TestWordsNoDaemonLineWritten(t *testing.T) { expectNone(t, checkLines(rootFS())) }
|
||||
|
||||
func TestWordsCatalogueCopyAgrees(t *testing.T) {
|
||||
catalogue := os.Getenv("MESH_CATALOG_DIR")
|
||||
if catalogue == "" {
|
||||
t.Skip("NOT COMPARED: MESH_CATALOG_DIR is not set, so the catalogue's copy of the list was not read")
|
||||
}
|
||||
retired, _, _ := parseGlossary(readFile(rootFS(), glossaryFile))
|
||||
expectNone(t, checkCopy(catalogue, retired))
|
||||
}
|
||||
|
||||
// The checks themselves, against broken trees (H5a: the glossary).
|
||||
|
||||
const glossaryFixture = `# Glossary
|
||||
|
||||
- **control plane** — the retired one's replacement is below.
|
||||
*Not:* ~~control plane~~, ~~overlay~~ (hq), ~~alert~~ (tools), ~~a / b~~
|
||||
- **controller** — decides.
|
||||
*Not:* ~~control plane~~
|
||||
- **Controller** — again.
|
||||
- *Identifier until renamed:* ` + "`mesh-host`, `nox-old`" + ` — the repository
|
||||
`
|
||||
|
||||
func TestGlossaryReadsNotLinesScopesIdentifiersAndHeads(t *testing.T) {
|
||||
retired, identifiers, heads := parseGlossary(glossaryFixture)
|
||||
want := []retiredWord{{"control plane", "all"}, {"overlay", "hq"}, {"alert", "tools"}, {"a", "all"}, {"b", "all"}, {"control plane", "all"}}
|
||||
if fmt.Sprint(retired) != fmt.Sprint(want) {
|
||||
t.Errorf("retired = %v, want %v", retired, want)
|
||||
}
|
||||
if fmt.Sprint(identifiers) != "[mesh-host nox-old]" {
|
||||
t.Errorf("identifiers = %v", identifiers)
|
||||
}
|
||||
if fmt.Sprint(heads) != "[control plane controller Controller]" {
|
||||
t.Errorf("heads = %v", heads)
|
||||
}
|
||||
got := checkUnique(retired, heads)
|
||||
expectProblem(t, got, `"Controller" heads two entries`)
|
||||
expectProblem(t, got, `"control plane" is a head word and also retired`)
|
||||
}
|
||||
|
||||
func TestToolsListHoldsTheWordsOfScopeAllAndTools(t *testing.T) {
|
||||
retired, _, _ := parseGlossary(glossaryFixture)
|
||||
if got := fmt.Sprint(toolsList(retired)); got != "[a alert b control plane]" {
|
||||
t.Errorf("toolsList = %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// H5b: prose blanking and matching.
|
||||
|
||||
func TestProseBlanksWhatIsNotRunningProse(t *testing.T) {
|
||||
text := "---\nfm: overlay\n---\n" +
|
||||
"bare overlay one\n" +
|
||||
"```\noverlay in a block\n```\n" +
|
||||
"span `overlay` x\n" +
|
||||
"> quoted overlay\n" +
|
||||
"say \"overlay\" and “overlay” and ‘overlay’\n" +
|
||||
"~~overlay~~ struck\n" +
|
||||
"[link](overlay.md)\n" +
|
||||
"<!-- overlay\nover lines -->\n" +
|
||||
"bare overlay two\n"
|
||||
got := prose(text, false)
|
||||
if strings.Count(got, "overlay") != 2 {
|
||||
t.Errorf("only the two bare uses stay: %q", got)
|
||||
}
|
||||
if strings.Count(got, "\n") != strings.Count(text, "\n") || len(got) != len(text) {
|
||||
t.Error("blanking keeps offsets and line numbers")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProseOfTheGlossaryBlanksNotAndIdentifierLines(t *testing.T) {
|
||||
got := prose("- **x** — y\n *Not:* control plane, plain\n- *Identifier until renamed:* mesh-host\nthe control plane\n", true)
|
||||
if strings.Count(got, "control plane") != 1 || strings.Contains(got, "mesh-host") {
|
||||
t.Errorf("only the running use stays: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWordMatchesWholeWordsPluralsAndLineBreaks(t *testing.T) {
|
||||
rx := wordRegexp("control plane")
|
||||
count := func(s string) int { return len(findBounded(rx, s)) }
|
||||
for s, want := range map[string]int{
|
||||
"the control plane runs": 1,
|
||||
"Control Planes": 1,
|
||||
"control-plane and control\nplane": 2,
|
||||
"uncontrol plane": 0,
|
||||
"control planet": 0,
|
||||
"control plane-x": 0,
|
||||
"x-control plane": 0,
|
||||
"control planexs": 0,
|
||||
} {
|
||||
if got := count(s); got != want {
|
||||
t.Errorf("%q: %d matches, want %d", s, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetiredCatchesAWordInProseAndNamesDocumentAndLine(t *testing.T) {
|
||||
retired := []retiredWord{{"control plane", "all"}, {"overlay", "hq"}, {"alert", "tools"}}
|
||||
broken := mapOf(map[string]string{
|
||||
"AGENTS.md": "fine\nthe control plane here\n",
|
||||
"03-DESIGN/01-to-be/a.md": "an overlay and \"a control plane quoted\" and `overlay` alert\n",
|
||||
"README.md": "mesh-host runs; `mesh-host` too\n",
|
||||
})
|
||||
got := checkRetired(broken, retired, []string{"mesh-host"}, "2026-10-23")
|
||||
expectProblem(t, got, `AGENTS.md:2: "control plane" is retired`)
|
||||
expectProblem(t, got, `03-DESIGN/01-to-be/a.md:1: "overlay" is retired`)
|
||||
expectProblem(t, got, `README.md:1: "mesh-host" is an identifier, allowed only in a code span`)
|
||||
if len(got) != 3 {
|
||||
t.Errorf("a quotation, a code span and a word of scope tools are not problems: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssuesAreHeldToTheRuleFromItsDay(t *testing.T) {
|
||||
retired := []retiredWord{{"overlay", "all"}}
|
||||
fsys := mapOf(map[string]string{
|
||||
"04-ISSUES/1-old/00-report.md": "---\nopened: 2026-10-06\n---\noverlay\n",
|
||||
"04-ISSUES/2-new/00-report.md": "---\nopened: 2026-10-07\n---\nfine\n",
|
||||
"04-ISSUES/2-new/01-notes.md": "an overlay\n",
|
||||
"04-ISSUES/3-nodate/00-report.md": "overlay\n",
|
||||
})
|
||||
got := checkRetired(fsys, retired, nil, "2026-10-23")
|
||||
expectProblem(t, got, "04-ISSUES/2-new/01-notes.md:1")
|
||||
if len(got) != 1 {
|
||||
t.Errorf("only the issue opened from 2026-10-07 on is read: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// H5c: allowances, with an injected clock.
|
||||
|
||||
const allowedFixture = `# Words allowed
|
||||
|
||||
| Document | Until | Why |
|
||||
|---|---|---|
|
||||
| ` + "`a.md`" + ` | 2026-12-01 | quotes what was |
|
||||
| ` + "`b.md`" + ` | 2026-10-01 | ran out |
|
||||
| ` + "`c.md`" + ` | soon | no date |
|
||||
| ` + "`d.md`" + ` | 2026-12-01 | |
|
||||
`
|
||||
|
||||
func TestAllowedReadsRowsAndTheInjectedClock(t *testing.T) {
|
||||
allowed, problems := parseAllowed(allowedFixture, "2026-10-23")
|
||||
if _, ok := allowed["a.md"]; !ok || len(allowed) != 2 {
|
||||
t.Errorf("a.md and d.md are in date: %v", allowed)
|
||||
}
|
||||
expectProblem(t, problems, "the allowance for b.md ran out on 2026-10-01")
|
||||
expectProblem(t, problems, "the allowance for c.md has no date")
|
||||
expectProblem(t, problems, "the allowance for d.md gives no reason")
|
||||
// The same text on an earlier day: b.md is still in date.
|
||||
if early, _ := parseAllowed(allowedFixture, "2026-09-30"); len(early) != 3 {
|
||||
t.Errorf("on 2026-09-30 a, b and d are in date: %v", early)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowanceHoldsAWordBackUntilItsDateAndShrinks(t *testing.T) {
|
||||
retired := []retiredWord{{"overlay", "all"}}
|
||||
allowedRow := "| `AGENTS.md` | 2026-12-01 | quotes what was |\n"
|
||||
tree := func(agents, allowed string) map[string]string {
|
||||
return map[string]string{"AGENTS.md": agents, allowedFile: allowed}
|
||||
}
|
||||
expectNone(t, checkRetired(mapOf(tree("an overlay\n", allowedRow)), retired, nil, "2026-10-23"))
|
||||
expectProblem(t, checkRetired(mapOf(tree("an overlay\n", allowedRow)), retired, nil, "2026-12-02"), "ran out on 2026-12-01")
|
||||
expectProblem(t, checkRetired(mapOf(tree("clean\n", allowedRow)), retired, nil, "2026-10-23"), "AGENTS.md no longer uses a retired word")
|
||||
expectProblem(t, checkRetired(mapOf(tree("an overlay\n", "")), retired, nil, "2026-10-23"), `AGENTS.md:1: "overlay" is retired`)
|
||||
}
|
||||
|
||||
// H5d: the daemons' lines and the catalogue comparison.
|
||||
|
||||
func TestLinesCatchesADaemonsLineButNotProse(t *testing.T) {
|
||||
fsys := mapOf(map[string]string{
|
||||
"AGENTS.md": "the mesh-controller module is prose\n" +
|
||||
"run `mesh-controller module check x` now\n" +
|
||||
"or:\n```\n$ mesh-controller push\n```\n" +
|
||||
"a quotation \"mesh-controller status\" stays\n" +
|
||||
"> mesh-controller doctor in a block quote\n" +
|
||||
"a mesh-controller pushed is another word\n",
|
||||
"04-ISSUES/1/00-report.md": "---\nopened: 2026-10-08\n---\nmesh-host apply\n",
|
||||
"04-ISSUES/2/00-report.md": "---\nopened: 2026-10-09\n---\nmesh-host apply\n",
|
||||
})
|
||||
got := checkLines(fsys)
|
||||
expectProblem(t, got, "AGENTS.md:2: `mesh-controller module` is a daemon's line")
|
||||
expectProblem(t, got, "AGENTS.md:5: `mesh-controller push` is a daemon's line")
|
||||
expectProblem(t, got, "04-ISSUES/2/00-report.md:4: `mesh-host apply`")
|
||||
if len(got) != 3 {
|
||||
t.Errorf("prose, quotations, block quotes and issues before 2026-10-09 are not problems: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinesListNamesEachLineOnce(t *testing.T) {
|
||||
list := linesList()
|
||||
seen := map[string]bool{}
|
||||
for _, l := range list {
|
||||
if seen[l] {
|
||||
t.Errorf("%q listed twice", l)
|
||||
}
|
||||
seen[l] = true
|
||||
}
|
||||
if !seen["mesh-controller push"] || !seen["mesh-controller module …"] || !seen["nox-mesh-host hand-over"] {
|
||||
t.Errorf("the list lacks a known line: %v", list[:5])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCatalogueCopyDifferenceIsCaughtAndNamesTheList(t *testing.T) {
|
||||
retired := []retiredWord{{"overlay", "all"}, {"alert", "tools"}, {"hq only", "hq"}}
|
||||
dir := t.TempDir()
|
||||
write := func(name, content string) {
|
||||
if err := os.WriteFile(path.Join(dir, name), []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
write("retired-words", "# copy\nalert\noverlay\n")
|
||||
write("retired-lines", strings.Join(linesList(), "\n")+"\n")
|
||||
expectNone(t, checkCopy(dir, retired))
|
||||
|
||||
write("retired-words", "alert\n")
|
||||
write("retired-lines", "mesh-controller push\n")
|
||||
got := checkCopy(dir, retired)
|
||||
expectProblem(t, got, "retired-words differs from the glossary: it should list alert, overlay")
|
||||
expectProblem(t, got, "retired-lines differs from this check's list")
|
||||
expectProblem(t, checkCopy(t.TempDir(), retired), "cannot be read")
|
||||
}
|
||||
|
||||
func mapOf(files map[string]string) fs.FS {
|
||||
m := fstest.MapFS{}
|
||||
for k, v := range files {
|
||||
m[k] = doc(v)
|
||||
}
|
||||
return m
|
||||
}
|
||||
+36
-29
@@ -403,38 +403,45 @@ and the operator act. **Recorded by** the controller's `identity` and `licences`
|
||||
back when it fails. The domain is continuous integration and delivery, named **delivery**, and its parts live in
|
||||
the repository `mesh-delivery` (ADR 0429 §2). **Recorded by** the `mesh-delivery` module (deliveries) and the
|
||||
controller (the planner, the rollout) until release 6 of ADR 0429; from each release on, by the parts it builds. **Downstream of** Module, Core, Health and repair and Data.
|
||||
**Decided by** ADR 0162, 0218, 0236, 0237, 0238, 0239, 0250, 0267, 0270, 0276, 0323, 0401–0408, 0428, 0429, 0444–0447.
|
||||
**Decided by** ADR 0162, 0218, 0236, 0237, 0238, 0239, 0250, 0267, 0270, 0276, 0323, 0401–0408, 0428, 0429, 0444–0447, 0524–0530.
|
||||
**Uses:** module, node, declaration and send (Placement), health (Health and repair), person.
|
||||
|
||||
- **merge check** — what a pull request carries before it may merge, judging the merge of its head into a
|
||||
named **trunk commit**, made in the builder's own clone and never pushed (ADR 0323). Until release 4 of
|
||||
ADR 0429 is delivered, two statuses: the **merge gate** (`mesh/merge-gate`, the build seat's composed check of
|
||||
the modules the change touches) and the **repository check** (`mesh/repo-check`, the repository's own
|
||||
`merge-check.sh`) (ADR 0238). From that release, one status, `mesh/check`, written by the checker: the
|
||||
change's tests, run by the language's build module, and the **composition check**; for a delivery group, one
|
||||
verdict on every member's head (ADR 0445, ADR 0447).
|
||||
named **trunk commit**, made in a clone of its own on a build node and never pushed (ADR 0323). One status,
|
||||
`mesh/check`, written by the checker: the change's tests, run by the language's build module, and the
|
||||
**composition check**; for a delivery group, one verdict on every member's head (ADR 0445, ADR 0447). Before
|
||||
release 4 of ADR 0429 a pull request carried two statuses, one for the modules the change touches and one
|
||||
for the repository's own script (ADR 0238).
|
||||
- **composition check** — whether the machines a change touches still compose with it, run by the assigner
|
||||
from the modules the indexer and the builder hold, with the composition the controller sends with
|
||||
(ADR 0446). It replaces the merge gate from release 4 of ADR 0429; the old word is struck once that release
|
||||
is delivered and every design that still says it is reworded (issue 1303).
|
||||
(ADR 0446). It replaces the controller's check of that kind, which composed every machine from release 4 of
|
||||
ADR 0429 back (issue 1303).
|
||||
*Not:* ~~merge gate~~ (hq)
|
||||
- **kept verdict** — a package's passing verdict at one test level, kept by the checker under the digest of
|
||||
everything its tests read, written once and read-only; a check takes it instead of running an unchanged
|
||||
package (ADR 0444).
|
||||
- **trunk commit** — the commit of the trunk a merge check's verdict was judged against: the trunk's head when
|
||||
the build seat cloned it. The verdict holds for that commit only, and goes out of date when the trunk moves
|
||||
the build module cloned it. The verdict holds for that commit only, and goes out of date when the trunk moves
|
||||
(ADR 0323).
|
||||
- **build seat** — the seat whose holder builds modules, `node-build-agent`; its holder on a node is the
|
||||
**builder**. Any node may hold the seat, so there is no build node by nature
|
||||
(ADR 0237).
|
||||
- **build seat** — a language's node seat, held on a node by that language's build module: `node-build-go`,
|
||||
`node-build-node` and `node-build-image` (ADR 0525 §2). Its accepted kinds are the **build streams**, and its
|
||||
verbs `current`, `pause` and `resume` serve that node's pulls. It replaces the seat `node-build-agent`, retired
|
||||
in R4g (ADR 0525 §10). Any node may hold it, and a node whose **build capacity** is above zero builds, so there is
|
||||
no build node by nature (ADR 0237).
|
||||
*Not:* ~~build machine~~
|
||||
- **build queue** — the builds waiting to run, kept by the bus as three work queues a builder takes in order:
|
||||
delivery builds, then checks of pull requests whose Approve is asked or given (ADR 0421 §10), then every other check; each check queue keeps one
|
||||
request per pull request, the newest (ADR 0406); one entry in it is a
|
||||
**build request**.
|
||||
The queue's verbs still say *ask* for an entry; the conversation's ask is another thing
|
||||
(see [Homonyms](#homonyms)).
|
||||
- **build queue** — the build requests waiting to run, kept by the bus alone: per language, the work-queue streams
|
||||
`delivery`, `check` and `recheck`, which a build module pulls from, deliveries first, while its node has a free
|
||||
slot of the build capacity (ADR 0525 §1, §4; ADR 0536 §4). No part keeps a list of them: the controller's queue
|
||||
and its verbs are gone, and a stream's backlog and the age of its oldest request are read from its consumers
|
||||
(ADR 0525 §9). One entry in it is a **build request**, an ask on a build stream for one whole job on one node;
|
||||
the conversation's ask is another thing (see [Homonyms](#homonyms)).
|
||||
- **build capacity** — how many build requests a node's build modules hold at once: a setting of the node that the
|
||||
build modules read, and 0 pulls nothing (ADR 0525 §5). Not a seat's capacity (**capacity**, Module).
|
||||
- **build module** — the module of one language that does the build steps for it on a node (fetch, build, digest,
|
||||
test), claiming that language's build seat: `mesh-build-go`, `mesh-build-node` and `mesh-build-image`. It pulls the
|
||||
build requests of its language and writes each step's end on its own key (ADR 0525 §3, §6). Not the **builder**.
|
||||
- **package** — what code resolves when it is **compiled**: an npm/cargo/pypi dependency, by **version**.
|
||||
Served by the **package-registry** (gitea). Only a builder talks to it.
|
||||
Served by the **package-registry** (gitea). Only a build module talks to it.
|
||||
- **artifact** — anything a build produces and the mesh delivers to a node by **digest**: an `image`, a
|
||||
mirrored `upstream` image, a `bundle` of the module's own code, an `archive`. Served by the
|
||||
**artifact store**, an OCI registry that holds every kind as content-addressed blobs
|
||||
@@ -478,7 +485,7 @@ controller (the planner, the rollout) until release 6 of ADR 0429; from each rel
|
||||
lone delivery (ADR 0401). Work is never held back on a branch other than a trunk: the trunk is what runs.
|
||||
*Not:* ~~release branch~~ (hq)
|
||||
- **toolchain** — a language's compiler and tools, installed by that language's build module on every node holding
|
||||
the build seat, pinned to one version that is the same on every builder; its version is part of a build's reuse
|
||||
its build seat, pinned to one version that is the same on every build node; its version is part of a build's reuse
|
||||
key (ADR 0405). No longer an image.
|
||||
- **delivery plan** — what a delivery does to the mesh: its **build plan** (the modules it moves and
|
||||
their dependents, in tiers), its **deploy plan** (per node, what it receives and what waits for a
|
||||
@@ -502,7 +509,7 @@ controller (the planner, the rollout) until release 6 of ADR 0429; from each rel
|
||||
and writes keys and never calls another part (ADR 0429 §1). Each key has one writer; the operator's
|
||||
credential may also write a want, the one manual escape. The parts: the **forge bridge** (forge events in,
|
||||
the only caller of the forge), the **indexer** (fingerprints, the candidates of a group, the modules of the
|
||||
trunk), the **builder** (claims and builds one fingerprint once), the **checker** (the merge check on built
|
||||
trunk), the **builder** (claims one fingerprint once and orders its build), the **checker** (the merge check on built
|
||||
artifacts), the **gate** (merges a group and moves the source revision; *gate part* where it could be read
|
||||
as another gate, see [Homonyms](#homonyms)), the **teller** (the operator's phone, and the only writer of an
|
||||
ask's answer), the **assigner** (writes each node's wants by its six rules), the **node agent** (the
|
||||
@@ -512,7 +519,7 @@ controller (the planner, the rollout) until release 6 of ADR 0429; from each rel
|
||||
- **fingerprint** — what identifies a module's build by its content: a hash over its source files, its
|
||||
manifest and the fingerprints of its build-time inputs, so equal content gives an equal fingerprint whatever
|
||||
the commit (ADR 0429 §5). It is today's source fingerprint with a base entering by its module's
|
||||
fingerprint, the builder by its own module's fingerprint, and a build resolving packages by its lock. From
|
||||
fingerprint, and a build resolving packages by its lock; from R4g the builder is no input of any fingerprint (ADR 0530). From
|
||||
release 4 a build's key, so one source is never built twice.
|
||||
- **source revision** — one revision of the key `source.main`, written only by the gate: every repository's
|
||||
merged head and the group that moved it. A group reaches the mesh as one source revision (ADR 0429 §11); a
|
||||
@@ -712,8 +719,8 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272, 0383, 03
|
||||
- **reference** — an opaque token in a message's words standing for a detail the content rule keeps out
|
||||
of them (a path, an address); opened with `detail` only on a `private` channel or through the mesh MCP server.
|
||||
- **agent** — a coding agent: the operator's, or one the mesh runs; to the mesh, an identity of kind agent
|
||||
(ADR 0386). Not the node-engine, and not the
|
||||
build seat's holder, whose seat name `node-build-agent` is an identifier to rename.
|
||||
(ADR 0386). Not the node-engine, and not a
|
||||
build module.
|
||||
- **thread** — one line of agent work, owned by the mesh: its turns, tool calls and results, its workspace
|
||||
and usage, kept whole for good on the bus by the thread seat; it outlives any session, node or front end
|
||||
(ADR 0383, ADR 0384).
|
||||
@@ -930,7 +937,7 @@ renaming one sense, the old sense moves to a *Not:* line and becomes mechanical.
|
||||
| | taking a consumer off a licence | **unbind** (verb `release`, to rename) | Identity and access |
|
||||
| | a copy of a work an engine can fetch | **media release** | Media |
|
||||
| gate | the judgement on a delivery's first node | **first-node gate** | Delivery |
|
||||
| | the pull request status, until release 4 of ADR 0429 | **merge gate**; from release 4, **composition check** | Delivery |
|
||||
| | the check of what a change makes of the machines it touches, a pull request status until release 4 of ADR 0429 | ~~merge gate~~ (hq); the **composition check** | Delivery |
|
||||
| | the part that merges a group and moves the source revision | **gate part** (`mesh-delivery-gate`) | Delivery |
|
||||
| | a failed step stopping its module's later steps (ADR 0136) | *a failed step holds its module* | Placement |
|
||||
| tier | a level of the mesh | **layer** | Core |
|
||||
@@ -942,7 +949,7 @@ renaming one sense, the old sense moves to a *Not:* line and becomes mechanical.
|
||||
| role | what a seat names, at a scope | **seat**; the bare word in older designs is tracked by issue 872 | Module |
|
||||
| | what an identity may call | **role** | Identity and access |
|
||||
| ask | a request for the operator's input | **ask** | Operator and conversation |
|
||||
| | an entry in the build queue | **build request** | Delivery |
|
||||
| | an entry in the build queue | **build request** (an ask on a build stream) | Delivery |
|
||||
| store | the one database server | **store** | Core |
|
||||
| | the OCI registry | **artifact store** | Delivery |
|
||||
| | a node's own copy of its last declaration | **last declaration** | Placement |
|
||||
@@ -954,13 +961,13 @@ renaming one sense, the old sense moves to a *Not:* line and becomes mechanical.
|
||||
| | the operator's terminal command, `mesh-cli` | **the `nox` command** | Operator and conversation |
|
||||
| knowledge base | the module holding the decision records and research efforts whole | **knowledge base** | The record |
|
||||
| | the predecessor's operational memory, in older playbooks (issue 430) | none: it no longer exists | — |
|
||||
| check | a pull request's two statuses, one from release 4 of ADR 0429 | **merge check**, **merge gate**, **repository check**; from release 4, `mesh/check` | Delivery |
|
||||
| check | a pull request's two statuses, until release 4 of ADR 0429 | **merge check**, **repository check**; from release 4, `mesh/check` | Delivery |
|
||||
| | whether the machines a change touches still compose, the assigner's | **composition check** | Delivery |
|
||||
| | a delivery group's verdict | **composed check** | Delivery |
|
||||
| | the controller's own examination | **self-check** | Health and repair |
|
||||
| | a file in `00-META/checks/` | **hq check** | The record |
|
||||
| agent | a coding agent | **agent** | Operator and conversation |
|
||||
| | the build seat's holder; from release 4 of ADR 0429 the part `mesh-delivery-builder` | **builder** | Delivery |
|
||||
| | the holder of the seat `node-build-agent` until R4g; from release 4 of ADR 0429 the part `mesh-delivery-builder` | **builder** (the part); **build module** (what builds) | Delivery |
|
||||
| the user | an account of a wrapped program, or of the bus server | **its account**, **bus account** | Identity and access |
|
||||
| | a human | **operator** or **person** | words every domain uses |
|
||||
| firewall | what the mesh enforces | **packet filter** | Connectivity |
|
||||
|
||||
@@ -71,7 +71,7 @@ The flow above is a rule, and a rule states how it is checked:
|
||||
decision, an `in-progress`/`implemented` design that names no owning code, two issue folders
|
||||
sharing a number or an issue report carrying a status key; a `graduated` research effort that
|
||||
does not say what it became is refused by the knowledge base's `propose` (ADR 0290). Run it with `records.py`, `index.py` and
|
||||
`words.py` before any HQ merge (`sh merge-check.sh` runs all four). The issue rules the check
|
||||
`words.py` before any HQ merge (`go test ./...` at the root runs all four as Go tests, and `sh merge-check.sh` runs the Python ones until the delivery of R4g is done: ADR 0527). The issue rules the check
|
||||
once held — `located` with an owner, `resolved` with a fix, a core issue with its replay — are
|
||||
refused at the write by the issue tracker's `move`
|
||||
(ADR 0275). What the checks cannot see — that code work actually started from a
|
||||
|
||||
@@ -111,7 +111,7 @@ issue whether it is being fixed in between, after the delivery, or waits, and on
|
||||
- what not to touch, and what lands beside it in other repositories;
|
||||
- the local facts: the worktree, the branch, the check commands, a temporary directory for tests outside
|
||||
the home (`TMPDIR`), and the unit tests to run (never a database server: tests that need one are
|
||||
integration tests, which `mesh-controller.try` and the forge's checks run).
|
||||
integration tests, which the try (`mesh-controller.try`) and the checker's merge check run).
|
||||
When the reading is large, a read-only agent on the smaller model answers the lead's exact questions with
|
||||
file:line, and the lead writes the file from its answers.
|
||||
5. **Write the findings into the issues** (the operator, 2026-10-17: the better the content of the issues, the
|
||||
@@ -173,7 +173,7 @@ issue whether it is being fixed in between, after the delivery, or waits, and on
|
||||
acceptance criteria**: one test or more per criterion at the level it names, each test's comment citing
|
||||
the criterion it proves by its label (`// 1118/AC2`), and no criterion left without its test; an integration criterion is a test
|
||||
that compiles and waits for the try at this step's end. The integration tests (those that start a store, a bus
|
||||
or a process) are run first by that try, and again by the forge's merge check.
|
||||
or a process) are run first by that try, and again by the checker's merge check.
|
||||
- **No pull request is opened in this step. When every issue is committed, the lead brings each feature branch up
|
||||
to the current trunk (fetch by name, then merge), pushes it and tries it** (`mesh-controller.try`, as step 3 item 1 says), so the review reads the integration tests' real
|
||||
results and judges none by reasoning (2026-10-19: a reviewer reasoned that a warning goes out at once, the
|
||||
@@ -184,7 +184,7 @@ issue whether it is being fixed in between, after the delivery, or waits, and on
|
||||
message ("issue 805: landing in mesh-catalog"); reach the mesh only through the mesh MCP server, never ssh;
|
||||
never push, open, merge or close a pull request; never write to a store's database by hand, a schema change
|
||||
is a numbered migration and the consolidated schema; Go only unless the context file says otherwise; never
|
||||
move another issue; **never run an integration test on the machine** (no `go test -tags integration`, no `merge-check.sh`,
|
||||
move another issue; **never run an integration test on the machine** (no `go test -tags integration`,
|
||||
no store or bus started by hand): compile them with `go vet -tags integration ./...` and leave the running to
|
||||
the lead's try at the end of this step, where `mesh-controller.try` runs them on the pushed branch. On 2026-10-17 three fixing agents ran them anyway, one for minutes on end, because the
|
||||
brief only said they wait for the check (the operator). Report back in under 150 words: commits, tests added and that they failed first, the
|
||||
@@ -230,17 +230,27 @@ issue whether it is being fixed in between, after the delivery, or waits, and on
|
||||
further case, a nicer form: the reviewer opens it with `issue-tracker.open` and names the number in its
|
||||
report, and the round does not wait for it. What a round asks is only what makes the change wrong against
|
||||
its issues, their acceptance criteria and its decisions.
|
||||
8. **A timer that writes state, or a rule fixed in code that overrides what a person sets, makes the change
|
||||
wrong.** The mesh is NATS-native: a part says what happened as an event and acts on events; nothing polls
|
||||
and rewrites state on an interval, and a policy the operator can change is a setting the module declares,
|
||||
never a constant. The reviewer reads every new or changed interval, ticker, sleep loop and periodic
|
||||
run-once in the change and asks of each: what event should drive it instead, and what setting turns it off.
|
||||
One with no answer is a change asked. On 2026-10-23 the forge module's loop rewrote every repository's
|
||||
branch protection every ten minutes from rules in its code and undid the operator's changes at the forge
|
||||
(issue 1903).
|
||||
|
||||
## Step 3 — Check (no review)
|
||||
|
||||
1. **Push the reviewed heads, try again a head the review changed, then open one pull request per repository,
|
||||
together.** **The integration tests run before the pull requests are opened**, first before the review (step 1)
|
||||
and again for a head the review changed since its try: `mesh-controller.try` with the
|
||||
repository and the pushed branch runs the repository's `merge-check.sh` (main's version) on the merge, in the
|
||||
toolchain containers and against the throwaway store and bus a merge check gets, and answers with an id to
|
||||
read back. A try that fails goes to step 3 item 3 as a failed check does; the pull requests are opened only
|
||||
repository and the pushed branch is a check ask of the checker (ADR 0501 §5): the checker runs the repository's tests on the
|
||||
merge with the trunk's head, at the unit and the integration level, through the build module of the repository's language and by
|
||||
that language's convention, as the merge check does, and answers with an id to read back. The try is an ask on the language's check
|
||||
stream (ADR 0525 §1), so it waits behind the asks before it until a build node has a free slot; no script of the repository decides
|
||||
it (ADR 0445, ADR 0529). A try that fails goes to step 3 item 3 as a failed check does; the pull requests are opened only
|
||||
when every head's last try passed (the operator, 2026-10-18: three pushes in a row failed their first merge check on
|
||||
faults a try finds in minutes). Never `merge-check.sh`, `go test -tags integration` or a store or bus started
|
||||
faults a try finds in minutes). Never `go test -tags integration` or a store or bus started
|
||||
by hand on the machine (the operator, 2026-10-18). Pull requests in two or
|
||||
more repositories that share the branch name are one delivery group (ADR 0239 §2).
|
||||
Where a session pushes through the forge tool `gitea_commit` (one commit on the forge's current base), the
|
||||
@@ -271,20 +281,21 @@ issue whether it is being fixed in between, after the delivery, or waits, and on
|
||||
(also in a delivery group that holds the catalogue's pull request); the lead runs mesh-controller's unit
|
||||
suite with `MESH_CHECK_BESIDE` naming a directory that holds the catalogue's branch, as mesh-controller's
|
||||
`internal/beside` says.
|
||||
2. **The forge's merge checks run the integration tests again, as the gate** (`mesh/merge-gate`, `mesh/repo-check`). No
|
||||
2. **The checker's merge check runs the integration tests again, as the gate** (its one status, `mesh/check`). No
|
||||
second review: step 2 reviewed this tree.
|
||||
3. **A check that fails** goes back to the same reviewer, continued by its id, with the failure, for **one more cycle** of step 2 with
|
||||
the same fixing agents; then push once again. **The failure is read from the check's whole log**, never
|
||||
reasoned from its summary: `mesh-delivery.show` names the check's build id, and `mesh-controller.builds` with
|
||||
`log` set to that id prints everything the builder said. A check's summary picks only some failing lines;
|
||||
on 2026-10-17 it showed a store test's `FAIL` without its cause, and a fix was first guessed from the code.
|
||||
**A change to `merge-check.sh` is judged by main's version of it**, so a test the change adds must pass under
|
||||
either version: one that runs the new script's behaviour runs a file of its own, never `merge-check.sh` itself.
|
||||
**So a change that needs the new script to judge it ships the script first, as a small fix of its own**, and the
|
||||
rest waits for its delivery: on 2026-10-17 milestone 3 of design 47 moved its integration tests behind a build
|
||||
tag in the same pull request as the script that runs that tag, and main's script would have skipped every
|
||||
tagged test, its acceptance tests among them. The prepare step looks for this: a change to `merge-check.sh` or to
|
||||
what it selects (build tags, the store-tests list) is cut ahead of the milestone.
|
||||
**A change to the Go build module's recipe is judged by the build module the nodes run**, not by the changed one, so a
|
||||
test the change adds must pass under either version. **A change that needs the new recipe to judge it ships the recipe
|
||||
first, as a small fix of its own**, and the rest waits for its delivery: on 2026-10-17 milestone 3 of design 47 moved its
|
||||
integration tests behind a build tag in the same pull request as the script that ran that tag, and main's script would
|
||||
have skipped every tagged test, its acceptance tests among them. Since R4g no repository holds such a script (ADR 0529):
|
||||
which tests run at which level is the build tag's (ADR 0445 §2), and hq's checks are Go tests like any repository's
|
||||
(ADR 0527). The prepare step looks for this: a change to what the checker's build module selects (its recipe, a build
|
||||
tag convention) is cut ahead of the milestone.
|
||||
4. **The milestone's acceptance is proven by its check.** A milestone that adds or changes a module
|
||||
carries an integration test that starts the module whole, its process against the real bus and store, and
|
||||
shows the milestone's acceptance sentence; the forge's checks run it (ADR 0391 §1, §6). Unit tests alone are not
|
||||
@@ -326,7 +337,7 @@ survive.
|
||||
**What a merge moves is read from its plan before the operator is told.** A change to mesh-sdk rebuilds every
|
||||
module that takes the SDK, with their backups and their asks for a person (issue 721): on 2026-10-20 the lead
|
||||
said an SDK merge moved nothing, and its delivery plan moved mesh-sdk with 41 dependents to all four machines.
|
||||
The lead reads the merge gate's plan in `mesh-delivery.show` and names what it moves before asking an Approve.
|
||||
The lead reads the delivery plan in `mesh-delivery.show` and names what it moves before asking an Approve.
|
||||
|
||||
**A failed plan's Retry is asked of the operator alone, never beside an Approve.** An Approve's merge starts a
|
||||
plan, and the controller refuses a Retry while another plan runs; on 2026-10-20 the operator chose both at once,
|
||||
@@ -340,7 +351,7 @@ mail's module to a build its upgrade policy holds for a person's word, and recre
|
||||
every push by hand, the lead reads `mesh-controller.plan` with `diff` for that machine, and for the machine holding
|
||||
the bus when a push issues accounts, and pushes only when the plan names what the lead means to change.
|
||||
|
||||
**A new module's first assignment is its first real composition.** The merge gate composes every machine as it is
|
||||
**A new module's first assignment is its first real composition.** The composition check composes every machine as it is
|
||||
assigned, so a module that no machine runs yet is composed by no check: on 2026-10-21 the build modules' first
|
||||
assignment found two faults no merge check could see (issue 1537, a template the controller filled for a value the
|
||||
forge serves only later; issue 1542, the node-engine refusing the module's own `.ssh`). So a new module is assigned
|
||||
@@ -348,6 +359,31 @@ to one machine first, its plan read before the push and its apply read after it
|
||||
answer, and `status` for a failed resource), and taken back at once when that machine cannot compose or apply, so
|
||||
the machine keeps applying everything else; the other machines follow once it runs there.
|
||||
|
||||
## Deliver the controller, the builder or a build module without the builder
|
||||
|
||||
The builder delivers its own module, the build modules and the controller from its keys, and nothing falls back to another builder
|
||||
(ADR 0524 §7). When the builder or a build module cannot write references, or a controller cannot take its own fix (issue 1886: the
|
||||
controller's plan could never finish, and no plan it made could deliver its own fix), the way is ADR 0526's, in this order, each
|
||||
step recorded in the hand-act log with its reason:
|
||||
|
||||
1. **Revert, with no build.** Revert the change that broke the builder or the build module: the earlier source's fingerprint has a done key naming
|
||||
references, so the revert's delivery builds nothing. Read first that the artifact store still holds the earlier artifact
|
||||
(`mesh-controller.artifacts`): it keeps a recorded build while it is among its module's five newest or a definition names it.
|
||||
2. **A build by a session's call.** Call the build module's fetch and build tools on one named build node (for Go, `build_go_fetch` and then
|
||||
`build_go_build` with `publish: true`); the build module pushes the artifact and answers its reference. `mesh-pack` in mesh-sdk gives a
|
||||
hand build the build module's form; write the Go version of each hand build down, since another version gives another digest.
|
||||
3. **Register by reference, at the controller's terminal only.** The operator names the module, its repository, its path and each artifact's
|
||||
reference there; no verb reaches it. Say before you ask that this is a terminal step, and which notifications it raises; check that the
|
||||
command exists on the node before you hand it over.
|
||||
4. **A controller that cannot take its own fix** is put on the control-node by hand: register its hand build as in step 3, read the control-node's
|
||||
plan, then `mesh-controller.push`. Only in the worst case does the operator send a declaration by hand at the terminal (its source and
|
||||
digest the hand build's) and register it as in step 3 so the next push keeps it.
|
||||
5. **Before a switch that moves the controller, the builder or a build module,** the lead keeps the running versions' artifacts outside the
|
||||
artifact store, by digest on two nodes, with module, artifact, commit, Go version and the steps that put each back; the store's nightly
|
||||
collection removes a blob the controller did not record.
|
||||
|
||||
Nothing here is a work-around for a delivery that can run normally: while the builder delivers, no hand act replaces it.
|
||||
|
||||
## Models
|
||||
|
||||
| Who | Model | Why |
|
||||
@@ -371,7 +407,7 @@ on, with when it started and its bound.
|
||||
|---|---|---|
|
||||
| a fixing agent's issue | 30 minutes | ask it where it stands; split or take over |
|
||||
| the review | 15 minutes a round | ask the reviewer; stop at round 3 |
|
||||
| a merge check | 15 minutes | read `mesh-delivery.show` (its state and verdict), then the check's log; the build queue (`mesh-controller.queue`) never lists a running check |
|
||||
| a merge check | 15 minutes | read `mesh-delivery.show` (its state and verdict), then the check's log; the check stream's backlog and the age of its oldest ask are in the delivery view (ADR 0525 §9) |
|
||||
| the Approve's ask | 5 minutes after both checks pass | read `mesh-delivery.show`: an ask not raised is a fault, said to the operator |
|
||||
| a merge after the Approve | 5 minutes | read `mesh-delivery.show` for the delivery |
|
||||
| a delivery plan | its class's budget (`mesh-delivery.times`), at most 30 minutes | read `mesh-controller.plans`, then `mesh-delivery.stalled` |
|
||||
|
||||
@@ -38,7 +38,7 @@ one delivery**. Everything before the try is local and fast.
|
||||
decision stays a small fix. **The session's own model (the larger) then writes the issue's acceptance
|
||||
criteria** as playbook [07](07-feature-branches.md) step 0 item 5 says: each one sentence, given, when, then,
|
||||
naming the level of the test that proves it, as an `issue-tracker.note` headed `Acceptance criteria`. A
|
||||
playbook or design edit has none: its check is `sh merge-check.sh`.
|
||||
playbook or design edit has none: its check is `go test ./...` at hq's root, which runs the four checks (ADR 0527).
|
||||
2. **Move the issue to `fixing`** with `by` as who@node.
|
||||
3. **Build on a local branch** `fix/<issue>-<slug>`, in a worktree of its own branched from `main`, never in
|
||||
the repository's own clone, which stays on `main` (playbook [07](07-feature-branches.md), step 0, item 2). Write the
|
||||
@@ -50,7 +50,7 @@ one delivery**. Everything before the try is local and fast.
|
||||
caught only by the review); read `git show --stat` before pushing.
|
||||
The tests are written from the acceptance criteria, at least one per criterion at its level, each citing the
|
||||
criterion it proves by its label (`<issue>/AC<n>`, playbook 07 step 0 item 5). Commit, naming the issue. For a playbook or design edit, run the repository's own checks (in hq,
|
||||
`sh merge-check.sh`). Then push the branch and run `mesh-controller.try` on it, so the review reads the
|
||||
`go test ./...` at the root). Then push the branch and run `mesh-controller.try` on it, so the review reads the
|
||||
integration tests' real results; a failed try goes back to the same agent before the review.
|
||||
4. **One review of the commit and its try** (a reviewer that did not write it, larger
|
||||
model; at most 3 rounds, as playbook 07; from round 2 only what is wrong; it reads each acceptance criterion
|
||||
@@ -60,13 +60,13 @@ one delivery**. Everything before the try is local and fast.
|
||||
operator's Approve of its merge is its review, and the review record says so.
|
||||
5. **Push the reviewed head, try it again if the review changed it, and open the pull request.** The trunk is what runs and nothing holds merged work back
|
||||
(ADR 0401), so a fix waits for no other change's delivery. `mesh-controller.try` on the pushed branch runs
|
||||
the integration tests, and the pull request is opened only when the head's last try passed; the forge's merge check runs
|
||||
the integration tests, and the pull request is opened only when the head's last try passed; the checker's merge check runs
|
||||
them again as the gate (playbook [07](07-feature-branches.md) step 3). They are never run on the machine. A
|
||||
failed try or check goes back to the same agent with the failure, and the reviewer reads only the change it makes.
|
||||
6. **The pull request's description carries the review record** (`Reviewed-by: <who@node> at <full sha>`,
|
||||
playbook [07](07-feature-branches.md) step 3) and the session that pushed it (`Pushed-by: <who@node>`, ADR 0505 §5), and mesh-delivery asks the operator's Approve as soon as its
|
||||
checks pass and merges the approved head (ADR 0421); nobody marks, labels, comments or rechecks. A fix to a module that changes how it runs carries an
|
||||
integration test starting the module whole against the real bus and store, run by `mesh-controller.try` and the forge's checks
|
||||
integration test starting the module whole against the real bus and store, run by `mesh-controller.try` and the checker's merge check
|
||||
(ADR 0391 §1, §6); unit tests alone are not that proof, and the lab is not used. The operator's word is waited for only when the fix changes what the operator sees, removes data,
|
||||
or needs the operator's terminal.
|
||||
7. **Follow its delivery plan to done** (`mesh-controller.plans` with `wait` and `until: done`), prove the fix on
|
||||
@@ -99,9 +99,9 @@ code.
|
||||
| Waiting on | Bound | Past the bound |
|
||||
|---|---|---|
|
||||
| building the fix | 1 hour | it is a feature: stop and plan it under playbook 07 |
|
||||
| a slot for the try | 30 minutes | `mesh-controller.try` holds at most four tries and refuses a fifth without queueing it: a watcher asks again every minute; past the bound, read `mesh-controller.queue` |
|
||||
| the try | 30 minutes | a try is a check ask on the language's check stream and waits behind the asks before it for a build node's free slot (ADR 0525); past the bound, read the stream's backlog and the age of its oldest ask in the delivery view |
|
||||
| the review | 15 minutes | ask the reviewer where it stands |
|
||||
| the merge check | 15 minutes | read `mesh-controller.queue` and the check's log |
|
||||
| the merge check | 15 minutes | read `mesh-delivery.show`, the check's log and the check stream's backlog in the delivery view |
|
||||
| the Approve's ask | 5 minutes after both checks pass | read `mesh-delivery.show`: an ask not raised is a fault, said to the operator |
|
||||
| the merge after the Approve | 5 minutes | read `mesh-delivery.show` for the delivery |
|
||||
| the delivery plan | its class's budget (`mesh-delivery.times`), at most 30 minutes | read `mesh-controller.plans`, then `mesh-delivery.stalled` |
|
||||
|
||||
@@ -302,7 +302,7 @@ code{font:12.5px ui-monospace,monospace;background:var(--soft);padding:1px 5px;b
|
||||
|
||||
<!-- ============ TESTS ============ -->
|
||||
<h2 id="tests">When tests run</h2>
|
||||
<p class="lead">Two levels. Unit tests run all the time on the machine; integration tests (a bus, a store or a process started) never run there: <code>mesh-controller.try</code> runs them on the pushed branch, and the forge's merge check runs them again as the gate.</p>
|
||||
<p class="lead">Two levels. Unit tests run all the time on the machine; integration tests (a bus, a store or a process started) never run there: <code>mesh-controller.try</code> runs them on the pushed branch (a check ask on the language's stream), and the checker's merge check runs them again as the gate.</p>
|
||||
<div class="box scroll">
|
||||
<svg viewBox="0 0 1080 170" role="img" aria-label="Test levels over a feature's timeline">
|
||||
<defs><marker id="ar3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0L10,5L0,10z" fill="var(--mute)"/></marker></defs>
|
||||
@@ -465,11 +465,11 @@ const P = [
|
||||
{n:"07",name:"Features",kind:"build",c:"--build",
|
||||
trig:"Work that changes code across issues or repositories, too large for a small fix.",
|
||||
who:"A lead (larger model), one fixing agent per repository (smaller), one reviewer (larger).",
|
||||
steps:["Prepare: name the branch, make the worktrees, settle every question.","Write one context file per repository.","Write the findings and acceptance criteria into each issue.","Survey every other reader and writer of each changed value. (new)","List every part the change reaches; copy fixtures from the real writer.","Develop: one agent per repository, one commit per issue.","A test fails first on behaviour, not on compiling. (new)","Bring each branch up to the trunk, push, and try it. (new)","Review all of it with the tries' results; at most 3 rounds.","Push the reviewed heads; try again only what changed.","Open one pull request per repository, together, with Reviewed-by and Pushed-by.","The operator's Approve on Telegram; the group merges in one pass.","Follow the delivery plan to done; resolve the issues; clean up."],
|
||||
steps:["Prepare: name the branch, make the worktrees, settle every question.","Write one context file per repository.","Write the findings and acceptance criteria into each issue.","Survey every other reader and writer of each changed value. (new)","List every part the change reaches; copy fixtures from the real writer.","Develop: one agent per repository, one commit per issue.","A test fails first on behaviour, not on compiling. (new)","Bring each branch up to the trunk, push, and try it. (new)","Review all of it with the tries' results; at most 3 rounds.","Push the reviewed heads; try again only what changed.","Open one pull request per repository, together, with Reviewed-by and Pushed-by.","The operator's Approve on Telegram; the group merges in one pass.","Follow the delivery plan to done; resolve the issues; clean up.","When the builder cannot deliver its own fix: revert, a build by a session's call, a registration at the terminal, copies kept outside the store. (new)"],
|
||||
prod:"One pull request per repository, one delivery plan, resolved issues, no branch or worktree left.",
|
||||
next:"02 (when it ships)",bounds:"Prepare 15 min · 30 min per issue · review 15 min, ≤ 3 rounds · one try and one check",
|
||||
prev:["Pull requests as scratchpads","Context bought again and again","Review rounds spent on what a run would show"],
|
||||
check:"The forge's merge-gate and repo-check; the review record; the tracker's move."},
|
||||
check:"The checker's one status, mesh/check; the review record; the tracker's move."},
|
||||
{n:"08",name:"Small fixes",kind:"build",c:"--build",
|
||||
trig:"One issue in one repository, about an hour of work.",
|
||||
who:"The session, with one reviewer on the larger model.",
|
||||
@@ -477,7 +477,7 @@ const P = [
|
||||
prod:"One pull request, one delivery, a resolved issue.",
|
||||
next:"—",bounds:"About an hour · one try before the review, one review, one check",
|
||||
prev:["A fix reviewed after its pull request opened","A long part run twice","A fix waiting on someone else's delivery","Ten SDK merges and eight controller merges in a day, one per fix"],
|
||||
check:"The forge's checks; the review record; the tracker's move; one delivery group and one delivery plan per milestone."},
|
||||
check:"The checker's status; the review record; the tracker's move; one delivery group and one delivery plan per milestone."},
|
||||
{n:"09",name:"Cleanup",kind:"side",c:"--side",
|
||||
trig:"The operator asks, or merged branches, stale pull requests and old worktrees pile up.",
|
||||
who:"A session on the operator's word; the operator decides every deletion.",
|
||||
|
||||
@@ -15,7 +15,7 @@ decisions:
|
||||
|
||||
# 30 — The mesh updates itself on a push
|
||||
|
||||
**Where it stands, 2026-10-17** (the records cleanup, issue 1072; playbook 07, "The design's state is written back"). Built on main: mesh-controller's delivery plans, merge gate and merge window (plan.go, release_plan.go, merge_gate.go, merge_window_bus.go, changeplan.go, with their migrations), which design 47 builds on. Not yet checked against this text nor given an as-is: issue 1075.
|
||||
**Where it stands, 2026-10-17** (the records cleanup, issue 1072; playbook 07, "The design's state is written back"). Built on main: mesh-controller's delivery plans, composition check and merge window (plan.go, release_plan.go, merge_gate.go, merge_window_bus.go, changeplan.go, with their migrations), which design 47 builds on. Not yet checked against this text nor given an as-is: issue 1075.
|
||||
|
||||
**Today the mesh does not update itself; a person drives the pipeline by hand, and one class of
|
||||
change freezes it.** A code change lands in `mesh-controller` or `mesh-catalog`, and getting it onto
|
||||
@@ -168,7 +168,7 @@ Revision, ADR 0238
|
||||
it — the module's own directory, the whole repository for a module built from its root, a repository a
|
||||
recipe names — and a file no build reads, at the root or in a directory holding no module, rebuilds
|
||||
nothing. One function maps a changed file onto modules and one answers what a merge moves and builds;
|
||||
the merge handler, the what-if, the merge gate and a pull request's check ask them. Only a commit on the
|
||||
the merge handler, the what-if, the composition check and a pull request's check ask them. Only a commit on the
|
||||
module's trunk is registered.
|
||||
|
||||
## What a push does not send (2026-10-05)
|
||||
|
||||
@@ -69,7 +69,7 @@ calls, the hand-act log, the facts snapshot), **`mesh-host`** (the node-engine:
|
||||
order, epoch refusal, the `report` verb, rollback witnessing), **mesh-tools** (the tool runner and the
|
||||
mesh MCP server: their heartbeat, passing every argument, health answers), **mesh-catalog** (the
|
||||
`operator-channel` seat's holder and channels, the watcher, the providers' retirement, the catalogue's
|
||||
merge gate), **mesh-sdk** (the TypeScript providers' loop and its retirement), **mesh-lab** (the
|
||||
composition check), **mesh-sdk** (the TypeScript providers' loop and its retirement), **mesh-lab** (the
|
||||
replays and the induced-failure scenarios).
|
||||
|
||||
---
|
||||
@@ -320,7 +320,7 @@ producer that wrote it.
|
||||
|
||||
| Probe | Asserts | From |
|
||||
|---|---|---|
|
||||
| D1 | every machine's declaration composes, and passes the node-engine's validation (the validator is a package of `mesh-host` the controller and the merge gate import — one validator) | 236, 263 |
|
||||
| D1 | every machine's declaration composes, and passes the node-engine's validation (the validator is a package of `mesh-host` the controller and the composition check import — one validator) | 236, 263 |
|
||||
| D2 | every holder of the mesh's resolver answers a machine name for IPv4, and NODATA for IPv6 | 262 |
|
||||
| D3 | every seat on record has a live holder that answers (`holder-silent`, H3) | 208, 218 |
|
||||
| D4 | every kept archive is held by a manifest | 253 |
|
||||
@@ -685,7 +685,7 @@ the bus server, the store and the node-engine. No secret and no address: an addr
|
||||
from a documentation range. It is kept in the artifact store as `facts/latest`, where the build seat
|
||||
reads it.
|
||||
|
||||
**The merge gate.** In mesh-controller, `mesh-host` and mesh-catalog, a check composes every machine of
|
||||
**The composition check.** In mesh-controller, `mesh-host` and mesh-catalog, a check composes every machine of
|
||||
the snapshot with the change applied and runs the node-engine's validator over each. A change that
|
||||
makes any machine fail to compose or validate fails its check, naming the machine's role and the
|
||||
module. The resolver module's tests run under both C libraries the snapshot lists. A test in each core
|
||||
@@ -907,7 +907,7 @@ a build whose migration cannot be undone; the verdicts of the next three core bu
|
||||
| Repository | Delivers |
|
||||
|---|---|
|
||||
| mesh-controller | the facts snapshot and S14 |
|
||||
| mesh-controller, `mesh-host`, mesh-catalog | the compose-and-validate merge gate; versions tested as run; the resolver's tests under both C libraries |
|
||||
| mesh-controller, `mesh-host`, mesh-catalog | the compose-and-validate check; versions tested as run; the resolver's tests under both C libraries |
|
||||
| mesh-lab | R4, R5 and the rest of the window's incidents; running the replays on every core merge |
|
||||
|
||||
**Done when:** the replays of 236, 262, 263 and 266 fail on the commit before their fix and pass after;
|
||||
@@ -921,7 +921,7 @@ the verdict as their status, and `merge-check.sh`; in `mesh-host` `merge-check.s
|
||||
installer's user list; in mesh-lab the replays of 262 and 266, the register and the prover; in hq the
|
||||
replay rule in `cycle.py`. The prover ran the replays of **236, 262, 263, 266 and 273: each fails on the
|
||||
commit before its fix** (236's check did not exist there) **and passes on it**. **Not yet:** R4, R5 and the
|
||||
rest of the window's incidents as replays; the merge gate's first runs against the live mesh's snapshot;
|
||||
rest of the window's incidents as replays; the composition check's first runs against the live mesh's snapshot;
|
||||
the status required to merge, which is the operator's setting on the forge; mesh-tools' and mesh-sdk's
|
||||
`merge-check.sh`.
|
||||
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-controller, mesh-catalog modules/mesh-delivery, mesh-delivery, mesh-host, mesh-sdk]
|
||||
updated: 2026-10-21
|
||||
code: [mesh-controller, mesh-catalog modules/mesh-delivery, mesh-delivery, mesh-host, mesh-sdk, hq 00-META/checks]
|
||||
updated: 2026-10-23
|
||||
decisions:
|
||||
- "ADR 0536"
|
||||
- "ADR 0534"
|
||||
- "ADR 0529"
|
||||
- "ADR 0530"
|
||||
- "ADR 0528"
|
||||
- "ADR 0527"
|
||||
- "ADR 0526"
|
||||
- "ADR 0525"
|
||||
- "ADR 0524"
|
||||
- "ADR 0494"
|
||||
- "ADR 0493"
|
||||
- "ADR 0491"
|
||||
@@ -109,8 +118,15 @@ source revision is derived from the keys: every **want** equals its module's **f
|
||||
forge.
|
||||
- **The indexer** computes each module's fingerprint at the merge preview and writes the candidates of each
|
||||
group and the modules of the trunk.
|
||||
- **The builder** claims a build by creating its key, once per fingerprint, and builds it; the builder is made in R4c from
|
||||
build-agent's code, and build-agent builds the builder's own module beside it until R4g, then leaves (ADR 0453 clause 5). The builder builds only on nodes given a capacity above zero, which two nodes have at first; a capacity of 0 means never chosen (ADR 0478 §3, ADR 0485).
|
||||
- **The builder** claims a build by creating its key, once per fingerprint, and orders it as an ask on the stream of the artifact's
|
||||
language; it makes no artifact's bytes since R4e and chooses no node, because the build modules pull the asks (ADR 0498 §1, ADR 0525 §1, §4). From R4g it
|
||||
delivers its own module, the build modules and the controller from its keys, and nothing falls back to build-agent, which is removed
|
||||
in that milestone (ADR 0453 clause 5, ADR 0524 §5, §7).
|
||||
- **The build modules** (`mesh-build-go`, `mesh-build-node`, `mesh-build-image`) each claim a node seat of their language
|
||||
(`node-build-go`, `node-build-node`, `node-build-image`) and pull its asks on their node while the node has a free slot of its
|
||||
**build capacity**, a setting of the node that the build modules read; a node at capacity 0, the control-node among them, pulls nothing
|
||||
(ADR 0525 §2, §4, §5). One ask is one whole job on one node: the fetch, the build and the push of a fingerprint, or the fetch, the digest and
|
||||
the levels of a check (ADR 0525 §3). A session may call a build module's tools on a named node (ADR 0525 §8).
|
||||
- **The checker** runs the merge check on built artifacts: a change's tests, through the build module of each
|
||||
module's language and by that language's convention, running only the packages whose inputs changed and
|
||||
taking the kept verdict of the rest; it writes the change's one status (ADR 0444, ADR 0445, ADR 0447); it runs dark in R4c-5 (ADR 0484).
|
||||
@@ -123,7 +139,9 @@ source revision is derived from the keys: every **want** equals its module's **f
|
||||
It also runs the **composition check**: whether the machines a change touches still compose with it, read
|
||||
from the keys the indexer and the builder hold and from what the controller keeps of machines, settings and
|
||||
grants, with the composition it sends with (ADR 0446). Its module
|
||||
is made in R4c for that check alone; its wants come in release 5.
|
||||
is made in R4g, in one milestone with release 5: it writes `want.<node>.<module>` by the six rules in its own bucket, never deletes one, and
|
||||
writes every difference between a want and what the controller sends with the rule that explains it (wants are only compared; the node agent
|
||||
applying them is release 6); the controller writes `grants.<module>.<fingerprint>` where it issues a credential (ADR 0524 §4, ADR 0528).
|
||||
- **The node agent**, the node-engine's own process, applies its wants one module at a time, backs up a data
|
||||
module first, and writes its haves. A node's declaration is not bounded by one bus message: a file over 64 KiB
|
||||
travels by digest in the bus's object store, in a bucket of that node's own that no other node can read, and a
|
||||
@@ -137,7 +155,8 @@ Nine of these are modules of the repository `mesh-delivery`, prefixed `mesh-deli
|
||||
repository is made by the forge module from its config (ADR 0428; issue 1128 makes the config value reach the
|
||||
node at once). The reviewer stays the review agent's session.
|
||||
|
||||
It lands in six releases, each leaving the mesh working, each one milestone below (ADR 0429 §7). Until the
|
||||
It lands in six releases, each leaving the mesh working, each one milestone below (ADR 0429 §7); releases 4 and 5 end in one milestone, R4g,
|
||||
with one switch-over and no rehearsal (ADR 0524 §1). Until the
|
||||
release that replaces it is delivered, each section from here to *The bootstrap* stays this design and its rule
|
||||
(ADR 0429 §21); each is rewritten when the release that replaces it is prepared (issue 1130).
|
||||
|
||||
@@ -152,7 +171,7 @@ release that replaces it is delivered, each section from here to *The bootstrap*
|
||||
(deliveries, groups, a key per │
|
||||
pull request, the state table) ▼
|
||||
node-engine on every node
|
||||
builders (build seat) ◄── work queues: delivery builds, approve-asked checks, other checks ───────────────── (reports; backups first)
|
||||
build modules (build seats) ◄── asks on streams: delivery, check, recheck ────────────────────────────── (reports; backups first)
|
||||
```
|
||||
|
||||
- **mesh-delivery** (the seat `mesh-delivery`) owns hotfixes, features and milestones as deliveries and delivery
|
||||
@@ -162,9 +181,8 @@ release that replaces it is delivered, each section from here to *The bootstrap*
|
||||
- **The controller** keeps planning, ordering, registration, sending and judging. It writes the chain's
|
||||
links it owns — builds, rollouts with their tiers, assignments — and the merge window's
|
||||
keys, all on the bus, by compare-and-set, while it holds the lease.
|
||||
- **The builders**, the holders of the build seat, build and check with toolchains installed on their node, as
|
||||
the build seat's own account, and take their jobs from the bus's work queues, delivery builds first
|
||||
(ADR 0405, ADR 0406).
|
||||
- **The build modules** build and check with toolchains installed on their node, each as an account of its own, and pull
|
||||
their asks from the bus's streams, deliveries first (ADR 0405, ADR 0525).
|
||||
- **The forge module** (`gitea`) publishes every forge event on the bus through the forge's system webhook
|
||||
(issue 729): pull requests updated, synchronized, merged and closed, and commit statuses; no label or comment
|
||||
moves a delivery, and mesh-delivery subscribes to none (ADR 0421 §11). It sets statuses, keeps the delivery's view as one comment on the pull request, and appends
|
||||
@@ -298,21 +316,25 @@ commit on the SDK's trunk or the head of an SDK pull request the forge holds, op
|
||||
|
||||
*ADR 0323, ADR 0350, ADR 0395, ADR 0406.*
|
||||
|
||||
- **Three work queues, taken in order.** Delivery builds go on a work-queue stream of their own; checks of pull
|
||||
requests whose Approve is asked or given on a second (ADR 0421 §10); every other check on a third. A builder takes from the first, then the
|
||||
second, then the third. Each check stream has one subject per pull request and keeps only the newest request:
|
||||
a newer request replaces one still waiting, by the stream's rule. The fresh check of such a pull request, asked
|
||||
on the merge path, is published on the second stream; a request left behind on the third for a head and trunk commit already
|
||||
judged or running is acknowledged and dropped when taken, by reading the pull request's key. Nothing pauses the
|
||||
worker or deletes and republishes a request (issue 558).
|
||||
- **A delivery builds first.** While a delivery builds, the build seat takes no new check or try, so an Approve ask
|
||||
may come later; a check already running finishes, and the oldest waiting check that has waited 30 minutes
|
||||
runs next, one at a time (ADR 0483, which changes ADR 0406 §3).
|
||||
- **Asks on streams, taken in order.** A build or a check step is an ask, a message on one of the work-queue streams of
|
||||
the artifact's language, which the build modules of that language claim as the accepted kinds of their node seat: `delivery` for the
|
||||
build of a trunk fingerprint, `check` for the build of a candidate's fingerprint, a new head's check and the checker's steps (a try is a
|
||||
check ask), and `recheck` for the checker's re-judges after a move of the trunk (ADR 0525 §1, §2, ADR 0536 §4). A build module pulls
|
||||
`delivery`, then `check`, then `recheck`, while it has a free slot of its node's build capacity, and no part chooses the node (ADR 0525 §4).
|
||||
Each check stream has one subject per pull request and keeps only the newest request: a newer request replaces one still waiting, by the
|
||||
stream's rule; a request left behind for a head and trunk commit already judged or running is acknowledged and dropped when taken, by
|
||||
reading the pull request's key. Nothing pauses the worker or deletes and republishes a request (issue 558). The controller's three work
|
||||
queues, `mesh-controller.queue` and its verbs are deleted; each stream's backlog and the age of its oldest ask are read from its
|
||||
consumers, shown in the delivery view and watched by the watchman (ADR 0525 §9).
|
||||
- **Nothing holds a check while a delivery builds.** The order of the streams is the only priority deliveries have: the controller's
|
||||
key saying that a delivery builds, and the let-through of a check that waited 30 minutes, are gone, so during a large delivery an
|
||||
Approve ask may come later (ADR 0525 §7, which supersedes ADR 0483).
|
||||
- **A running check stops itself.** It watches its pull request's key and stops when the head or the trunk
|
||||
commit in it moves, without asking the forge (issue 726). An ended check delivers no verdict, so none of its
|
||||
builds is reused.
|
||||
- **A failed builder loses nothing.** A job a builder took and never acknowledged goes to another builder
|
||||
after its wait.
|
||||
- **A failed build node loses nothing.** An ask a build module took and never acknowledged goes to another node by the
|
||||
stream's own acknowledgement wait, with no timer of a part; a step's end is written on the build module's step key named
|
||||
by the ask's id (ADR 0525 §6).
|
||||
- **A change is built once** (ADR 0350). A check that passes also builds what its merge would move into the
|
||||
artifact store's scratch namespace under a reuse key, and the plan of the merge commit copies the build of the
|
||||
same key, named by the check's verdict, instead of building.
|
||||
@@ -360,9 +382,16 @@ From its delivery, a check is this:
|
||||
on its head; it passes only when the head merged onto its trunk's head passed its tests and the composition
|
||||
check. For a member of a delivery group it is the group's one verdict, the same on every member's head: every
|
||||
member's tests, read beside the others' heads, and one composition of all members' changes. It replaces
|
||||
`mesh/merge-gate` and `mesh/repo-check`; the forge requires it and the gate reads it from R4f, the delivery in which
|
||||
the checker's line is the only one (ADR 0447 §5); the checker writes it unrequired from R4c (ADR 0453 clause 4). ADR 0484 builds the dark half, the verdict, its line and its status, in R4c-5, as one delivery group of four repositories with the shared library a member, one composed check, one Approve and one delivery plan; for this milestone only, before R4e, a group may hold a mesh-sdk member (ADR 0494 §1, §2, which replace ADR 0484 §1 and change ADR 0482 §7): the checker records the status as an act it owes in its own bucket and the forge bridge, the only part that calls the forge, performs it; the checker judges the head merged onto the trunk, for pull requests into the trunk named main only and for hq none, and until the isolated run step exists (issue 1309) it runs a change's code without the sandbox, as today's checks do (ADR 0484 §2, §3, §5, §8, §11). `mesh/delivery-group` stays the delivery's view, not a required check. Wherever
|
||||
`mesh/merge-gate` and `mesh/repo-check`; the gate and the merge tool read it from R4f, the delivery in which
|
||||
the checker's line is the only one, and the forge requires no status on any repository (ADR 0447 §5 as ADR 0534 §1 changes it); the checker writes it from R4c (ADR 0453 clause 4). ADR 0484 builds the dark half, the verdict, its line and its status, in R4c-5, as one delivery group of four repositories with the shared library a member, one composed check, one Approve and one delivery plan; for this milestone only, before R4e, a group may hold a mesh-sdk member (ADR 0494 §1, §2, which replace ADR 0484 §1 and change ADR 0482 §7): the checker records the status as an act it owes in its own bucket and the forge bridge, the only part that calls the forge, performs it; the checker judges the head merged onto the trunk, for pull requests into the trunk named main only and for hq none, and until the isolated run step exists (issue 1309) it runs a change's code without the sandbox, as today's checks do (ADR 0484 §2, §3, §5, §8, §11). `mesh/delivery-group` stays the delivery's view, not a required check. Wherever
|
||||
this design says both checks passed, they read this one status.
|
||||
- **hq is checked like any repository, by Go tests.** hq's four checks (records, the reading order's index, the development cycle and
|
||||
the words) are Go tests in one package at hq's root module, which holds no other Go code; the checker runs them through the Go build
|
||||
module at the unit level and within its bound (ADR 0527 §1, §4). The checker's exception for hq goes: an hq pull request gets a verdict
|
||||
and `mesh/check` like any other, and, with build-agent removed, no part writes `mesh/repo-check` for any repository (ADR 0527 §2 as ADR 0534 §1
|
||||
changes it: the forge requires no status on any repository, and the checks gate the Approve and the merge tool instead). hq's own pull
|
||||
request in R4g's group is judged by the script on hq's main, run as today, so it adds the Go tests beside the Python checks; once the
|
||||
delivery is done, a small fix deletes the Python checks and `merge-check.sh` (ADR 0527 §3 as ADR 0534 §7 changes it, issue 1296).
|
||||
- **What the status says.** Its line says the verdict, the tests run and kept at each level, the machines
|
||||
composed, and how long each part took: waiting in the queue, cloning, the unit level, the integration level's
|
||||
setup and its running, and the composition. A failure names the test or the machine first. The line links to
|
||||
@@ -375,8 +404,7 @@ From its delivery, a check is this:
|
||||
judged by its own bounds: the unit level by its running time (five seconds), the integration level by its
|
||||
setup (thirty seconds) and its tests' running time (sixty seconds), measured and judged apart; the integration level's bounds fail a repository from its
|
||||
first check that meets them (ADR 0453 clause 9). A
|
||||
repository's `merge-check.sh` goes in R4g (ADR 0453 clause 8); hq, which builds no module, keeps its script until issue 1296 settles how
|
||||
it is checked (ADR 0445).
|
||||
repository's `merge-check.sh` goes in R4g (ADR 0453 clause 8). hq, which builds no module, is checked the same way (next bullet).
|
||||
- **Only what changed runs.** The build module names, from the toolchain, everything each package's tests read,
|
||||
and the checker keeps a package's passing verdict at each level under the digest of those inputs. A check
|
||||
runs a package only when no passing verdict is kept under its digest; a check asked again after a failure
|
||||
@@ -399,8 +427,9 @@ From its delivery, a check is this:
|
||||
mesh-sdk version it composed with, and while the assigner and the controller run different versions the
|
||||
controller's self-check raises a condition until they agree. No judge is built from a change: a change to the
|
||||
composition is proved by its own tests, a field only a newer composition reads goes out as optional first, and
|
||||
the running composition refuses a manifest that uses a field it does not read, naming the field. What it fails on, and its warning past twelve rebuilt modules, are today's merge
|
||||
gate's (ADR 0446, ADR 0237 §2).
|
||||
the running composition refuses a manifest that uses a field it does not read, naming the field. What it fails on, and its warning past twelve rebuilt modules, are the composition check's, as they were the controller's composition check's (ADR 0446, ADR 0237 §2).
|
||||
The assigner writes the verdict under the checker's hash of the heads it judged, in its own bucket, and the checker reads it into the
|
||||
composition of its verdict, which says "not run" until the assigner's module exists (ADR 0528 §5).
|
||||
|
||||
## The Approve and the merge
|
||||
|
||||
@@ -540,24 +569,29 @@ delivery time.
|
||||
*ADR 0267, ADR 0350, ADR 0405.*
|
||||
|
||||
- **One build module per language** (Go; Node and TypeScript) installs its toolchain as a declared package on
|
||||
every node holding the build seat, pinned to one version that is the same on every builder. The toolchain
|
||||
image and its module retire.
|
||||
- **Every builder runs the same toolchain, and says so.** Each builder names its toolchain versions with every
|
||||
build outcome, and the controller's self-check raises a condition while two builders differ.
|
||||
- **Builds and tests run directly**, as the build seat's own account on its node, in no container. The
|
||||
every node holding its node seat, pinned to one version that is the same on every build node. The toolchain
|
||||
image and its module are removed in R4g: mesh-tools's module whose images hold the toolchains and the module of
|
||||
the repository mesh-tools-go go, so no toolchain image and no module providing one remains (ADR 0524 §5, issue 979).
|
||||
- **Every build node runs the same toolchain, and says so.** Each build module names its toolchain version with every
|
||||
step outcome, and the controller's self-check raises a condition while two nodes differ.
|
||||
- **Builds and tests run directly**, as the build module's own account on its node, in no container. The
|
||||
build module of each module's language runs its tests by that language's convention; no repository's
|
||||
script or toolchain line decides it, hq's alone excepted until issue 1296 (ADR 0445).
|
||||
script or toolchain line decides it, hq's included (ADR 0527).
|
||||
- **A judge has an account of its own, until release 4 is delivered (R4f).** Until then, a judge built from a sibling's
|
||||
merge (ADR 0382 §3) is built and run as a second account of the Go build module, whose home, configuration and
|
||||
cache the build seat's account cannot write, so a check's script cannot change it. From R4f no judge is
|
||||
built from a change and the account is not made (ADR 0446 §5).
|
||||
- **A module may ship its own Dockerfile.** A build module of its own builds the module's `image` artifact from it, rootless, with no root and no container runtime socket on the build node; a pull request's image is built and thrown away, and only a merged trunk commit's image reaches the registry, by digest. Until that module has taken over, build-agent builds images (ADR 0487).
|
||||
- **A module may ship its own Dockerfile.** A build module of its own builds the module's `image` artifact from it, rootless, with no root and no container runtime socket on the build node; a pull request's image is built and thrown away, and only a merged trunk commit's image reaches the registry, by digest. From R4g that module is the only part that builds an image, and build-agent builds none (ADR 0487 §7, ADR 0524 §5); the other builds
|
||||
are Go and Node programs, which their build modules build.
|
||||
- **Every dependency comes by the version its lock file names**, checked against its checksum: the mesh's own
|
||||
code from the mesh's forge, its own packages from its package registry, any other from its public source.
|
||||
- **A builder fetches from the forge over SSH with a key of its own**, as the build seat's account, under a forge
|
||||
account for that builder that reads the mesh's repositories and pushes nothing.
|
||||
- **A build module fetches from the forge over SSH with a key of its own**, as its own account, under a forge
|
||||
account for that module that reads the mesh's repositories and pushes nothing.
|
||||
- **A build's reuse key** holds the installed toolchain's version and the build module's recipe version in place
|
||||
of the toolchain image's digest, so a toolchain update rebuilds only the modules of that language.
|
||||
of the toolchain image's digest, so a toolchain update rebuilds only the modules of that language. From R4g no
|
||||
module's fingerprint takes the builder as an input: a change of the builder rebuilds nothing, and a change of how an
|
||||
artifact is built is a change of the build module's recipe version; the switch itself rebuilds every module once and
|
||||
moves only the modules whose artifact digest changed (ADR 0530).
|
||||
|
||||
## One chain, on the bus alone
|
||||
|
||||
@@ -642,6 +676,31 @@ and its files). A person has `plans go <plan> --why`.
|
||||
- Any other plan waits for `deliver` while the seat has a holder on record; with none, the controller starts it.
|
||||
- A replaced controller or mesh-delivery carries on from the bus: the keys, the queues and the durable
|
||||
consumers hold every step, so nothing is lost or repeated (ADR 0406 §6).
|
||||
- The controller takes every module, the builder's own and the build modules among them, from the builder's done key whose artifacts
|
||||
all name a reference; a module whose key is not yet done waits, named in the plan, and never fails it (ADR 0524 §7).
|
||||
|
||||
### Delivering the controller, the builder or a build module without the builder
|
||||
|
||||
With build-agent removed, the mesh has no builder outside the cycle, so the way to deliver a part that cannot be delivered the usual
|
||||
way is written down before the switch (ADR 0526, which ADR 0522 §6 asked of R4g's record). In this order, each step recorded:
|
||||
|
||||
1. **A revert, with no build.** A broken builder or build module is undone by reverting its change: the earlier source's fingerprint has a done
|
||||
key naming references, so the delivery of the revert builds nothing. It needs the earlier artifact in the artifact store, which keeps a
|
||||
recorded build's artifact while it is among its module's five newest or a definition names it (ADR 0526 §1).
|
||||
2. **A build by a session's call.** A session calls the build module's fetch and build tools on one named build node (for Go, a fetch and then
|
||||
a build with publishing on); the build module pushes the artifact to the artifact store and answers its reference, as it does for the
|
||||
builder (ADR 0526 §2, ADR 0525 §8). `mesh-pack`, in mesh-sdk, builds and packs a Go bundle with the Go build module's recipe, so a hand
|
||||
build has the build module's form; with the same Go version it has the build module's digest (ADR 0526 §5).
|
||||
3. **A registration by reference, at the controller's terminal only.** The operator names the module, its repository, its path and each
|
||||
artifact's reference at the controller's terminal, and the controller registers it with no builder key, recording the act with its why;
|
||||
no verb reaches it (ADR 0526 §3).
|
||||
4. **A controller that cannot take its own fix** is put on the control-node by hand: its hand build is registered as in step 3 and the
|
||||
control-node is pushed with its plan read first. In the worst case the operator sends the control-node a declaration whose source and
|
||||
digest are the hand build's, recorded as a declaration sent by hand, and registers it as in step 3 so that the next push keeps it
|
||||
(ADR 0526 §4; the bootstrap of issue 1886).
|
||||
5. **Copies kept outside the store.** Before a switch that moves the controller, the builder or a build module, the artifacts of the
|
||||
running versions are kept outside the artifact store on two nodes, each by digest with its module, artifact, commit, Go version and the
|
||||
steps that put it back, because the store's nightly collection removes a blob the controller did not record (ADR 0526 §6).
|
||||
|
||||
## Acceptance tests
|
||||
|
||||
@@ -720,11 +779,17 @@ development, the check's share about a minute):
|
||||
group's merges (ADR 0418 §1, §5, §6).
|
||||
- The controller's tables of builds, build requests, rollouts and their tiers, batches and merges, and
|
||||
assignments; the record mirrors, probe D17's audit and healer H6 for those kinds (ADR 0408).
|
||||
- At release 4's deliveries (R4f for the merge gate, the judge and the two statuses; R4g for the scripts): `merge-check.sh` and its `# mesh-check-toolchain` line in every repository a module is
|
||||
built from, hq's alone kept until issue 1296 (ADR 0445 §6); the controller's merge gate, composing every
|
||||
- At release 4's deliveries (R4f for the composition check, the judge and the two statuses; R4g for the scripts): `merge-check.sh` and its `# mesh-check-toolchain` line in every repository a module is
|
||||
built from, hq's included once the delivery is done (ADR 0445 §6, ADR 0527 §3); the controller's composition check, composing every
|
||||
machine from clones and throwaway stores (ADR 0446 §1, §6); the judge built from a change and the Go build
|
||||
module's second account for it (ADR 0446 §5); and the two statuses `mesh/merge-gate` and `mesh/repo-check`
|
||||
(ADR 0447 §1).
|
||||
(ADR 0447 §1, ADR 0527 §2).
|
||||
- At R4g's delivery (ADR 0524, ADR 0525): build-agent, its program, its module and its Dockerfile code; mesh-tools's toolchain
|
||||
image module and the module of the repository mesh-tools-go; the seat `node-build-agent`, retired for the build modules' seats;
|
||||
the controller's build queue, its three work queues and their verbs, the key saying that a delivery builds and the let-through of a
|
||||
check after 30 minutes (ADR 0483, superseded); the fallback to build-agent for a module whose builder does not say it writes
|
||||
references; the builder as an input of every module's fingerprint (ADR 0530); and the controller's per-node capacity choice, which the
|
||||
build modules' own pull replaces.
|
||||
|
||||
## Milestones
|
||||
|
||||
@@ -754,7 +819,7 @@ release reports the controller's code lines against research 063's baseline (acc
|
||||
| R2c | The controller says when it last heard each machine from presence alone, and no longer hears the alive message | 14 | mesh-controller | 1183 (every reader of last heard takes presence; the controller's alive side deleted) |
|
||||
| R2d | The node-engine sends no alive message | 14 | the node-engine (`mesh-host`) | 1158 (the node-engine's alive side deleted) |
|
||||
| R3a | The gate, the bridge, the indexer and the teller can read one layout of the source revision, candidates, modules, merges, asks, answers, owed acts, forge messages and conditions, and one function computes the source fingerprint: mesh-sdk ships them before their users (ADR 0437) | — (15 needs it) | mesh-sdk | 1200 (source revision, candidates, modules, forge messages), 1201 (merges, asks, owed acts, answers), 1202 (a condition on its one subject), 1203 (the source fingerprint, proven against three fingerprints the live builder recorded); ahead of it in mesh-controller, so those could be read: 1204 and 1208 (`builds` says each build's source fingerprint) |
|
||||
| R3b | The controller finds the delivery seat's holder by its claim and records each build's fingerprint inputs; the router takes conditions on their one subject; the operator sees no change (ADR 0437) | — | mesh-controller, mesh-catalog (messenger) | 1210 (the controller's own path and the merge gate's rule by the seat's claim), 1211 (a merge's announcement heard from the seat's holder), 1212 (the pull requests' keys read from the holder's bucket), 1213 (a merge's warrant read under the asker the announcement names: the teller, or mesh-delivery until R3d), 1214 (the builder computes the source fingerprint with mesh-sdk's function), 1215 (a build record carries its fingerprint's inputs), 1216 (a module may say a condition only under its own name, proven on the controller's grant); in the messenger: 1217 (the router takes a condition on its one subject), 1218 (the same over the bus); one delivery group. Its prepare step found that the controller hears no `transition` or `group` event, and that the router cannot refuse a condition under another module's name, the grant does: both go into R3c's insight on ADR 0437 with 1209 |
|
||||
| R3b | The controller finds the delivery seat's holder by its claim and records each build's fingerprint inputs; the router takes conditions on their one subject; the operator sees no change (ADR 0437) | — | mesh-controller, mesh-catalog (messenger) | 1210 (the controller's own path and the composition check's rule by the seat's claim), 1211 (a merge's announcement heard from the seat's holder), 1212 (the pull requests' keys read from the holder's bucket), 1213 (a merge's warrant read under the asker the announcement names: the teller, or mesh-delivery until R3d), 1214 (the builder computes the source fingerprint with mesh-sdk's function), 1215 (a build record carries its fingerprint's inputs), 1216 (a module may say a condition only under its own name, proven on the controller's grant); in the messenger: 1217 (the router takes a condition on its one subject), 1218 (the same over the bus); one delivery group. Its prepare step found that the controller hears no `transition` or `group` event, and that the router cannot refuse a condition under another module's name, the grant does: both go into R3c's insight on ADR 0437 with 1209 |
|
||||
| R3c | The forge bridge, the indexer and the teller run beside today's mesh-delivery and write their keys; the view shows modules against the controller's source fingerprints; nothing is merged by them yet (ADR 0437) | 15 in part | mesh-delivery (forge bridge, indexer, teller, view), mesh-catalog (the forge module) | 1235 (the forge bridge writes pull messages on FORGE), 1236 (its merged message, with the trees of the merge commit), 1237 (its messages for the pull requests open at its start and each reconnect), 1238 (the indexer writes the candidate groups), 1239 (the indexer writes the modules' fingerprints at a trunk merge, and 1209: none for an image that compiles a Go program from a context), 1240 (the teller runs and carries nothing yet), 1241 (the view shows the candidate groups and each module's fingerprint against the controller's); in the forge module: 1234 (a merge commit's trees in one call, and the head branch in the open pull requests); one delivery group, mesh-catalog first. Its prepare step found three orders: the forge had no tool for a commit's trees, so the forge module joins the milestone as a member of its group; and mesh-sdk shipped two layouts before it, the controller's build record (1223) and a build's directory and a pull request's module directories (1233). It also found that the bridge's FORGE is a stream the others hear by `consumes`, not a bucket they `read`; that the broker test of the three credentials goes into R3d with the gate's (1243); and that the parts hand-write their bus connection until mesh-sdk gives one (1242): facts for ADR 0437's next insight, with R3d's |
|
||||
| R3d | The gate's keys can carry what the forge needs and the controller answers a what-if as data, so the gate has what it reads before it runs; a failed delivery plan asks the operator Retry on the phone, and a seat whose holder is assigned nowhere passes to its only claimant; the operator sees only the Retry notice (ADR 0450, ADR 0449) | — | mesh-sdk, mesh-controller | in mesh-sdk: 1321 (a merge key carries the Approve's heads), 1322 (an owed act carries a status's target, a commit without a pull request, two comments), 1323 (a name a key cannot hold is escaped); in mesh-controller: 1324 (what-if as data), 1325 (the build seat reads the gate's pull keys), 1326 (the gate's buckets writable by its runner only), 1243 (the three parts' credentials); and, delivered first as feature no-terminal (ADR 0449): 1317 (the seat passes to its only claimant), 1314 (a failed delivery plan raises a condition), 1315 (Retry offered and performed), 1316 (a refused answer is told). Its prepare step found four things the gate reads that ship before it, which cut the former R3d in two (ADR 0450), and two steps on the path that needed the operator's terminal (ADR 0449) |
|
||||
| R3e | A group reaches the mesh as one source revision written by the gate; the merge's Approve is asked by the teller and answered on the phone until issue 953; the view shows the open asks; today's module mesh-delivery is carried over into the gate when it first holds the seat and ends; no step needs the operator's terminal (ADR 0437, ADR 0450) | 15 | mesh-delivery (gate's parts: forge bridge, teller, indexer, view), mesh-catalog (the gate, mesh-delivery removed, mesh-issues), mesh-tools (the tool runner's waits) | opened when it is prepared, from R3d's survey; the controller's deletions it makes possible go to release 4 (1318) |
|
||||
@@ -763,11 +828,13 @@ release reports the controller's code lines against research 063's baseline (acc
|
||||
| R4c | The new parts run dark beside today's: the indexer writes the manifests by digest and the fingerprints in the new form beside today's (its own, first delivery), then the build modules (the Go build module, the Node and TypeScript build module: toolchain, sandbox, fetch, digest, test, build), the builder, the checker writing `mesh/check` as a status the forge does not require, and the assigner's composition check writing into the checker's report; the build seat's accounts are decided by a record before the build modules' pull requests are opened (ADR 0453 clauses 4, 6, 7, 8, 11, 12, 16; ADR 0465 §4); the builder is ADR 0478's. It is delivered in ADR 0465 §5's order: mesh-sdk's layouts; the forge bridge and the indexer with the controller's facts in format 2; the build modules; the builder; the checker; the bus user list's composer moved into mesh-sdk; the assigner's composition check; the fetch and run steps. The build modules each run as an account of their own with a forge key known read-only on one forge user (ADR 0466); each builds with the node's installed toolchain at the agreed version and does not refuse on a node with another (ADR 0477, issue 1572), answers a long step with its id and finishes it on a key, is called on a named machine, and runs on the four nodes that run the build agent today (ADR 0469) | 22, 23 (measured on the dark run, proved in R4f) | mesh-delivery (indexer, builder, checker, assigner's composition check), mesh-catalog (build modules, ssh-client, gitea), mesh-controller (the build agent's whole-check tests, the script's steps as tests), mesh-sdk | 971 (its record first), 1297 (the assigner's composition check), 1309 (the fetch and run steps), 1287, 1288, 1289, 1290, 1291, 1292 (the checker; the build modules run the tests), 1251; the steps of 1293 and 1294 that are no test become tests in R4c to R4e; the rest opened when it is prepared |
|
||||
| R4d | Four modules stop being built from a Dockerfile: route-proxy, nats, n8n and mail's autoconfiguration module (automx, replaced by a Go program), each rework its own release step, nats in a delivery of its own; free between R4a and R4e and never in the same delivery as another release (ADR 0452, ADR 0453 clauses 13, 14, 16) | — | mesh-catalog, mesh-controller (route-proxy), the node-engine (`mesh-host`: n8n's supplementary group) | the four image reworks, opened when it is prepared |
|
||||
| R4e | The controller asks no build: a build is keyed by its fingerprint, the new form becomes the only one as one group, and a merge builds nothing its check built; builders fetch over SSH, so an SDK change ships with its users; the vendored copies leave mesh-controller and mesh-sdk; delivered only once the builder's artifact digests equal build-agent's (ADR 0453 clauses 2, 4, 5, 6) | 16, 2, 4 in part | mesh-controller (builds are no longer asked), mesh-delivery (builder), mesh-sdk | 979 (the installed toolchains; closed with the toolchain images in R4g), 1304, the controller asking no build (opened when it is prepared) |
|
||||
| R4f | A pull request carries one required status that says what it proved and how long each part took; the controller's merge gate and its replays are deleted with the two old statuses; the controller's deletions R3e makes possible go; each level's times are measured, and a bound fails a repository from the first check that meets it (ADR 0447, ADR 0450 §8, ADR 0453 clauses 4, 9, 10) | 21, 22, 23, 24 (the status; the scripts' absence is proved in R4g) | mesh-delivery (checker, assigner), mesh-controller (merge gate, replays, `delivery-plan`, `forget-checks`, the asker `mesh-delivery` deleted), mesh-catalog (gitea) | 1299 (the merge gate deleted; closes issue 709, the replays' socket), 1300, 1301, 1302 (one status), 1305 (ADR 0482 §8), 1318 |
|
||||
| R4g | The clean-up: the scripts of the repositories a module is built from, build-agent with its socket and Dockerfile code, the toolchain images and the word merge gate go; the builder's own module is delivered from the builder's keys (ADR 0453 clauses 5, 8, 10) | 4 (from R4g mesh-build-image, rootless, is the only part that builds an image: ADR 0487 §7), 24 (no `merge-check.sh` in a repository a module is built from) | mesh-controller (build-agent removed), mesh-catalog, mesh-delivery, mesh-tools (scripts deleted), hq | 1295 (`merge-check.sh` goes; the tests of 1293 and 1294 are R4c to R4e's), 979 (closed with the toolchain images), 1303 (the word merge gate struck); 1296 (how hq is checked) is the operator's and undated by this release |
|
||||
| R5 | The assigner writes what each node should run by its six rules, compared with what the controller sends, and the view shows every difference; the operator answers the asks that need no proof of who answered in the view (ADR 0437 §10) | 17 | mesh-delivery (assigner), mesh-controller (grants keys) | opened when it is prepared |
|
||||
| R4f | A pull request carries one required status that says what it proved and how long each part took; the controller's composition check and its replays are deleted with the two old statuses; the controller's deletions R3e makes possible go; each level's times are measured, and a bound fails a repository from the first check that meets it (ADR 0447, ADR 0450 §8, ADR 0453 clauses 4, 9, 10) | 21, 22, 23, 24 (the status; the scripts' absence is proved in R4g) | mesh-delivery (checker, assigner), mesh-controller (composition check, replays, `delivery-plan`, `forget-checks`, the asker `mesh-delivery` deleted), mesh-catalog (gitea) | 1299 (the composition check deleted; closes issue 709, the replays' socket), 1300, 1301, 1302 (one status), 1305 (ADR 0482 §8), 1318 |
|
||||
| R4g | The last milestone of release 4 and release 5 as one, with one switch-over and no rehearsal (ADR 0524 §1): builds and checks are asks on streams that each build node pulls when it has a free slot, deliveries first, with nothing holding checks and no controller queue (ADR 0525); the assigner is made with its composition check, writes each node's wants compared with what the controller sends and every difference, and the view answers the asks that need no proof of who answered (ADR 0528, ADR 0437 §10); no module's fingerprint takes the builder (ADR 0530); the scripts of the repositories a module is built from, build-agent with its socket and Dockerfile code, the toolchain images and the controller's build code go, and the builder delivers its own module, the build modules and the controller from its keys (ADR 0453 clauses 5, 8, 10; ADR 0524 §5, §7); the ways to deliver without the builder are recorded (ADR 0526); hq's four checks are Go tests (ADR 0527); each repository's script goes (ADR 0529); the word for the composition check's predecessor is struck. Its pull requests are opened only after five readings, the revert path read, not shown on a test merge (ADR 0524 §3) | 4 (from R4g mesh-build-image, rootless, is the only part that builds an image: ADR 0487 §7), 17, 24 (no `merge-check.sh` in a repository a module is built from) | mesh-sdk, mesh-controller (build-agent removed), mesh-delivery, mesh-catalog, mesh-tools, mesh-tools-go, the node-engine (`mesh-host`), mesh-media-catalog, okioki, hq | 1887 (the flow), 1297 (the assigner), 1295 (`merge-check.sh` goes), 979 (closed with the toolchain images), 1296 and 1303 (hq's check, the old word struck), 1299, 1305, 1294, 1318 (the controller's deletions) |
|
||||
| R6 | Each node applies from its wants, a new version reaches the module's canary first and the rest once it is healthy there, and the plans are deleted from the code | 18, 19 | the node-engine (`mesh-host`: the node agent), mesh-controller (plans deleted), mesh-delivery (the gate's delivery states go) | opened when it is prepared |
|
||||
|
||||
Release 5 (the assigner's wants, compared only) is not a milestone of its own: ADR 0524 §1 folds it into R4g, whose row above holds
|
||||
it; release 6, the node agent applying the wants, stays the next milestone.
|
||||
|
||||
The issues are linked `depends-on` in this order; 976 depends on 975, and 979 also on 971. R3's prepare step on 2026-10-19 found ADR 0429 §20 contradicting itself and four release orders; ADR 0437 (accepted, the operator's Approve) cuts release 3 into R3a to R3d, each one release, and moves answering asks in the view to R5. R2c's prepare step on 2026-10-19 found one more order: the alive message is the only source of when the controller last heard a machine, which `nodes`, `node` and `status` read, so the controller takes that from presence (1183, R2c) and is delivered before the node-engine stops sending (1158, R2d); 1158 depends on 1183. In release 1-2, 1132 and 1136 depend on 1131, 1134 and 1135 on 1136, 1134 also on 1132 and 1128, and 1137 on 1134. Release 1-2 was confirmed as one milestone; its prepare step on 2026-10-18 found the release orders hidden in it, and the operator chose at 18:52 to cut it by them, so it is the milestones R1a to R2d above, each one release (the operator, 2026-10-19: a milestone is one release): an SDK change still ships before its users until R4e (ADR 0429 §10, ADR 0453 clause 2); a new key on the wire goes only to a node that says it reads it; a new repository's first module registers at its merge's Approve (ADR 0432, issue 1146); the bus gives the watchman connect events only through a system account (ADR 0431); and release 2's other gaps are settled by ADR 0433 and ADR 0434. A node that is down has no effect on a delivery and plays what it missed when it returns (ADR 0435, the operator, 2026-10-18; issues 1160, 1161). Milestone 3's first live group (milestone 4's, on 2026-10-17) held one Approve but merged in two passes and went out in two delivery plans: a member's tests read the other member's repository, so the first merge made the second's verdict out of date, and a closed group never asked its composed verdict again (issues 1049, 1082). The operator put a fix of milestone 3 after milestone 4 and before milestone 5 on 2026-10-18; acceptance test 1 is proved again by it. The operator put milestone 5b after milestone 5 and before milestone 6 on 2026-10-18, as an issue rather than a research effort; where the planning goes is settled by a decision record before it is built (issue 1117), and milestone 6's build changes are read against it. Milestone 2b was
|
||||
put before milestone 3 by the operator on 2026-10-17. Milestone 3 builds ADR 0418, ADR 0419 and ADR 0421
|
||||
(issues 977, 978 and 1022), and splits mesh-controller's unit tests from its integration tests (issue 895, the
|
||||
|
||||
@@ -448,7 +448,7 @@ open:
|
||||
composes the field — the endpoint as the port the machine published it on, the defaults written out — only
|
||||
for a machine whose newest statement says so, and strips it for an older engine, which parses strictly
|
||||
and would refuse the whole declaration. So the engine rolls out first, then the controller, and the
|
||||
catalogue declares only once that controller runs: until then the merge gate refuses a declaration, which
|
||||
catalogue declares only once that controller runs: until then the composition check refuses a declaration, which
|
||||
is the protection working.
|
||||
- **The engine's looks.** http and tcp from the machine to the published port; a redirect is an answer and
|
||||
is not followed; a certificate is not judged. A unit's readiness is read from the show liveness already
|
||||
|
||||
@@ -151,15 +151,15 @@ glossary says it is the older word. **Proof:** `words.py` passes.
|
||||
stage.* A file resource takes `"template": "files/<name>"` in place of `content`; the module's build packs
|
||||
`files/` into an archive artifact of its own (the existing `archive` packing); registration and `Resolve`
|
||||
read each template's text back from the packed tree into `content`, so composition and the node-engine
|
||||
are unchanged; `module check` and the merge gate read templates from the repository tree beside the
|
||||
are unchanged; `module check` and the composition check read templates from the repository tree beside the
|
||||
manifest; a file naming both `template` and `content` is refused. **Proof:** composition tests: a module
|
||||
with its text in `files/` composes byte-identically to the same module with it inline; the merge gate
|
||||
with its text in `files/` composes byte-identically to the same module with it inline; the composition check
|
||||
judges a change to a template alone.
|
||||
|
||||
**T2 (issue 655) — The twelve modules with copies.** *mesh-catalog. A day.* adwaita, clipmenu, dunst, feh,
|
||||
gnome-keyring, i3, i3status-rust, lemurs, picom, rofi, screen-lock and xdg move to their templates; the
|
||||
34 inline copies and the `sameAsSource` tests go. **Proof:** each module's composed declaration on its
|
||||
nodes is unchanged (the merge gate's plan says no file changes).
|
||||
nodes is unchanged (the composition check's plan says no file changes).
|
||||
|
||||
**T3 (issue 656) — The desktop modules.** *mesh-catalog. A day.* Every remaining module assigned only to
|
||||
workstations. **Proof:** as T2.
|
||||
|
||||
@@ -54,8 +54,10 @@ in running prose; a quotation keeps its words.
|
||||
|
||||
## Ground rules
|
||||
|
||||
- **Markdown only** — but for the checks in `00-META/checks/` and `merge-check.sh` at the root, which
|
||||
runs them on every pull request as its `mesh/repo-check` (ADR 0238), and
|
||||
- **Markdown only** — but for the four checks, Go tests in the package `00-META/checks/` with `go.mod` at the
|
||||
root and no other Go code (ADR 0527), which the checker runs on every pull request as `mesh/check`;
|
||||
until they replace them, the Python checks beside them and `merge-check.sh` at the root keep running as
|
||||
`mesh/repo-check` (ADR 0238); and
|
||||
`00-META/process/playbooks.html`, a drawn overview of the playbooks kept for a later docs site (the operator,
|
||||
2026-10-19); a change to a playbook's steps updates it in the same pull request. Nothing checks that
|
||||
yet: issue 1222 asks for the check.
|
||||
|
||||
Reference in New Issue
Block a user