From 8da33e43d41f2f8c8053a2ee84665dd757c6ce94 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 16:37:54 +0100 Subject: [PATCH] Knowledge base: progress says how far a design, milestone, issue or record has come (issue 1947) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A lead's five-minute report and 'is design 47 nearing its end' were joined by hand from the design file, the tracker and the deliveries. One verb now works the state out (ADR 0424 §1), minimal for every five minutes, detailed for the report's tree. Milestones are read from the design's table until the tracker's milestone records (issue 1781) are delivered. --- modules/mesh-knowledge/README.md | 1 + .../cmd/mesh-knowledge/design.go | 281 ++++ .../mesh-knowledge/cmd/mesh-knowledge/kb.go | 7 + .../mesh-knowledge/cmd/mesh-knowledge/main.go | 36 +- .../cmd/mesh-knowledge/manifest_test.go | 11 +- .../cmd/mesh-knowledge/progress.go | 832 ++++++++++++ .../cmd/mesh-knowledge/progress_test.go | 348 +++++ .../testdata/progress/README.md | 7 + .../testdata/progress/deliveries.json | 5 + .../testdata/progress/groups.json | 3 + .../47-delivery-from-commit-to-delivered.md | 1133 +++++++++++++++++ .../01-to-be/53-the-issue-tracker.md | 319 +++++ .../57-building-config-and-provisioning.md | 363 ++++++ .../testdata/progress/hq/COMMIT | 1 + .../testdata/progress/show-car-hunter-2.json | 1 + .../testdata/progress/tracker-rows.json | 13 + modules/mesh-knowledge/module.json | 13 +- 17 files changed, 3366 insertions(+), 8 deletions(-) create mode 100644 modules/mesh-knowledge/cmd/mesh-knowledge/design.go create mode 100644 modules/mesh-knowledge/cmd/mesh-knowledge/progress.go create mode 100644 modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go create mode 100644 modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/README.md create mode 100644 modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/deliveries.json create mode 100644 modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/groups.json create mode 100644 modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/hq/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md create mode 100644 modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/hq/03-DESIGN/01-to-be/53-the-issue-tracker.md create mode 100644 modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/hq/03-DESIGN/01-to-be/57-building-config-and-provisioning.md create mode 100644 modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/hq/COMMIT create mode 100644 modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/show-car-hunter-2.json create mode 100644 modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/tracker-rows.json diff --git a/modules/mesh-knowledge/README.md b/modules/mesh-knowledge/README.md index 30e3cec5..943c8c89 100644 --- a/modules/mesh-knowledge/README.md +++ b/modules/mesh-knowledge/README.md @@ -27,6 +27,7 @@ operator, it is accepted only on the operator's Approve, given on a channel that | `import` | `dry-run`, `commit` | hq's decision records and research efforts at the commit the records module stands at, each whole at its own number | | `return` | `number`, `reason` | drafted clauses sent back to be drafted again; refused for a record whose clauses another record changed or links by number | | `withdraw` | `number`, `kind`, `why` | a reserved or proposed record withdrawn, as a new version naming why; its number stays spent. A pending review is cleared and a waiting operator's ask is ended. Refused for a record already accepted, superseded or withdrawn | +| `progress` | `of` (`mesh`, `design `, `milestone /`, `issue `, `decision ` or `ADR `, `research `), `detail` (`minimal` or `detailed`), `bound` | how far it has come (hq issue 1947): each child done ✓, moving ▶, waiting ⏸ or stuck ✗ with when it last moved, worked out from the issue tracker's rows and mesh-delivery's deliveries and groups, never written (ADR 0424 §1). `minimal` is one line per milestone, for a report every five minutes (two or three bus calls); `detailed` the report's tree with each issue's pull requests, the groups holding them and what it depends on. Milestones are read from the design's `## Milestones` table through the records module, cached by hq's commit, until the issue tracker holds milestone records (issue 1781); what the table does not say for certain is said in `warnings` | `ask`, `get`, `holds` and `history` stay within `bound` bytes (4096 unless asked for, at most 65536) and say what they cut and how to read the rest. The module's own tool `knowledge_health` answers its health (below). diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/design.go b/modules/mesh-knowledge/cmd/mesh-knowledge/design.go new file mode 100644 index 00000000..adffa13b --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/design.go @@ -0,0 +1,281 @@ +package main + +// A design as the progress verb reads it (novox/hq issue 1947): its title, its state as its frontmatter says it, the +// decisions it names, and its milestones from its `## Milestones` table. Designs are still documents in hq, read +// through the records module, until ADR 0423 makes them records of the knowledge base (issue 428); then this reads +// those records instead. +// +// Milestones are read from the design's table until the issue tracker holds milestone records (ADR 0493 §1, §4, +// built under issue 1781); when 1781 is delivered, the table reader is replaced by a read of those records and this +// parse is deleted (the lead of design 47, 2026-10-26). The Issues cell is prose, so what the parse cannot read +// for certain it says as a warning, never hides: an issue in two milestones (against ADR 0493 §2), a range written +// in words, a row naming no issue. + +import ( + "fmt" + "regexp" + "sort" + "strconv" + "strings" +) + +// DesignsFolder is where the designs being built are, in hq. +const DesignsFolder = "03-DESIGN/01-to-be" + +// Design is one design as read. +type Design struct { + Number int + Path string + Title string + Status string + Decisions []string + Milestones []Milestone + Warnings []string +} + +// Milestone is one row of a design's milestone table. +type Milestone struct { + ID string + Outcome string + Issues []int +} + +var ( + designFile = regexp.MustCompile(`^([0-9]{2,3})-[a-z0-9-]+\.md$`) + titleLine = regexp.MustCompile(`^#\s+[0-9]+\s+[—-]\s+(.+)$`) + decisionItem = regexp.MustCompile(`ADR ([0-9]{4})`) + // notAnIssue is a number the Issues cell names that is not an issue: a record, a clause, a test, a release, a pull + // request, a document. + notAnIssue = regexp.MustCompile(`(?i)(ADR|research|acceptance tests?|tests?|§|clauses?|release|design|to-be|as-is|playbook|version)\s*[0-9][0-9, and]*|[a-z-]*#[0-9]+|[0-9]{4}-[0-9]{2}-[0-9]{2}|[0-9]{1,2}:[0-9]{2}`) + rangeWords = regexp.MustCompile(`\b([0-9]{3,5}) to ([0-9]{3,5})\b`) + butOne = regexp.MustCompile(`\bbut ([0-9]{3,5})\b`) + issueNum = regexp.MustCompile(`\b[0-9]{3,5}\b`) +) + +// MostRange is the widest range in words the parse expands; a wider one is said and not expanded. +const MostRange = 60 + +// parseDesign reads a design document. +func parseDesign(path, content string) *Design { + d := &Design{Path: path} + if m := designFile.FindStringSubmatch(lastPart(path)); m != nil { + d.Number, _ = strconv.Atoi(m[1]) + } + lines := strings.Split(content, "\n") + body := 0 + if len(lines) > 0 && strings.TrimSpace(lines[0]) == "---" { + for i := 1; i < len(lines); i++ { + l := lines[i] + if strings.TrimSpace(l) == "---" { + body = i + 1 + break + } + if v, ok := strings.CutPrefix(l, "status:"); ok { + d.Status = strings.TrimSpace(v) + } + for _, m := range decisionItem.FindAllStringSubmatch(l, -1) { + d.Decisions = append(d.Decisions, "ADR "+m[1]) + } + } + } + inMilestones, header := false, -1 + issueCol, outcomeCol := -1, -1 + seen := map[int]string{} + for i := body; i < len(lines); i++ { + l := strings.TrimSpace(lines[i]) + if d.Title == "" { + if m := titleLine.FindStringSubmatch(l); m != nil { + d.Title = strings.TrimSpace(m[1]) + } + } + if strings.HasPrefix(l, "## ") { + inMilestones = strings.EqualFold(strings.TrimSpace(strings.TrimPrefix(l, "## ")), "Milestones") + header = -1 + continue + } + if !inMilestones || !strings.HasPrefix(l, "|") { + continue + } + cells := cellsOf(l) + if header < 0 { + header = i + issueCol, outcomeCol = -1, -1 + for c, h := range cells { + switch strings.ToLower(h) { + case "issues", "issue": + issueCol = c + case "outcome": + outcomeCol = c + } + } + continue + } + if i == header+1 && strings.Trim(l, "|-: ") == "" { + continue + } + if issueCol < 0 || issueCol >= len(cells) || len(cells) == 0 { + continue + } + m := Milestone{ID: cells[0]} + if outcomeCol >= 0 && outcomeCol < len(cells) { + m.Outcome = cells[outcomeCol] + } + var warn []string + m.Issues, warn = issuesOf(cells[issueCol]) + for _, w := range warn { + d.Warnings = append(d.Warnings, fmt.Sprintf("milestone %s: %s", m.ID, w)) + } + if len(m.Issues) == 0 { + d.Warnings = append(d.Warnings, fmt.Sprintf("milestone %s names no issue", m.ID)) + } + for _, n := range m.Issues { + if other, ok := seen[n]; ok && other != m.ID { + d.Warnings = append(d.Warnings, fmt.Sprintf("issue %d is in milestones %s and %s; an issue belongs to at most one (ADR 0493 §2)", n, other, m.ID)) + continue + } + seen[n] = m.ID + } + d.Milestones = append(d.Milestones, m) + } + return d +} + +// issuesOf is the issue numbers a table's Issues cell names, in order, and what it could not read for certain. +func issuesOf(cell string) ([]int, []string) { + var warn []string + s := notAnIssue.ReplaceAllString(cell, " ") + var nums []int + for _, m := range rangeWords.FindAllStringSubmatch(s, -1) { + lo, _ := strconv.Atoi(m[1]) + hi, _ := strconv.Atoi(m[2]) + if hi < lo || hi-lo > MostRange { + warn = append(warn, fmt.Sprintf("%q is a range in words the reader does not expand", m[0])) + continue + } + warn = append(warn, fmt.Sprintf("%q is a range in words, read as the %d issues %d to %d", m[0], hi-lo+1, lo, hi)) + for n := lo; n <= hi; n++ { + nums = append(nums, n) + } + } + but := map[int]bool{} + for _, m := range butOne.FindAllStringSubmatch(s, -1) { + n, _ := strconv.Atoi(m[1]) + but[n] = true + } + s = rangeWords.ReplaceAllString(s, " ") + s = butOne.ReplaceAllString(s, " ") + for _, m := range issueNum.FindAllString(s, -1) { + n, _ := strconv.Atoi(m) + nums = append(nums, n) + } + out, have := []int{}, map[int]bool{} + for _, n := range nums { + if !have[n] && !but[n] { + have[n] = true + out = append(out, n) + } + } + return out, warn +} + +func cellsOf(row string) []string { + row = strings.TrimSpace(row) + row = strings.TrimPrefix(row, "|") + row = strings.TrimSuffix(row, "|") + parts := strings.Split(row, "|") + for i := range parts { + parts[i] = strings.TrimSpace(parts[i]) + } + return parts +} + +func lastPart(p string) string { + if i := strings.LastIndex(p, "/"); i >= 0 { + return p[i+1:] + } + return p +} + +// milestone finds a milestone by its id, as the table writes it, without regard to case or spaces. +func (d *Design) milestone(id string) *Milestone { + want := strings.ToLower(strings.Join(strings.Fields(id), " ")) + for i := range d.Milestones { + if strings.ToLower(strings.Join(strings.Fields(d.Milestones[i].ID), " ")) == want { + return &d.Milestones[i] + } + } + return nil +} + +// issues is every issue its milestones name, each once, in order. +func (d *Design) issues() []int { + var out []int + have := map[int]bool{} + for _, m := range d.Milestones { + for _, n := range m.Issues { + if !have[n] { + have[n] = true + out = append(out, n) + } + } + } + return out +} + +// designsCache is the designs read at one commit of hq: read again only when the records module's checkout moves. +type designsCache struct { + commit string + paths []string + byPath map[string]*Design +} + +// designs answers the designs being built, read at the commit the records module stands at, from the cache while +// that commit stands (novox/hq issue 1947, 1947/AC6). +func (k *KB) designs(all bool, number int) ([]*Design, string, error) { + if k.Records == nil { + return nil, "", fmt.Errorf("the knowledge base reads no records here") + } + commit, err := k.Records.Standing() + if err != nil { + return nil, "", fmt.Errorf("the records module does not say where hq stands: %v", err) + } + k.progMu.Lock() + defer k.progMu.Unlock() + c := k.progCache + if c == nil || c.commit != commit { + c = &designsCache{commit: commit, byPath: map[string]*Design{}} + _, docs, err := k.Records.List(DesignsFolder) + if err != nil { + return nil, "", fmt.Errorf("the records module does not list %s: %v", DesignsFolder, err) + } + for _, doc := range docs { + if designFile.MatchString(doc) { + c.paths = append(c.paths, DesignsFolder+"/"+doc) + } + } + sort.Strings(c.paths) + k.progCache = c + } + var out []*Design + for _, p := range c.paths { + n, _ := strconv.Atoi(designFile.FindStringSubmatch(lastPart(p))[1]) + if !all && n != number { + continue + } + d := c.byPath[p] + if d == nil { + content, _, err := k.Records.Read(p) + if err != nil { + return nil, "", fmt.Errorf("the records module does not read %s: %v", p, err) + } + d = parseDesign(p, content) + c.byPath[p] = d + k.progReads++ + } + out = append(out, d) + } + if !all && len(out) == 0 { + return nil, "", fmt.Errorf("no design %d is in %s at hq %s", number, DesignsFolder, short(commit)) + } + return out, commit, nil +} diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/kb.go b/modules/mesh-knowledge/cmd/mesh-knowledge/kb.go index 1b7eb772..25bb30ea 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/kb.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/kb.go @@ -99,6 +99,13 @@ type KB struct { refMu sync.Mutex ref *Reference refAt time.Time + + // Seats reaches another seat's verb or a module's tool over the bus, for progress (novox/hq issue 1947). + Seats SeatAsk + // progCache is the designs read at one commit of hq; progReads counts the documents read, for its test. + progMu sync.Mutex + progCache *designsCache + progReads int } func (k *KB) now() time.Time { diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/main.go b/modules/mesh-knowledge/cmd/mesh-knowledge/main.go index 556c29ae..4c0cc099 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/main.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/main.go @@ -37,7 +37,7 @@ import ( const Seat = "knowledge-base" // Verbs are the seat's verbs, as the manifest declares and claims them (ADR 0290 rule 6). -var Verbs = []string{"ask", "get", "list", "findings", "holds", "links", "history", "reserve", "propose", "import", "return", "withdraw"} +var Verbs = []string{"ask", "get", "list", "findings", "holds", "links", "history", "reserve", "propose", "import", "return", "withdraw", "progress"} // HealthTool is the module's own tool the node-engine asks how it is. const HealthTool = "knowledge_health" @@ -83,6 +83,7 @@ func newKB(store Store) *KB { return &KB{ Store: store, Records: busRecords{}, + Seats: stdio.Ask, Reserve: busReserver, Judge: busJudgement(os.Getenv("MESH_NODE")), Asks: sdkAskBus{}, @@ -1120,6 +1121,39 @@ func Tools(k *KB, caller func(map[string]any) string) []stdio.Tool { } return k.WithdrawRecord(caller(m), key, why) }}, + {Name: Seat + ".progress", + Description: "How far something has come, in one call: of mesh (every design in progress), design (its " + + "milestones), milestone / (its issues, such as milestone 47/R6), issue (its pull requests " + + "and deliveries) or decision / research (the designs naming it and the issues linked to it). Each " + + "thing is done ✓, moving ▶, waiting ⏸ or stuck ✗, worked out from the issue tracker and mesh-delivery " + + "and never written by anyone, with when it last moved. detail minimal (the default) gives one line per " + + "milestone, cheap enough for a report every five minutes; detailed gives the report's tree: each " + + "milestone not done with its issues, their pull requests, the groups holding them and what each waits " + + "for, for about every thirty. Milestones are read from the design's milestone table until the issue " + + "tracker holds milestone records (issue 1781); what the table does not say for certain is said in " + + "warnings. Within a bound it states, 4 KiB unless asked for more, saying what it cut.", + Input: schema([]string{"of"}, map[string]any{ + "of": text("what: mesh, design 47, milestone 47/R6, issue 1945, decision 493 (or ADR 0493) or research 72"), + "detail": map[string]any{"type": "string", "enum": []string{DetailMinimal, DetailDetailed}, + "description": "minimal (the default): one line per child; detailed: the tree with issues, pull requests and groups"}, + "bound": boundArg()}), + Run: func(m map[string]any) (any, error) { + a, err := readArgs("progress", m, "of", "detail", "bound") + if err != nil { + return nil, err + } + var p ProgressAsk + if p.Of, err = a.str("of"); err != nil { + return nil, err + } + if p.Detail, err = a.str("detail"); err != nil { + return nil, err + } + if p.Bound, err = a.num("bound", false); err != nil { + return nil, err + } + return k.Progress(p) + }}, } } diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/manifest_test.go b/modules/mesh-knowledge/cmd/mesh-knowledge/manifest_test.go index 5ab02721..8043c502 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/manifest_test.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/manifest_test.go @@ -62,8 +62,10 @@ func TestItDeclaresAndHoldsTheKnowledgeBaseSeatOnceForTheMesh(t *testing.T) { if m.Module != Module || !reflect.DeepEqual(m.Seats, want) || !reflect.DeepEqual(m.Claims, want) { t.Fatalf("module %q, seats %+v, claims %+v", m.Module, m.Seats, m.Claims) } - if !reflect.DeepEqual(Verbs, []string{"ask", "get", "list", "findings", "holds", "links", "history", "reserve", "propose", "import", "return", "withdraw"}) { - t.Fatalf("the verbs are not ADR 0290 rule 6's: %v", Verbs) + // ADR 0290 rule 6's verbs and those added after it; progress says a design's state, which ADR 0424 §1 makes the + // knowledge base's to work out (novox/hq issue 1947). + if !reflect.DeepEqual(Verbs, []string{"ask", "get", "list", "findings", "holds", "links", "history", "reserve", "propose", "import", "return", "withdraw", "progress"}) { + t.Fatalf("the verbs are not ADR 0290 rule 6's and those added after it: %v", Verbs) } if !reflect.DeepEqual(m.Emits, Events) { t.Fatalf("emits %v, the code says %v", m.Emits, Events) @@ -77,11 +79,12 @@ func TestItDeclaresAndHoldsTheKnowledgeBaseSeatOnceForTheMesh(t *testing.T) { t.Fatalf("uses %v, consumes %v", m.Uses, m.Consumes) } if !reflect.DeepEqual(m.Invokes, []string{"seat:issue-tracker.reserve", "claude-code.claude_code_judge", "claude-code.claude_code_judged", "records.records_status", - "records.records_list", "records.records_read", "seat:operator-channel.notify"}) { + "records.records_list", "records.records_read", "seat:operator-channel.notify", "seat:issue-tracker.list", "seat:issue-tracker.get", + "seat:mesh-delivery.deliveries", "seat:mesh-delivery.show", "seat:mesh-delivery.groups"}) { t.Fatalf("invokes %v", m.Invokes) } src := "" - for _, f := range []string{"main.go", "hq.go"} { + for _, f := range []string{"main.go", "hq.go", "progress.go"} { src += readFile(t, f) } for _, inv := range m.Invokes { diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/progress.go b/modules/mesh-knowledge/cmd/mesh-knowledge/progress.go new file mode 100644 index 00000000..6bfd3b71 --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/progress.go @@ -0,0 +1,832 @@ +package main + +// progress: how far a design, a milestone, an issue or a record has come, in one call (novox/hq issue 1947). The +// knowledge base works a design's state out and nobody writes it (ADR 0424 §1, §2); this verb says it at every +// level, in two forms: minimal, one line per child, cheap enough for every five minutes; detailed, the tree of a +// lead's report with the pull requests, the held groups and what waits on whom, for about every thirty. +// +// It keeps no state of its own. It reads, each a request and reply over the bus: the designs through the records +// module (design.go, cached by hq's commit), the issues from the issue tracker (`issue-tracker.list`, whose rows +// say each issue's delivery and when it last moved), and the deliveries and groups from mesh-delivery. + +import ( + "encoding/json" + "errors" + "fmt" + "regexp" + "sort" + "strconv" + "strings" + "time" +) + +// The two forms of an answer. +const ( + DetailMinimal = "minimal" + DetailDetailed = "detailed" +) + +// The states, each with the mark a report draws. +const ( + StateDone = "done" + StateMoving = "moving" + StateWaiting = "waiting" + StateStuck = "stuck" +) + +var marks = map[string]string{StateDone: "✓", StateMoving: "▶", StateWaiting: "⏸", StateStuck: "✗"} + +// rank orders the states from best to worst, for a parent taking its children's. +var rank = map[string]int{StateDone: 0, StateWaiting: 1, StateMoving: 2, StateStuck: 3} + +// The most a detailed answer reads one by one, so one call stays one call's cost. +const ( + MostShows = 30 + MostGets = 20 + // MostIssuesAsked is the most the tracker's list answers by numbers in one call. + MostIssuesAsked = 200 +) + +// SeatAsk asks another seat's verb or a module's tool over the bus and answers its result (stdio.Ask). +type SeatAsk func(key string, body any) (json.RawMessage, error) + +// ProgressAsk is what progress is asked. +type ProgressAsk struct { + Of string + Detail string + Bound int +} + +// Item is one thing whose progress is said: a design, a milestone, an issue, a pull request. +type Item struct { + ID string `json:"id"` + State string `json:"state"` + Title string `json:"title,omitempty"` + Done int `json:"done,omitempty"` + Total int `json:"total,omitempty"` + Moved *time.Time `json:"moved,omitempty"` + Why string `json:"why,omitempty"` + Children []Item `json:"children,omitempty"` +} + +// Progress is the answer. +type Progress struct { + Of string `json:"of"` + Detail string `json:"detail"` + State string `json:"state"` + Tree string `json:"tree"` + Items []Item `json:"items,omitempty"` + Warnings []string `json:"warnings,omitempty"` + Source string `json:"source"` + ReadAt time.Time `json:"read-at"` + Cut string `json:"cut,omitempty"` +} + +// trackerRow is a row of `issue-tracker.list`. +type trackerRow struct { + Number int `json:"number"` + Status string `json:"status"` + Title string `json:"title"` + Needs string `json:"needs"` + Ready bool `json:"ready"` + Delivery string `json:"delivery"` + Worker string `json:"worker"` + Since *time.Time `json:"since"` + Stale bool `json:"stale"` + Abandoned bool `json:"abandoned"` + Moved *time.Time `json:"moved"` +} + +// deliveryRow is a row of `mesh-delivery.deliveries`, with the issues `mesh-delivery.show` names for it. +type deliveryRow struct { + ID string `json:"id"` + Number int `json:"number"` + State string `json:"state"` + Since time.Time `json:"since"` + Waits string `json:"waits"` + Group string `json:"group"` + Issues []int `json:"-"` + Title string `json:"-"` +} + +// groupRow is a row of `mesh-delivery.groups`. +type groupRow struct { + ID string `json:"id"` + State string `json:"state"` + Why string `json:"why"` + Members []string `json:"members"` +} + +var ( + designOf = regexp.MustCompile(`^design\s+([0-9]{1,3})$`) + milestoneOf = regexp.MustCompile(`(?i)^milestone\s+([0-9]{1,3})/(.+)$`) + issueOf = regexp.MustCompile(`^issue\s+([0-9]{1,6})$`) + recordOf = regexp.MustCompile(`(?i)^(ADR|decision|research)\s+([0-9]{1,4})$`) +) + +// Progress says how far what `of` names has come. +func (k *KB) Progress(a ProgressAsk) (*Progress, error) { + detail := strings.TrimSpace(a.Detail) + if detail == "" { + detail = DetailMinimal + } + if detail != DetailMinimal && detail != DetailDetailed { + return nil, fmt.Errorf("detail is %s or %s, not %q", DetailMinimal, DetailDetailed, a.Detail) + } + if k.Seats == nil { + return nil, errors.New("the knowledge base reaches no other seat here") + } + of := strings.Join(strings.Fields(a.Of), " ") + p := &Progress{Of: of, Detail: detail, ReadAt: k.now()} + var err error + switch { + case strings.EqualFold(of, "mesh"): + err = k.progressMesh(p) + case designOf.MatchString(strings.ToLower(of)): + n, _ := strconv.Atoi(designOf.FindStringSubmatch(strings.ToLower(of))[1]) + err = k.progressDesign(p, n, "") + case milestoneOf.MatchString(of): + m := milestoneOf.FindStringSubmatch(of) + n, _ := strconv.Atoi(m[1]) + err = k.progressDesign(p, n, m[2]) + case issueOf.MatchString(strings.ToLower(of)): + n, _ := strconv.Atoi(issueOf.FindStringSubmatch(strings.ToLower(of))[1]) + err = k.progressIssue(p, n) + case recordOf.MatchString(of): + m := recordOf.FindStringSubmatch(of) + n, _ := strconv.Atoi(m[2]) + kind := "ADR" + if strings.EqualFold(m[1], "research") { + kind = "research" + } + err = k.progressRecord(p, kind, n) + default: + return nil, fmt.Errorf("of names mesh, design , milestone / (such as milestone 47/R6), issue , "+ + "decision (or ADR ) or research , not %q", a.Of) + } + if err != nil { + return nil, err + } + fitProgress(p, boundOf(a.Bound)) + return p, nil +} + +// issueRows asks the tracker for these issues, in chunks it takes, and answers them by number with the numbers it +// does not know. +func (k *KB) issueRows(numbers []int) (map[int]trackerRow, []int, error) { + rows := map[int]trackerRow{} + var missing []int + for from := 0; from < len(numbers); from += MostIssuesAsked { + chunk := numbers[from:min(from+MostIssuesAsked, len(numbers))] + raw, err := k.Seats("seat:issue-tracker.list", map[string]any{"numbers": chunk, "limit": len(chunk)}) + if err != nil { + return nil, nil, fmt.Errorf("the issue tracker does not answer: %v", err) + } + var l struct { + Rows []trackerRow `json:"rows"` + Missing []int `json:"missing"` + } + if err := json.Unmarshal(raw, &l); err != nil { + return nil, nil, fmt.Errorf("issue-tracker.list answered something else: %v", err) + } + for _, r := range l.Rows { + rows[r.Number] = r + } + missing = append(missing, l.Missing...) + } + return rows, missing, nil +} + +// issueItem is one issue's state, from its tracker row (1947/AC3). +func issueItem(r trackerRow) (Item, bool) { + it := Item{ID: strconv.Itoa(r.Number), Title: r.Title, Moved: r.Moved} + if it.Moved == nil { + it.Moved = r.Since + } + held := strings.HasPrefix(r.Delivery, "held") || strings.Contains(r.Delivery, " failed") + switch { + case r.Status == "wontfix": + return it, false + case r.Status == "resolved": + it.State, it.Why = StateDone, "resolved" + if r.Delivery != "" && !strings.HasPrefix(r.Delivery, "delivered") { + it.Why = "resolved · " + r.Delivery + } + case r.Abandoned || r.Stale: + it.State, it.Why = StateStuck, fmt.Sprintf("%s, worker %s stale since %s", r.Status, r.Worker, when(r.Since)) + case held: + it.State, it.Why = StateStuck, r.Status+" · "+r.Delivery + case r.Status == "diagnosing" || r.Status == "fixing": + it.State, it.Why = StateMoving, fmt.Sprintf("%s since %s (%s)", r.Status, when(r.Since), r.Worker) + if r.Delivery != "" { + it.Why += " · " + r.Delivery + } + case r.Needs == "decision" || r.Needs == "operator": + it.State, it.Why = StateWaiting, r.Status+", needs "+map[string]string{"decision": "a decision", "operator": "the operator"}[r.Needs] + case !r.Ready: + it.State, it.Why = StateWaiting, r.Status+", waits for what it depends on" + default: + it.State, it.Why = StateWaiting, r.Status+", ready" + } + return it, true +} + +// rollUp gives a parent its children's state: stuck when one is, else moving when one moves, else waiting when one +// waits, else done; done and total counted; moved the latest of theirs (1947/AC3). +func rollUp(parent *Item, children []Item) { + parent.State, parent.Done, parent.Total = StateDone, 0, len(children) + if len(children) == 0 { + parent.State, parent.Why = StateWaiting, "no issues yet" + return + } + worst := StateDone + for _, c := range children { + if c.State == StateDone { + parent.Done++ + } + if rank[c.State] > rank[worst] { + worst = c.State + } + if c.Moved != nil && (parent.Moved == nil || c.Moved.After(*parent.Moved)) { + m := *c.Moved + parent.Moved = &m + } + } + parent.State = worst +} + +// milestoneItems is each milestone of a design as an item with its issues as children. +func milestoneItems(d *Design, rows map[int]trackerRow, missing map[int]bool) ([]Item, []string) { + var items []Item + var warn []string + for _, m := range d.Milestones { + mi := Item{ID: m.ID, Title: m.Outcome} + var kids []Item + for _, n := range m.Issues { + if missing[n] { + warn = append(warn, fmt.Sprintf("milestone %s names %d, which is not an issue", m.ID, n)) + continue + } + r, ok := rows[n] + if !ok { + continue + } + if it, counts := issueItem(r); counts { + kids = append(kids, it) + } + } + rollUp(&mi, kids) + mi.Children = kids + items = append(items, mi) + } + return items, warn +} + +// progressDesign says a design's milestones, or with milestone one milestone's issues. +func (k *KB) progressDesign(p *Progress, number int, milestone string) error { + ds, commit, err := k.designs(false, number) + if err != nil { + return err + } + d := ds[0] + p.Source = fmt.Sprintf("the milestone table of %s at hq %s (until the tracker's milestone records, issue 1781)", d.Path, short(commit)) + p.Warnings = append(p.Warnings, d.Warnings...) + ms := d + if milestone != "" { + m := d.milestone(milestone) + if m == nil { + var ids []string + for _, x := range d.Milestones { + ids = append(ids, x.ID) + } + return fmt.Errorf("design %d has no milestone %q; its milestones are %s", number, milestone, strings.Join(ids, ", ")) + } + ms = &Design{Number: d.Number, Path: d.Path, Title: d.Title, Milestones: []Milestone{*m}} + } + rows, missingList, err := k.issueRows(ms.issues()) + if err != nil { + return err + } + missing := map[int]bool{} + for _, n := range missingList { + missing[n] = true + } + items, warn := milestoneItems(ms, rows, missing) + p.Warnings = append(p.Warnings, warn...) + var extra *extras + if p.Detail == DetailDetailed { + if extra, err = k.readExtras(items); err != nil { + return err + } + p.Warnings = append(p.Warnings, extra.warnings...) + } + top := Item{ID: fmt.Sprintf("design %d", d.Number), Title: d.Title} + rollUp(&top, items) + top.Children = items + if milestone != "" { + top = items[0] + top.ID = fmt.Sprintf("milestone %d/%s", d.Number, items[0].ID) + } + p.State = top.State + p.Items = []Item{top} + p.Tree = renderTree(top, p.Detail, extra, milestone != "") + return nil +} + +// progressMesh says every design being built: those with milestones one line each, the rest counted. +func (k *KB) progressMesh(p *Progress) error { + ds, commit, err := k.designs(true, 0) + if err != nil { + return err + } + p.Source = fmt.Sprintf("the designs in %s at hq %s", DesignsFolder, short(commit)) + var with []*Design + var without []string + for _, d := range ds { + if d.Status != "in-progress" { + continue + } + if len(d.Milestones) == 0 { + without = append(without, fmt.Sprintf("%02d", d.Number)) + continue + } + with = append(with, d) + } + var numbers []int + for _, d := range with { + numbers = append(numbers, d.issues()...) + } + rows, missingList, err := k.issueRows(numbers) + if err != nil { + return err + } + missing := map[int]bool{} + for _, n := range missingList { + missing[n] = true + } + top := Item{ID: "mesh", Title: "the designs being built"} + var kids []Item + for _, d := range with { + items, _ := milestoneItems(d, rows, missing) + di := Item{ID: fmt.Sprintf("design %d", d.Number), Title: d.Title} + rollUp(&di, items) + if p.Detail == DetailDetailed { + di.Children = items + } + kids = append(kids, di) + } + rollUp(&top, kids) + top.Children = kids + p.State = top.State + p.Items = []Item{top} + var b strings.Builder + fmt.Fprintf(&b, "The mesh · %d designs in progress, %d with milestones\n", len(with)+len(without), len(with)) + for i, di := range kids { + branch := "├── " + if i == len(kids)-1 && len(without) == 0 { + branch = "└── " + } + fmt.Fprintf(&b, "%s%s %s · %s — %d of %d milestones done%s\n", branch, marks[di.State], di.ID, di.Title, di.Done, di.Total, movedSay(di.Moved)) + } + if len(without) > 0 { + fmt.Fprintf(&b, "└── %d designs in progress name no milestones (issue 845): %s\n", len(without), strings.Join(without, ", ")) + } + p.Tree = strings.TrimRight(b.String(), "\n") + return nil +} + +// progressIssue says one issue: its state, its pull requests and their deliveries, what it waits for. +func (k *KB) progressIssue(p *Progress, n int) error { + rows, missing, err := k.issueRows([]int{n}) + if err != nil { + return err + } + r, ok := rows[n] + if len(missing) > 0 || !ok { + return fmt.Errorf("%d is not an issue the tracker holds", n) + } + it, counts := issueItem(r) + if !counts { + it.State, it.Why = StateDone, "wontfix" + } + it.ID = fmt.Sprintf("issue %d", n) + p.Source = "the issue tracker and mesh-delivery" + extra, err := k.readExtras([]Item{{Children: []Item{{ID: strconv.Itoa(n), State: StateMoving}}}}) + if err != nil { + return err + } + p.Warnings = append(p.Warnings, extra.warnings...) + p.State = it.State + p.Items = []Item{it} + var b strings.Builder + fmt.Fprintf(&b, "%s issue %d · %s — %s%s\n", marks[it.State], n, it.Title, it.Why, movedSay(it.Moved)) + if g := extra.gets[n]; g != nil { + fmt.Fprintf(&b, "├── %d notes; last %s\n", g.notes, g.last) + if len(g.waitsFor) > 0 { + fmt.Fprintf(&b, "├── depends on %s\n", strings.Join(g.waitsFor, ", ")) + } + } + lines := extra.pullLines(n) + if len(lines) == 0 { + b.WriteString("└── no pull request open or ended in the last day names it\n") + } + for i, l := range lines { + branch := "├── " + if i == len(lines)-1 { + branch = "└── " + } + b.WriteString(branch + l + "\n") + } + p.Tree = strings.TrimRight(b.String(), "\n") + return nil +} + +// progressRecord says what builds a decision record or a research effort: the designs that name it and the issues +// linked to it. +func (k *KB) progressRecord(p *Progress, kind string, n int) error { + key := Key{KindDecision, n} + name := fmt.Sprintf("ADR %04d", n) + if kind == "research" { + key, name = Key{KindResearch, n}, fmt.Sprintf("research %03d", n) + } + g, err := k.Get(GetAsk{Kind: key.Kind, Number: key.Number, Bound: LeastBound}) + if err != nil { + return err + } + ds, commit, err := k.designs(true, 0) + if err != nil { + return err + } + p.Source = fmt.Sprintf("the knowledge base, the designs at hq %s and the issue tracker's links", short(commit)) + var designs []string + for _, d := range ds { + for _, dn := range d.Decisions { + if dn == name { + designs = append(designs, fmt.Sprintf("design %d (%s)", d.Number, d.Status)) + } + } + } + var kids []Item + if kind == "ADR" { + raw, err := k.Seats("seat:issue-tracker.list", map[string]any{"linked": fmt.Sprintf("decision:%04d", n), "limit": MostIssuesAsked}) + if err != nil { + return fmt.Errorf("the issue tracker does not answer: %v", err) + } + var l struct { + Rows []trackerRow `json:"rows"` + } + if err := json.Unmarshal(raw, &l); err != nil { + return fmt.Errorf("issue-tracker.list answered something else: %v", err) + } + for _, r := range l.Rows { + if it, counts := issueItem(r); counts { + kids = append(kids, it) + } + } + } + top := Item{ID: name, Title: g.Title} + rollUp(&top, kids) + top.Children = kids + if len(kids) == 0 { + top.Why = "no issue is linked to it" + } + p.State = top.State + p.Items = []Item{top} + var b strings.Builder + fmt.Fprintf(&b, "%s · %s — %s\n", name, g.Title, g.Status) + if len(designs) == 0 { + b.WriteString("├── no design names it\n") + } else { + fmt.Fprintf(&b, "├── named by %s\n", strings.Join(designs, ", ")) + } + if kind != "ADR" { + b.WriteString("└── issues link decisions, not research efforts: see what it became (knowledge-base.links)\n") + } else if len(kids) == 0 { + b.WriteString("└── no issue is linked to it (issue-tracker.link, kind decision)\n") + } else { + fmt.Fprintf(&b, "└── %s %d of %d linked issues done%s\n", marks[top.State], top.Done, top.Total, movedSay(top.Moved)) + if p.Detail == DetailDetailed { + for i, c := range kids { + branch := " ├── " + if i == len(kids)-1 { + branch = " └── " + } + fmt.Fprintf(&b, "%s%s %s %s — %s%s\n", branch, marks[c.State], c.ID, c.Title, c.Why, movedSay(c.Moved)) + } + } + } + p.Tree = strings.TrimRight(b.String(), "\n") + return nil +} + +// extras is what a detailed answer reads beyond the rows: the open deliveries with their issues, the groups, and +// each issue not done read whole. +type extras struct { + deliveries []deliveryRow + groups map[string]groupRow + gets map[int]*issueGot + warnings []string +} + +type issueGot struct { + notes int + last string + waitsFor []string +} + +// final says a delivery's state says nothing more will happen to it. +func final(state string) bool { + return state == "merged" || state == "closed" || state == "delivered" +} + +// readExtras reads the deliveries not final with the issues each names, the groups, and the issues not done. +func (k *KB) readExtras(items []Item) (*extras, error) { + x := &extras{groups: map[string]groupRow{}, gets: map[int]*issueGot{}} + raw, err := k.Seats("seat:mesh-delivery.deliveries", map[string]any{}) + if err != nil { + return nil, fmt.Errorf("mesh-delivery does not answer: %v", err) + } + var dl struct { + Deliveries []deliveryRow `json:"deliveries"` + } + if err := json.Unmarshal(raw, &dl); err != nil { + return nil, fmt.Errorf("mesh-delivery.deliveries answered something else: %v", err) + } + shown := 0 + for _, d := range dl.Deliveries { + if final(d.State) { + continue + } + if shown == MostShows { + x.warnings = append(x.warnings, fmt.Sprintf("more than %d deliveries are open; the rest are not read", MostShows)) + break + } + shown++ + raw, err := k.Seats("seat:mesh-delivery.show", map[string]any{"id": d.ID}) + if err != nil { + x.warnings = append(x.warnings, fmt.Sprintf("mesh-delivery.show %s: %v", d.ID, err)) + continue + } + var s struct { + Delivery struct { + Issues []int `json:"issues"` + Title string `json:"title"` + } `json:"delivery"` + } + _ = json.Unmarshal(raw, &s) + d.Issues, d.Title = s.Delivery.Issues, s.Delivery.Title + x.deliveries = append(x.deliveries, d) + } + raw, err = k.Seats("seat:mesh-delivery.groups", map[string]any{}) + if err != nil { + return nil, fmt.Errorf("mesh-delivery does not answer: %v", err) + } + var gs []groupRow + if err := json.Unmarshal(raw, &gs); err != nil { + return nil, fmt.Errorf("mesh-delivery.groups answered something else: %v", err) + } + for _, g := range gs { + x.groups[g.ID] = g + } + got := 0 + for _, m := range items { + for _, c := range m.Children { + if c.State == StateDone { + continue + } + n, _ := strconv.Atoi(c.ID) + if got == MostGets { + x.warnings = append(x.warnings, fmt.Sprintf("more than %d issues are not done; the rest are not read whole", MostGets)) + return x, nil + } + got++ + raw, err := k.Seats("seat:issue-tracker.get", map[string]any{"number": n, "brief": true}) + if err != nil { + x.warnings = append(x.warnings, fmt.Sprintf("issue-tracker.get %d: %v", n, err)) + continue + } + var g struct { + Notes []struct { + At time.Time `json:"at"` + By string `json:"by"` + Text string `json:"text"` + } `json:"notes"` + Left int `json:"notes-left-out"` + Links []struct { + Kind string `json:"kind"` + Target string `json:"target"` + } `json:"links"` + } + _ = json.Unmarshal(raw, &g) + ig := &issueGot{notes: len(g.Notes) + g.Left, last: "none"} + if len(g.Notes) > 0 { + last := g.Notes[len(g.Notes)-1] + ig.last = fmt.Sprintf("%s by %s", when(&last.At), last.By) + } + for _, l := range g.Links { + if l.Kind == "depends-on" { + ig.waitsFor = append(ig.waitsFor, l.Target) + } + } + x.gets[n] = ig + } + } + return x, nil +} + +// pullLines is one line per open delivery naming issue n, and per group it is in. +func (x *extras) pullLines(n int) []string { + if x == nil { + return nil + } + var out []string + groups := map[string]bool{} + for _, d := range x.deliveries { + named := false + for _, i := range d.Issues { + if i == n { + named = true + } + } + if !named { + continue + } + state := StateWaiting + if strings.HasPrefix(d.State, "held") || strings.Contains(d.Waits, "a person") { + state = StateStuck + } else if d.State == "checking" || d.State == "merging" || d.State == "delivering" { + state = StateMoving + } + line := fmt.Sprintf("%s %s %s since %s", marks[state], d.ID, d.State, when(&d.Since)) + if d.Waits != "" { + line += ", waits for " + d.Waits + } + out = append(out, line) + if d.Group != "" { + groups[d.Group] = true + } + } + var gids []string + for g := range groups { + gids = append(gids, g) + } + sort.Strings(gids) + for _, id := range gids { + g, ok := x.groups[id] + if !ok { + continue + } + mark := marks[StateMoving] + if g.State == "held" { + mark = marks[StateStuck] + } + out = append(out, fmt.Sprintf("%s group %s %s: %s", mark, g.ID, g.State, g.Why)) + } + return out +} + +// renderTree draws a design's or a milestone's tree: minimal, one line per milestone with the done ones folded into +// one line; detailed, each milestone not done with its issues, their pull requests and groups. +func renderTree(top Item, detail string, x *extras, oneMilestone bool) string { + var b strings.Builder + if oneMilestone { + fmt.Fprintf(&b, "%s %s · %s — %d of %d done%s\n", marks[top.State], top.ID, top.Title, top.Done, top.Total, movedSay(top.Moved)) + writeIssues(&b, "", top.Children, detail, x) + return strings.TrimRight(b.String(), "\n") + } + fmt.Fprintf(&b, "%s %s · %s — %d of %d milestones done%s\n", marks[top.State], top.ID, top.Title, top.Done, top.Total, movedSay(top.Moved)) + var done []string + issuesDone := 0 + var open []Item + for _, m := range top.Children { + if m.State == StateDone { + done = append(done, m.ID) + issuesDone += m.Total + continue + } + open = append(open, m) + } + if len(done) > 0 { + branch := "├── " + if len(open) == 0 { + branch = "└── " + } + fmt.Fprintf(&b, "%s%s %d milestones done (%d issues): %s\n", branch, marks[StateDone], len(done), issuesDone, strings.Join(done, ", ")) + } + for i, m := range open { + branch, under := "├── ", "│ " + if i == len(open)-1 { + branch, under = "└── ", " " + } + left := notDone(m.Children) + if m.Total == 0 { + left = m.Why + } + if detail == DetailMinimal { + fmt.Fprintf(&b, "%s%s %s %d/%d %s%s\n", branch, marks[m.State], m.ID, m.Done, m.Total, left, movedSay(m.Moved)) + continue + } + fmt.Fprintf(&b, "%s%s %s · %s — %d of %d done%s\n", branch, marks[m.State], m.ID, clip(m.Title, 90), m.Done, m.Total, movedSay(m.Moved)) + writeIssues(&b, under, m.Children, detail, x) + } + return strings.TrimRight(b.String(), "\n") +} + +// writeIssues draws a milestone's issues: minimal, those not done; detailed, every issue, and under each not done +// its pull requests, groups and what it depends on. +func writeIssues(b *strings.Builder, indent string, kids []Item, detail string, x *extras) { + var show []Item + for _, c := range kids { + if detail == DetailDetailed || c.State != StateDone { + show = append(show, c) + } + } + for i, c := range show { + branch, under := "├── ", "│ " + if i == len(show)-1 { + branch, under = "└── ", " " + } + fmt.Fprintf(b, "%s%s%s %s %s — %s%s\n", indent, branch, marks[c.State], c.ID, clip(c.Title, 70), c.Why, movedSay(c.Moved)) + if detail != DetailDetailed || c.State == StateDone || x == nil { + continue + } + n, _ := strconv.Atoi(c.ID) + var lines []string + if g := x.gets[n]; g != nil { + if len(g.waitsFor) > 0 { + lines = append(lines, "depends on "+strings.Join(g.waitsFor, ", ")) + } + lines = append(lines, fmt.Sprintf("%d notes; last %s", g.notes, g.last)) + } + lines = append(lines, x.pullLines(n)...) + for j, l := range lines { + br := "├── " + if j == len(lines)-1 { + br = "└── " + } + fmt.Fprintf(b, "%s%s%s%s\n", indent, under, br, l) + } + } +} + +// notDone is a short say of the issues of a milestone not done, for its one line. +func notDone(kids []Item) string { + var parts []string + for _, c := range kids { + if c.State != StateDone { + parts = append(parts, c.ID+" "+c.Why) + } + } + if len(parts) > 3 { + return fmt.Sprintf("%s; and %d more", strings.Join(parts[:3], "; "), len(parts)-3) + } + return strings.Join(parts, "; ") +} + +// when is a time as the operator reads it: local, its day and its hour. +func when(t *time.Time) string { + if t == nil || t.IsZero() { + return "an unknown time" + } + return t.Local().Format("01-02 15:04") +} + +func movedSay(t *time.Time) string { + if t == nil { + return "" + } + return " · moved " + when(t) +} + +// fitProgress keeps an answer within its bound: the items go first, then the tree's last lines, saying what was cut. +func fitProgress(p *Progress, bound int) { + if size(p) <= bound { + return + } + const raise = ": raise bound (at most 65536) or ask a smaller of, such as one milestone" + p.Cut = "the items, the tree's last lines and some warnings" + raise + var cut []string + if len(p.Items) > 0 { + p.Items = nil + cut = append(cut, "the items (the tree says the same)") + } + lines := strings.Split(p.Tree, "\n") + dropped := 0 + for size(p) > bound && len(lines) > 1 { + lines = lines[:len(lines)-1] + dropped++ + p.Tree = strings.Join(lines, "\n") + } + if dropped > 0 { + cut = append(cut, fmt.Sprintf("the tree's last %d lines", dropped)) + } + for size(p) > bound && len(p.Warnings) > 0 { + p.Warnings = p.Warnings[:len(p.Warnings)-1] + if !strings.Contains(strings.Join(cut, ""), "warnings") { + cut = append(cut, "some warnings") + } + } + if exact := strings.Join(cut, "; ") + raise; len(exact) <= len(p.Cut) { + p.Cut = exact + } +} diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go new file mode 100644 index 00000000..9e00ae37 --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go @@ -0,0 +1,348 @@ +package main + +import ( + "encoding/json" + "os" + "path/filepath" + "reflect" + "strings" + "sync" + "testing" + "time" +) + +// The progress verb's tests (novox/hq issue 1947). The designs are hq's, captured at a commit; the tracker's rows and +// the delivery seat's answers were captured from the live mesh on 2026-10-26 (testdata/progress/README.md). + +func progressRecords() dirRecords { + return dirRecords{dir: filepath.Join("testdata", "progress", "hq")} +} + +// seatsWorld plays the issue tracker and mesh-delivery from the captured answers, and counts what it is asked. +type seatsWorld struct { + mu sync.Mutex + rows map[int]json.RawMessage + linked map[string][]int + asked map[string]int + files map[string]json.RawMessage +} + +func newSeatsWorld(t *testing.T) *seatsWorld { + t.Helper() + s := &seatsWorld{rows: map[int]json.RawMessage{}, linked: map[string][]int{}, asked: map[string]int{}, files: map[string]json.RawMessage{}} + var rows []json.RawMessage + if err := json.Unmarshal(readFixture(t, "tracker-rows.json"), &rows); err != nil { + t.Fatal(err) + } + for _, r := range rows { + var n struct{ Number int } + _ = json.Unmarshal(r, &n) + s.rows[n.Number] = r + } + for _, f := range []string{"deliveries.json", "groups.json", "show-car-hunter-2.json"} { + s.files[f] = readFixture(t, f) + } + return s +} + +func readFixture(t *testing.T, name string) json.RawMessage { + t.Helper() + raw, err := os.ReadFile(filepath.Join("testdata", "progress", name)) + if err != nil { + t.Fatal(err) + } + return raw +} + +func (s *seatsWorld) ask(key string, body any) (json.RawMessage, error) { + s.mu.Lock() + defer s.mu.Unlock() + s.asked[key]++ + raw, _ := json.Marshal(body) + var a struct { + Numbers []int `json:"numbers"` + Linked string `json:"linked"` + Number int `json:"number"` + ID string `json:"id"` + } + _ = json.Unmarshal(raw, &a) + switch key { + case "seat:issue-tracker.list": + numbers := a.Numbers + if a.Linked != "" { + numbers = s.linked[a.Linked] + } + var rows []json.RawMessage + for _, n := range numbers { + if r, ok := s.rows[n]; ok { + rows = append(rows, r) + } + } + return json.Marshal(map[string]any{"rows": rows, "matched": len(rows)}) + case "seat:issue-tracker.get": + return json.Marshal(map[string]any{"number": a.Number, "notes": []map[string]any{{"at": "2026-10-26T15:12:15Z", "by": "shanks.node-tools", "text": "a note"}}, + "notes-left-out": 96, "links": []map[string]any{{"kind": "depends-on", "target": "1943"}}}) + case "seat:mesh-delivery.deliveries": + return s.files["deliveries.json"], nil + case "seat:mesh-delivery.groups": + return s.files["groups.json"], nil + case "seat:mesh-delivery.show": + if a.ID == "jschoubben/car-hunter@cc4c8ce12901" { + return s.files["show-car-hunter-2.json"], nil + } + return json.RawMessage(`{"delivery": {}}`), nil + } + return nil, os.ErrNotExist +} + +// countingRecords is Records that counts the documents read and can be moved to another commit. +type countingRecords struct { + dirRecords + commit string + reads int +} + +func (c *countingRecords) Standing() (string, error) { return c.commit, nil } +func (c *countingRecords) Read(p string) (string, string, error) { + c.reads++ + content, _, err := c.dirRecords.Read(p) + return content, c.commit, err +} + +// 1947/AC2: the milestone table of design 47, as hq holds it, read into milestones with their issues, and what the +// prose does not say for certain said as warnings. +func TestTheDesignReaderReadsTheMilestoneTable(t *testing.T) { + content, _, err := progressRecords().Read(DesignsFolder + "/47-delivery-from-commit-to-delivered.md") + if err != nil { + t.Fatal(err) + } + d := parseDesign(DesignsFolder+"/47-delivery-from-commit-to-delivered.md", content) + if d.Number != 47 || d.Title != "Delivery: from a commit to delivered" || d.Status != "in-progress" || !contains(d.Decisions, "ADR 0276") { + t.Fatalf("design %d %q %q %v", d.Number, d.Title, d.Status, d.Decisions) + } + if len(d.Milestones) != 29 { + t.Fatalf("%d milestones", len(d.Milestones)) + } + for _, c := range []struct { + id string + want []int + }{ + {"1", []int{973, 525}}, + {"2b", []int{1025, 1019}}, + {"R6", []int{1942, 1945, 1943, 1944, 1946, 1906, 1130}}, + {"r4d", []int{}}, + {"3 fix", []int{1082, 1049, 1047, 1083, 1079, 1084}}, + } { + m := d.milestone(c.id) + if m == nil { + t.Fatalf("no milestone %s", c.id) + } + if !reflect.DeepEqual(m.Issues, c.want) { + t.Errorf("milestone %s: %v, want %v", c.id, m.Issues, c.want) + } + } + if r4a := d.milestone("R4a"); len(r4a.Issues) != 14 || !containsInt(r4a.Issues, 1360) { + t.Errorf("R4a's range 1353 to 1364 read as %v", r4a.Issues) + } + warned := strings.Join(d.Warnings, "\n") + for _, w := range []string{"issue 1209 is in milestones R3b and R3c", "issue 971 is in milestones 6 and R4c", + `"1353 to 1364" is a range in words, read as the 12 issues`, "milestone R4d names no issue"} { + if !strings.Contains(warned, w) { + t.Errorf("no warning %q in\n%s", w, warned) + } + } + // Design 57's range with an exception, and a design with no milestone table. + content, _, _ = progressRecords().Read(DesignsFolder + "/57-building-config-and-provisioning.md") + d57 := parseDesign(DesignsFolder+"/57-building-config-and-provisioning.md", content) + if m := d57.milestone("1"); m == nil || containsInt(m.Issues, 659) || !containsInt(m.Issues, 678) || !containsInt(m.Issues, 685) { + t.Errorf("design 57 milestone 1: %+v", d57.Milestones) + } + content, _, _ = progressRecords().Read(DesignsFolder + "/53-the-issue-tracker.md") + if d53 := parseDesign(DesignsFolder+"/53-the-issue-tracker.md", content); len(d53.Milestones) != 0 || d53.Status != "in-progress" { + t.Errorf("design 53: %+v", d53.Milestones) + } +} + +func containsInt(xs []int, n int) bool { + for _, x := range xs { + if x == n { + return true + } + } + return false +} + +// 1947/AC6: the designs are read once per commit of hq, and again when it moves; the mesh level names those with +// milestones one by one and counts the rest. +func TestDesignsAreReadAgainOnlyWhenHqMoves(t *testing.T) { + rec := &countingRecords{dirRecords: progressRecords(), commit: "aaaa"} + seats := newSeatsWorld(t) + k := &KB{Records: rec, Seats: seats.ask, Now: func() time.Time { return time.Date(2026, 10, 26, 15, 30, 0, 0, time.UTC) }} + p, err := k.Progress(ProgressAsk{Of: "mesh"}) + if err != nil { + t.Fatal(err) + } + if rec.reads != 3 { + t.Fatalf("read %d documents for the mesh, want 3", rec.reads) + } + if !strings.Contains(p.Tree, "3 designs in progress, 2 with milestones") || !strings.Contains(p.Tree, "design 47 · Delivery: from a commit to delivered") || + !strings.Contains(p.Tree, "1 designs in progress name no milestones (issue 845): 53") { + t.Fatalf("mesh:\n%s", p.Tree) + } + for range 2 { + if _, err := k.Progress(ProgressAsk{Of: "design 47"}); err != nil { + t.Fatal(err) + } + } + if rec.reads != 3 { + t.Fatalf("read %d documents with hq standing still, want 3", rec.reads) + } + rec.commit = "bbbb" + if _, err := k.Progress(ProgressAsk{Of: "design 47"}); err != nil { + t.Fatal(err) + } + if rec.reads != 4 { + t.Fatalf("read %d documents after hq moved, want 4", rec.reads) + } + if _, err := k.Progress(ProgressAsk{Of: "design 99"}); err == nil || !strings.Contains(err.Error(), "no design 99") { + t.Fatalf("an unknown design: %v", err) + } +} + +// 1947/AC3: an issue's state from its tracker row, and a parent's from its children. +func TestIssueStatesAndTheirRollUp(t *testing.T) { + seats := newSeatsWorld(t) + row := func(n int) trackerRow { + var r trackerRow + if err := json.Unmarshal(seats.rows[n], &r); err != nil { + t.Fatal(err) + } + return r + } + for _, c := range []struct { + n int + state string + why string + counts bool + }{ + {1942, StateDone, "resolved", true}, + {1946, StateDone, "resolved · fix merged, not delivered: mesh-catalog#475", true}, + {1906, StateDone, "resolved · held: mesh-delivery#39", true}, + {1945, StateMoving, "fixing since", true}, + {1049, StateStuck, "worker fix-chain-cat@shanks stale since", true}, + {1126, StateWaiting, "open, needs a decision", true}, + {774, StateWaiting, "open, ready", true}, + {1021, "", "", false}, + } { + it, counts := issueItem(row(c.n)) + if counts != c.counts || it.State != c.state || !strings.Contains(it.Why, c.why) { + t.Errorf("issue %d: %s %q (counts %v), want %s %q", c.n, it.State, it.Why, counts, c.state, c.why) + } + } + at := func(h int) *time.Time { t := time.Date(2026, 10, 26, h, 0, 0, 0, time.UTC); return &t } + for _, c := range []struct { + name string + kids []Item + state string + done int + moved *time.Time + }{ + {"all done", []Item{{State: StateDone, Moved: at(9)}, {State: StateDone, Moved: at(11)}}, StateDone, 2, at(11)}, + {"one moves", []Item{{State: StateDone}, {State: StateMoving, Moved: at(12)}, {State: StateWaiting}}, StateMoving, 1, at(12)}, + {"one stuck outranks moving", []Item{{State: StateMoving}, {State: StateStuck}}, StateStuck, 0, nil}, + {"only waiting", []Item{{State: StateDone}, {State: StateWaiting}}, StateWaiting, 1, nil}, + {"no issue yet", nil, StateWaiting, 0, nil}, + } { + var p Item + rollUp(&p, c.kids) + if p.State != c.state || p.Done != c.done || !reflect.DeepEqual(p.Moved, c.moved) { + t.Errorf("%s: %s %d %v", c.name, p.State, p.Done, p.Moved) + } + } +} + +// 1947/AC1 (its unit side): design 47 in its two forms, and one milestone. +func TestProgressOfADesignMinimalAndDetailed(t *testing.T) { + seats := newSeatsWorld(t) + k := &KB{Records: progressRecords(), Seats: seats.ask, Now: func() time.Time { return time.Date(2026, 10, 26, 15, 30, 0, 0, time.UTC) }} + p, err := k.Progress(ProgressAsk{Of: "design 47"}) + if err != nil { + t.Fatal(err) + } + raw, _ := json.Marshal(p) + if len(raw) > DefaultBound || p.Detail != DetailMinimal || !strings.Contains(p.Source, "issue 1781") { + t.Fatalf("%d bytes, %s, %s", len(raw), p.Detail, p.Source) + } + if seats.asked["seat:issue-tracker.list"] != 1 || seats.asked["seat:mesh-delivery.deliveries"] != 0 || seats.asked["seat:issue-tracker.get"] != 0 { + t.Fatalf("minimal asked %v", seats.asked) + } + lines := strings.Split(p.Tree, "\n") + if !strings.HasPrefix(lines[0], "✗ design 47 · Delivery: from a commit to delivered — ") { + t.Fatalf("first line %q", lines[0]) + } + for _, want := range []string{"▶ R6 6/7 1945 fixing since", "✗ 3 fix 0/1 1049 fixing, worker fix-chain-cat@shanks stale since"} { + if !strings.Contains(p.Tree, want) { + t.Errorf("no %q in\n%s", want, p.Tree) + } + } + d, err := k.Progress(ProgressAsk{Of: "design 47", Detail: DetailDetailed, Bound: MostBound}) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{"├── ✓ 1946 ", "resolved · fix merged, not delivered: mesh-catalog#475", + "▶ 1945 The controller still plans", "jschoubben/car-hunter@cc4c8ce12901 ready since 10-26 17:05, waits for its merge", + "depends on 1943", "97 notes; last 10-26 17:12 by shanks.node-tools"} { + if !strings.Contains(d.Tree, want) { + t.Errorf("detailed: no %q in\n%s", want, d.Tree) + } + } + if len(d.Items) != 1 || d.Items[0].ID != "design 47" { + t.Fatalf("items %+v", d.Items) + } + m, err := k.Progress(ProgressAsk{Of: "milestone 47/r6", Detail: DetailDetailed}) + if err != nil { + t.Fatal(err) + } + if !strings.HasPrefix(m.Tree, "▶ milestone 47/R6 · ") || !strings.Contains(m.Tree, "✓ 1130") { + t.Fatalf("milestone:\n%s", m.Tree) + } + small, _ := k.Progress(ProgressAsk{Of: "design 47", Detail: DetailDetailed, Bound: LeastBound}) + if raw, _ := json.Marshal(small); len(raw) > LeastBound || !strings.Contains(small.Cut, "raise bound") { + t.Fatalf("%d bytes within %d; cut %q", len(raw), LeastBound, small.Cut) + } + for _, bad := range []ProgressAsk{{Of: "design"}, {Of: "milestone 47/R99"}, {Of: "design 47", Detail: "full"}} { + if _, err := k.Progress(bad); err == nil { + t.Errorf("%+v was answered", bad) + } + } +} + +// 1947/AC5: an issue's own level, and a record's: the designs that name it and the issues linked to it. +func TestProgressOfAnIssueAndARecord(t *testing.T) { + w := imported(t, newMemStore()) + seats := newSeatsWorld(t) + seats.linked["decision:0276"] = []int{1942, 1945} + w.k.Records, w.k.Seats = progressRecords(), seats.ask + p, err := w.k.Progress(ProgressAsk{Of: "issue 1945"}) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{"▶ issue 1945 · The controller still plans", "97 notes", "depends on 1943", + "jschoubben/car-hunter@cc4c8ce12901 ready"} { + if !strings.Contains(p.Tree, want) { + t.Errorf("issue: no %q in\n%s", want, p.Tree) + } + } + r, err := w.k.Progress(ProgressAsk{Of: "ADR 0276", Detail: DetailDetailed}) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{"ADR 0276 · ", "named by design 47 (in-progress)", "▶ 1 of 2 linked issues done", "✓ 1942"} { + if !strings.Contains(r.Tree, want) { + t.Errorf("record: no %q in\n%s", want, r.Tree) + } + } + if _, err := w.k.Progress(ProgressAsk{Of: "issue 99999"}); err == nil { + t.Error("an issue the tracker does not hold was answered") + } +} diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/README.md b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/README.md new file mode 100644 index 00000000..9ca111c3 --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/README.md @@ -0,0 +1,7 @@ +Captured for the progress verb's tests (novox/hq issue 1947), never written by hand: + +- `hq/`: three designs of hq at the commit in `hq/COMMIT`, copied whole. +- `tracker-rows.json`: rows of `issue-tracker.list` with `numbers`, as the live tracker answered them on + 2026-10-26 16:2x (before rows said `moved`). +- `deliveries.json`, `groups.json`, `show-car-hunter-2.json`: parts of `mesh-delivery.deliveries`, `.groups` and + `.show` as the live delivery seat answered them on 2026-10-26 16:2x, cut to the members named here. diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/deliveries.json b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/deliveries.json new file mode 100644 index 00000000..9954faac --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/deliveries.json @@ -0,0 +1,5 @@ +{"deliveries": [ + {"for": "17m26s", "id": "jschoubben/car-hunter@cc4c8ce12901", "number": 2, "since": "2026-10-26T15:05:01Z", "state": "ready", "waits": "its merge"}, + {"for": "34m7s", "id": "novox/mesh-catalog@8c5f27627b67", "number": 502, "since": "2026-10-26T14:48:20Z", "state": "merged"}, + {"for": "19h50m33s", "group": "fix/1945-credential-when-row-without-seal", "id": "novox/mesh-delivery@61196c99e6a4", "number": 46, "since": "2026-10-25T19:31:54Z", "state": "held", "waits": "a person: merged without a passing check: only a person decides what follows"} +]} diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/groups.json b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/groups.json new file mode 100644 index 00000000..b222ed9f --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/groups.json @@ -0,0 +1,3 @@ +[ + {"composed": "asked as mesh/check 3fad1982742a", "composed_gate": true, "id": "fix/1945-credential-when-row-without-seal", "members": ["novox/mesh-controller@fbf14b68dbab (held)", "novox/mesh-delivery@61196c99e6a4 (held)"], "state": "held", "why": "a member is held: novox/mesh-controller@fbf14b68dbab (merged without a passing check: only a person decides what follows); novox/mesh-delivery@61196c99e6a4 (merged without a passing check: only a person decides what follows)"} +] diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/hq/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/hq/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md new file mode 100644 index 00000000..67362a8b --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/hq/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md @@ -0,0 +1,1133 @@ +--- +layer: to-be +status: in-progress +code: [mesh-controller, mesh-catalog modules/mesh-delivery-gate, mesh-delivery, mesh-host, mesh-sdk, mesh-vault, hq 00-META/checks] +updated: 2026-10-26 +decisions: + - "ADR 0551" + - "ADR 0550" + - "ADR 0549" + - "ADR 0548" + - "ADR 0547" + - "ADR 0539" + - "ADR 0536" + - "ADR 0534" + - "ADR 0529" + - "ADR 0530" + - "ADR 0528" + - "ADR 0527" + - "ADR 0526" + - "ADR 0525" + - "ADR 0524" + - "ADR 0494" + - "ADR 0493" + - "ADR 0491" + - "ADR 0488" + - "ADR 0487" + - "ADR 0486" + - "ADR 0485" + - "ADR 0484" + - "ADR 0483" + - "ADR 0482" + - "ADR 0480" + - "ADR 0478" + - "ADR 0477" + - "ADR 0469" + - "ADR 0466" + - "ADR 0465" + - "ADR 0463" + - "ADR 0457" + - "ADR 0453" + - "ADR 0452" + - "ADR 0450" + - "ADR 0449" + - "ADR 0447" + - "ADR 0446" + - "ADR 0445" + - "ADR 0444" + - "ADR 0437" + - "ADR 0435" + - "ADR 0434" + - "ADR 0433" + - "ADR 0431" + - "ADR 0429" + - "ADR 0428" + - "ADR 0426" + - "ADR 0421" + - "ADR 0420" + - "ADR 0419" + - "ADR 0418" + - "ADR 0408" + - "ADR 0407" + - "ADR 0406" + - "ADR 0405" + - "ADR 0404" + - "ADR 0403" + - "ADR 0402" + - "ADR 0401" + - "ADR 0398" + - "ADR 0395" + - "ADR 0382" + - "ADR 0377" + - "ADR 0364" + - "ADR 0362" + - "ADR 0360" + - "ADR 0350" + - "ADR 0323" + - "ADR 0313" + - "ADR 0306" + - "ADR 0304" + - "ADR 0299" + - "ADR 0282" + - "ADR 0276" + - "ADR 0270" + - "ADR 0267" + - "ADR 0250" + - "ADR 0239" + - "ADR 0238" + - "ADR 0233" + - "ADR 0214" + - "ADR 0201" +--- + +# 47 — Delivery: from a commit to delivered + +**A change reaches the machines in one of two ways, both owned by mesh-delivery: a hotfix (one pull request, +delivered at its merge) or a feature (one branch name across its repositories: in two or more, one delivery +group merged in one pass on the operator's one Approve as one source revision; in one, a lone delivery). From +release 6 each node takes a merged change from its wants, the module's canary first (ADR 0539). A design's +milestones are features delivered one after another. The trunk is what runs; there is no "release branch"** +(ADR 0401, ADR 0418 §1, ADR 0421 §2). + +**From the delivery of issues 1781 to 1783, a milestone is a record of the issue tracker and the unit that is released.** Its issues are linked to it, and pull requests and commits to the issues, by the issue tracker's link verb, never matched from a description; a review is recorded by a verb of the delivery seat; the mesh builds and checks each pull request as it comes in, and asks the operator once to release the milestone when every issue is ready; its pull requests are the delivery group, in whatever repositories and on whatever branches, and nothing ships outside a released milestone (ADR 0493 §1 to §11). Until that delivery the branch-name rules in this design stay what runs, kept by hand (ADR 0493 §12). A change across any repositories, the mesh-sdk included, is one group whose developer orders no release, from R4e's delivery for a group with a mesh-sdk member; until then an SDK change ships before its users (ADR 0482 §1, §7). + +**Every step starts on an event, and the state lives on the bus.** The forge's events, the parts' keys and the +nodes' haves move a delivery; nothing collects merges, plans them or orders them in tiers. From release 6 each +node applies its wants one module at a time, a new fingerprint reaching a module's canary first and its other +nodes once it runs healthy there (ADR 0429 §6, ADR 0539); and one chain, from module to source revision to build +to want to have, is kept in the bus's key-value store as its only source (ADR 0406, ADR 0408, ADR 0429 §17). A +running delivery never touches merging: a merge moves the source revision, and the wants follow it. + +This is the delivery of [to-be 10](10-delivery.md) carried out. To-be 10 says what is built: the comparison +of source and artifacts decides, and nothing here changes that. This design says who answers for one +commit's journey, in what order the steps are allowed, and where each fact of it is kept. It was rewritten on +2026-10-16 from the decisions of research 055, which replaced the "release branches", the toolchain image, the +controller's delivery tables and the timers that polled them. + +## Where it goes: parts that watch and write keys + +ADR 0429 (accepted on 2026-10-18, from research 063) takes building and delivering out of the controller. They +become independent **parts**, each a process with its own bus credential that watches keys and writes keys: +no part calls another, there is no plan object and no orchestrator, and each key has one writer. A delivered +source revision is derived from the keys: every **want** equals its module's **fingerprint**, and every +**have** equals its want and is healthy. + +- **The forge bridge** turns the forge's events into stream messages and is the only part that calls the + 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. At each trunk merge of the catalogue it also reads the catalogue's + **placement files**, one per node, into its state `placement`: one key `placement.` with that node's + modules, their pins, host ports and bindings, and the source revision. It adds the nodes to `modules.` + and says `placement-changed` only for a node whose key differs (ADR 0549 §1, §2). +- **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). + It judges a delivery once per set of heads, and carries a verdict to a newer trunk commit by its package + digests instead of judging it again (ADR 0548 §1, §5; *A move of the trunk carries every verdict*, below). +- **The gate** merges a group's pull requests once check, review and the operator's Approve agree on the same + heads, and then moves the mesh's source revision in one write. While the check gates merges, it holds an + approved merge for another delivery's running check of the repositories the merge goes into, and keeps the + merge's **merge turn** there (ADR 0548 §2, §3, as ADR 0551 §1 bounds them; *Nothing merges into a repository + under check*, below). +- **The teller** carries conditions and asks to the operator's phone and is the only writer of an ask's answer. +- **The assigner** writes which fingerprint each node should run, by six rules: the build is done, the + fingerprint is not held, the node is the module's **canary** or the canary runs it healthy, at most `step` + other nodes are ahead when `step` is above zero, every provider runs healthy, and the module's grant + `grants..` exists in the grants part's state (ADR 0429 §6, ADR 0547 §4). No rule waits on a + composition: the node composes its own (ADR 0547 §2). Each want `want..` names the fingerprint, + the artifact's digest, the source revision and whether the module holds data; the assigner also writes the + node's own want `want..node`, and never deletes a want (ADR 0408 §4). A move a person must allow is judged + by the node agent, which waits; the assigner raises the ask and on a yes writes the want again naming it + (ADR 0547 §3). It reads which modules run on which node from the indexer's `placement`, on its watch + (ADR 0549 §3); until ADR 0547's third milestone it also reads the controller's assignments, writes no want for + a module on a node that the two do not both name, and raises a condition naming the pair while they differ + (ADR 0549 §5). + 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 the machines, settings and grants it reads, with + mesh-sdk's composition, the one every node agent composes with (ADR 0446, ADR 0547 §2). Its module was made in + R4g with release 5; from release 6 it writes no difference (ADR 0524 §4, ADR 0528, ADR 0539 §8). +- **The node agent**, the node-engine's own process, follows its node's wants, **composes each wanted module for + its own node** with mesh-sdk's composition from the manifest by digest, the module's resolved values from + mesh-config, its secrets and bus credential asked of the vault sealed to the node's key, its licences and what + its providers contribute, applies one module at a time and only what differs from what runs, backs up a data + module first, and writes its haves with the fingerprint and the digest of the composition it runs (ADR 0547 §2, + §3; ADR 0539 §16). Nothing composed crosses the bus, so nothing is signed; of other nodes it reads only their + public records and the manifests wanted on them. On the node holding `mesh-broker` it also writes the bus's user + list, alone and with no want (*The bus's user list*, below). +- **The grants part** (module `mesh-grants`, holding the seat `mesh-grants`) composes who may say what on the bus + with mesh-sdk's bus composer: each module's membership, the buckets, logs and streams a manifest declares, the + bus's user list, and from ADR 0547's third milestone `grants..` (ADR 0547 §5, §6). It publishes + memberships from the first milestone; until the operator's `take-over` it keeps the bus's user list, the + assertions and the grants in shadow, composing and comparing them with what the bus loaded and writing nothing + of them that takes effect; its tools read all it holds and redo all it does. +- **The watchman** turns the bus's connections into presence and mirrors every key whose next move a machine + owes into a **pulse**, whose expiry is a stall. +- **The status view** is the operator's command `mesh-delivery-cli` and a web view, whose wants page sets each + want beside its node's have; it reads every bucket, writes only its own answers to asks, and says four events + derived from the keys: `delivered` once for each source revision whose wants and haves all agree, and + `moving`, `settled` and `stuck` once for a module on a node at one composition (ADR 0539 §17; *A delivery* + below). + +Ten of these are modules of the repository `mesh-delivery`, prefixed `mesh-delivery-` (ADR 0429 §2); the +repository is made by the forge module from its config (ADR 0428; issue 1128 makes the config value reach the +node at once); the grants part is one of them. The reviewer stays the review agent's session. + +**The controller leaves build and delivery** (ADR 0547, accepted 2026-10-25). The drawing above is the target, and +no act of build and delivery is the controller's in it: no part takes in done keys, composes centrally, signs a +composition, mints a bus account, writes a grant, publishes a membership, composes the user list or forwards a try +on the controller's behalf. Its code is not changed for this; it stops by losing its lease (ADR 0547 §1). The way +there is three milestones, one issuer of each key at every moment (ADR 0547 §11): + +1. **The list follows, the grants part in shadow** (issue 1945): the bus node applies the user list alone, with + no want; mesh-sdk carries a copy of the bus composer, held byte for byte to the one release 6 runs; the grants + part publishes memberships (by the one grant the operator allowed in the controller's composer and its + mesh-sdk copy, publishing the assignment subjects and nothing more) and composes and compares the user list, + assertions and grants in shadow with its tools; the vault hashes the bus accounts it is handed into its key + `bus-accounts.`. The agent of the node holding `mesh-broker` hands the vault, sealed to it, the hash of + every user of the list the bus loaded, and the vault adopts the hash of each user it holds no value for, and + again when the loaded list carries a new hash for that user (ADR 0550 §4). The + operator's `take-over` makes the grants part's list the one loaded; it is allowed only when the users of both + lists are equal but for users placement no longer names, so it waits until the enrolment users and the persons + have records on the bus, and the second milestone waits for it (ADR 0547 §8, §11). +2. **Nodes compose** (issue 1126): each node agent composes its own modules; no node applies a composition made + elsewhere but the bus's user list, which the bus node keeps able to follow until the third milestone so `hand-back` has a list to return to. +3. **Nothing is left with the controller** (issues 1126, 774): the vault mints bus accounts, the grants part + writes grants, the try is the checker's own verb, placement is read from the catalogue alone (ADR 0549 §5, + §6), the vault signs with the mesh's signing key (ADR 0550 §2), and the controller's lease is handed to a holder that is no controller once every other job + it keeps has a home by its own decision (research 072). + +No machine is enrolled before the third milestone: until the vault mints enrolment tokens and node accounts, +those are the controller's alone (ADR 0550 §6). + +Until each milestone is delivered, release 6's text below holds for what that milestone replaces (ADR 0547 §11). + +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). Release 6's were +rewritten when it was prepared, from ADR 0539, and its acceptance tests restated after its delivery; release 6 +is delivered, so ADR 0539's clauses hold (ADR 0539 §20), and the as-is design describes the delivery as it runs. + +## The parts + +The target (ADR 0547): + +``` + forge ──forge messages──► forge bridge ──► the bus ◄── done keys ── builder, build modules (build seats) + │ ▲ + gate: the merge, the source revision ◄┤ ├── indexer: fingerprints, manifests, placement + grants part: memberships, assertions,◄┤ ├── assigner: each node's wants, by six rules + the user list, grants │ ├── watchman: presence, pulses, stuck keys + vault: secrets, signing, accounts ◄┤ └── status view: delivered, moving, settled, stuck + ▼ + node agent in the node-engine on every node + (reads its wants; composes its own modules; backs up first; applies; writes its haves; + on the bus node, writes the bus's user list) +``` + +- **The gate** (module `mesh-delivery-gate`, holding the seat `mesh-delivery`) owns hotfixes, features and + milestones up to the merge: it keeps one key per pull request, asks the operator's Approve, merges a group in + one pass and moves the source revision in one write. It asks the controller nothing, consumes no event of a + plan, never sends to a machine and writes no composition (ADR 0539 §17). +- **The controller** is no part of build and delivery (ADR 0547 §1). The jobs it still keeps outside them + (nodes and adoption, identities, secrets and given values, licences, config moves, conditions and facts, ADR + 0429 §9) each move by a decision of their own under research 072 before it loses its lease. Two are decided, by + the operator's answers of 2026-10-26: placement lives in the catalogue (ADR 0549), and the mesh's signing key in + the vault (ADR 0550 §2). +- **The vault** keeps every secret the mesh keeps (ADR 0550 §1): the mesh's signing key, licence grants and + tokens, bus account passwords and module secrets; a holder gets a value only sealed to its own key, or asks the + vault to act with it. It makes the mesh's signing key and signs enrolments and node certificates on request, by + its verb `sign` (ADR 0550 §2). From ADR 0547's third milestone it mints each bus account a membership names, + seals it to its node and writes its bcrypt hash for the grants part alone; until then it hashes the accounts it + is handed, and adopts from the bus node's agent the hashes of the accounts it holds no value for (ADR 0547 §9, + ADR 0550 §4). +- **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 the gate subscribes to none (ADR 0421 §11). It sets statuses, keeps the delivery's view + as one comment on the pull request, and appends to the commit's note under `refs/notes/mesh-plan`. +- **Each node's backup seat** (`node-backup`) backs up the data a module keeps on its node before the node agent + applies a new fingerprint of it, and says on the bus when that backup ended (ADR 0404, ADR 0539 §16). + +## Two ways a change reaches the machines + +*ADR 0401.* + +| Way | What it is | How it is delivered | +|---|---|---| +| **hotfix** | one pull request in one repository | at its merge: it moves the source revision, and each module it changes is wanted on its nodes by the assigner's rules (ADR 0539) | +| **feature** | one branch name across its repositories; in two or more, its pull requests are one delivery group; in one, a single pull request | a group is merged in one pass once its composed check passed at every member's head and the operator gave the group's one Approve (ADR 0418 §4, ADR 0421 §2); its merges are one source revision, each module built once and wanted on each node that runs it (ADR 0418 §1, ADR 0429 §11); in one repository, as a lone delivery like a hotfix | + +- **The trunk is what runs.** Every delivery is cut from a repository's trunk. A merge into any other branch + starts no delivery, and the controller says so for that merge (issue 889). Holding merged work back, on a + branch beside the trunk or per module, was dropped: from 2026-10-13 23:52 to 2026-10-16 09:32 feature 57's + branches held the controller for 55.7 h, and 28 of 68 other merges touched a held module (research 055, + finding 3). +- **A design's milestones are features, one after another.** Each milestone is delivered as a feature when its + own group is ready; the next starts when the previous one's source revision is delivered (playbook 04). +- **A milestone is functional, its delivery shaped technically.** A milestone is named by what the operator or + the mesh can do once it is delivered; how it ships is shaped by the modules its changes move and the nodes that + run them, read from the indexer's candidates of its group through the view's `indexer_candidates` (the gate's + `what-if` went at release 6, ADR 0539 §17). +- **What a merge waits for.** After its checks, a merge waits for one act of a person: the operator's Approve, + asked as soon as both required checks passed at the head (ADR 0421 §1, §3). For a group the Approve is asked + once, when every member's checks and the group's composed check passed, and covers every member's head + (ADR 0421 §2); it is the review ADR 0382 §6 asks (ADR 0421 §9). No ready mark, label, comment, forge review or + recheck stands before it. The operator's Approve is asked for every merge into main, a hotfix's too + (ADR 0398 §3; the operator, 2026-10-16 23:40). + +## A delivery + +*ADR 0239, ADR 0429 §1, ADR 0539 §17.* + +Its id is `/@`. The gate holds, up to the merge: + +- the pull request (number, base, head branch, description), or none for a commit that reached the trunk + without one; +- its **state** and its **transitions** (the last hundred, each with when, the event, from, to and why); +- its **Approve**: the ask, the operator's answer with its warrant, and the review record the pull request was + opened with (ADR 0421 §5, §8); +- once merged, **the commit it landed on the trunk as** and the source revision its merge moved. Every member of + a delivery group moves the same source revision (ADR 0429 §11). + +**Delivered is derived, never recorded.** A source revision is delivered when every want of a module the +indexer holds equals that module's fingerprint and every have equals its want, running and healthy (ADR 0429 +§1). No part records it: the status view derives it from `source.main`, the wants and the haves, and says the +event `delivered` once for each source revision that reaches it. The issue tracker hears that event to say a +fix delivered, and a newer delivered revision covers the older merges of its repository, as the forge says one +contains the other (issue 1946). + +**A module on a node is moving, settled or stuck.** The status view says `moving` when a module's want on a +node differs from its have, `settled` when the have reaches the want running and healthy, and `stuck` when the +module is held, its health check failed, or the watchman says the want stopped moving past its bound (thirty +minutes); each once for a node, a module and a composition. The messenger holds the operator's notices that +touch that module on that node from `moving` until `settled` or `stuck`, then tells them once with how it +ended, as ADR 0505 §3 has it for a delivery in motion (issue 1946). + +**One key per pull request.** Beside the delivery, the gate keeps one key per open pull request on the +bus: its head, the trunk commit its current check is asked against, and its verdict, written by +compare-and-set (ADR 0406 §4). It writes a new head when the forge says one, and a new trunk commit when the +pull request's verdict is carried to it or a whole check is asked against it (ADR 0548 §5, §7). A running check +watches that key and stops only when the head in it moves; a trunk move never ends it (ADR 0548 §1). + +## The state table + +*ADR 0539 §17.* + +One table, compiled into the gate, read by its code, its test and its `table` verb. It holds the merge's states +only: what follows the merge is the wants' and the haves', and the view derives it (*A delivery* above). Each +state has a **bound**: how long it may be held before `stalled` lists it, for a merge or a check past its bound. + +| State | Means | Bound | +|---|---|---| +| proposed | a head was announced and is being checked | 1 hour from when its check is asked; none while its verdict is out of date and no check is asked | +| ready | it passed; its Approve is asked (a group member's once the group is ready), and it waits for it and the merge | none | +| rejected | it failed its check | none | +| held | it waits for a person, a carry or a check: an approved merge whose verdict is being carried to the trunk's head, a conflict, a member the forge refused, and, while the check gates merges, an approved merge waiting on its merge turn for another delivery's running check (ADR 0548 §2, §8, ADR 0551 §1) | 1 day; a merge turn 30 minutes | +| merged | merged on its trunk; its source revision moved | final | +| closed | closed without a merge | final | + +**A verdict holds for one trunk commit, and is carried when the trunk moves** (ADR 0323, ADR 0548 §5). The +check judges the head merged into the trunk's head when the build module clones it, and the verdict names both, +and the commit of each sibling it read at its trunk (ADR 0548 §6). When another merge moves the trunk, or a +sibling's trunk, a `ready` delivery goes back to `proposed` on the event `trunk moved`, its `mesh/check` is +pending and says it is being carried, and the checker carries the verdict to the new commit once any check +running on it has ended: it merges the head onto the new trunk head, computes every package's digest, and runs +only the packages whose digest the move changed; the composition is kept while no fingerprint of a module it +composed changed. A carried pass names the commits it was carried from (`carried-from`). An Approve already +asked stays; the merge waits for the carried verdict, and the Approve is not asked again for a trunk move +(ADR 0421 §1, §3, as ADR 0548 §8 changes them). The row from `proposed` to `ready` or `rejected` takes only a +verdict that names the trunk's head (*A move of the trunk carries every verdict*, below). + +**Nothing of a merged delivery is kept moving in the gate.** The delivery states of the plans (delivering, the +backup hold, superseded, covered, stopped, retired and failed) and the machine steps went at release 6: a +module's progress on each node is its want against its have, shown by the view (ADR 0539 §17). + +## Order without a person + +*ADR 0419 §1, §2, ADR 0429 §6, ADR 0539 §6, §7.* + +A delivery group has no order among its members, and nobody writes one (ADR 0419 §1). From release 6 there are +no tiers: no plan orders modules into steps. What reaches a node, and when, is the assigner's six rules, read +per module and node from the keys: + +- **A build before its want.** No want names a fingerprint whose build is not done; a module whose build waits + is named by nothing and fails nothing (rule 1; ADR 0539 §18). +- **A provider before its consumers.** A module's new fingerprint is wanted on a node only while every provider + it requires runs healthy (rule 5). +- **The canary before the rest.** A new fingerprint goes to the module's canary first, and to its other nodes + once it runs healthy there, at most `step` of them ahead when `step` is above zero (rules 3 and 4). +- **A credential and a composition before the want.** The module's credential and grant are written when its + build is done, and its composition for the node must exist, before a want can name it (rules 6 and 7; + ADR 0539 §7). +- **An SDK change ships with its users.** mesh-sdk is a library built into the modules that use it, so no + module waits for it to be sent anywhere (ADR 0482 §1). +- **No `after:` line is read.** The gate reads no `after:` line, no refusal asks for one, and a description + that holds one is told once on its pull request that the line is not read (ADR 0419 §2). + +The planner's edges (*built by*, *SDK before its users*, *verb before caller*, *version skew*) and its one +default, the node-engine before the controller, ordered the tiers of a plan until release 6 (ADR 0419 §3 to +§5). For release 6's own switch, the last plan of release 5's controller, the version-skew edge and that +default yielded so the node-engine and the controller shared one tier (ADR 0539 §11, issue 1942). + +**The SDK by version.** A Go module names the SDK by its released version and fetches it from the mesh's forge, +checked against its recorded checksum; vendored copies go. Inside a group, a member may name the SDK member's +head: its merge check resolves that commit from the forge and builds against it, and the head stays fetchable +after the group merges because the forge keeps every pull request's head. The catalogue's check accepts an SDK +commit on the SDK's trunk or the head of an SDK pull request the forge holds, open or merged (ADR 0402 §2 to §4). + +## Checks + +### One check per set of heads, kept by the bus + +*ADR 0323, ADR 0350, ADR 0395, ADR 0406, ADR 0548 §1.* + +- **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 carries after a move of a trunk or a sibling (ADR 0525 §1, §2, ADR 0536 §4, ADR 0548 §5). 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 check runs to its end.** A delivery, a lone pull request or a delivery group, is judged by one check per + set of heads: the checker claims its verdict key once, and the check runs to its verdict (ADR 0548 §1). Only a + new head ends a running check: a push, or a member joining or leaving its group. The check watches its pull + request's key and stops when the head in it moves, without asking the forge (issue 726); an ended check + delivers no verdict, so none of its builds is reused. A trunk move never ends a check: its verdict goes out of + date when it ends, and is carried. A check that failed because a part of the mesh was not heard is still asked + again when the nodes change, which judges heads no check judged. +- **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 build is keyed by its module's fingerprint, so the merge commit's + fingerprints find the builds the check already made, and the merge builds nothing again (ADR 0453 clause 5). + +### A move of the trunk carries every verdict + +*ADR 0548 §5 to §8, which change ADR 0395 §1 to §3 and ADR 0323 §3, §4; ADR 0551 §1 keeps them whether or not +the check gates merges.* + +- **A carry, not a check.** A trunk move puts every verdict judged on the old commit out of date and carries it, + once any check running on it has ended: one ask on the build seats' `recheck` stream that merges the head onto + the new trunk head and computes every package's digest. A package whose digest has a kept verdict does not run; + a move that changes nothing a package's tests read runs nothing; the composition check is kept while no + fingerprint of a module it composed changed, and runs again otherwise (ADR 0548 §5). A carried pass is + rewritten under its key by compare-and-set with the new trunk heads and names the commits it was carried from; + a failure fails it, naming the package and the move. This bends the operator's "only a conflict asks a new + check" once, on purpose: a package whose inputs the move changed runs, so the trunk never gets a combination no + test ran. +- **A sibling's move is carried too.** A verdict names the commit of each sibling it read at its trunk, beside + the repositories it merged (ADR 0484 §4); a move of one is carried as a trunk move is, and only the packages + whose tests read siblings can change digest and run (ADR 0548 §6). +- **Only a head git refused gets a whole check.** A carry whose merge conflicts, or a merge the forge refuses for + a conflict, fails the verdict naming the files in conflict, and its Approve's ask is withdrawn; its next check + is a whole check of its head, at its next push or at the first later trunk move onto which it merges cleanly + (ADR 0548 §7). That is the one check a trunk move asks; a verdict that can be carried is never checked again + for a trunk move. +- **The line says the carry.** A carried verdict's line names the head on the new trunk commit first, as the gate + and the merge tool read a line, then per level the trunk commit it was carried from and the packages run and + kept (ADR 0548 §8). While a verdict is carried, `mesh/check` on the heads is pending and says so (ADR 0548 §6). +- **A group carries one verdict.** For a group, a move carries the group's one verdict on every member's head; + the group's own merges, made in one pass on its Approve in its order, carry nothing (ADR 0418 §4, ADR 0323 §6). +- A pull request with no push for 7 days and no Approve asked or given is left out; its statuses say out of date + and that a push or a recheck asks its check (ADR 0421 §10). +- Its cost is at most one carry per open verdict per trunk move, each running only the packages whose digest the + move changed; issue 944 measures it. + +### A delivery group's checks + +*ADR 0239 §14, ADR 0323 §6, ADR 0382, ADR 0403, ADR 0418 §4, ADR 0421 §2, §9.* + +- **The composed check is each member's gate.** The group's composed check composes every machine with all + members' heads, each merged into its own trunk's head first, judged by the controller built from the group's + controller member when it has one (ADR 0382 §3). Its verdict sets `mesh/delivery-group` and each member's + `mesh/merge-gate`, and names every member it read. No gate runs for a member alone. +- **Each member's own repository check stays required.** Each member's `mesh/repo-check` runs its repository's + suite with the siblings read beside it (ADR 0382 §2). +- **No composed verdict, no gate.** A member's gate is pending while no composed verdict at the members' current + heads exists. A member's new head, close or base move puts the composed verdict out of date, and with it every + member's gate. When mesh-delivery cannot say the groups, the pull request is checked alone and its verdict + says so (ADR 0382 §1). +- **A group merges in one pass, with no order.** Once the composed check passed at every member's head and the + operator's one Approve covers those heads, mesh-delivery merges every member in one pass, no member waiting for + another's merge or delivery (ADR 0418 §4). The Approve is the review ADR 0382 §6 asks: the merge tool reads its + warrant for those heads in place of an approving forge review (ADR 0421 §9). A member the forge refuses is + merged again on its fresh check while the group waits in the gate, `held`, and its source revision does not move + (ADR 0539 §17). + +### Nothing merges into a repository under check + +*ADR 0548 §2 to §4, §9, §10, as ADR 0551 bounds them.* + +**Only while the check gates merges.** The hold, the merge turns and their stalled line hold only while the +setting `check-gates-merge` of mesh-delivery-gate, set together with the forge module's, is true. While it is +false (the operator's setting since 2026-10-25: the check runs, but gates no merge), the gate writes no turn and +holds no approved merge for another delivery's running check: the merge is handed to the forge bridge at once, +the running check ends on the commit it read, and its verdict is carried (ADR 0551 §1, §2). The checker does not +read the setting: it reads `turns`, finds none, and claims its checks at once. + +- **The hold.** The gate hands no approved merge to the forge bridge while a verdict of the checker's verdicts + bucket in the state `running` names, among its trunk heads, a repository the merge goes into; the verdict of + the merging delivery's own heads excepted. The gate already watches that bucket, so the verdict leaving + `running` (a pass, a failure, its key deleted) gives the waiting merge its turn at once, with no timer and no + new event. Siblings a check reads beside are not held: the catalogue is read beside nearly every check. A merge + made at the forge by hand is not held; the check it lands under ends on the commit it read and is then carried + (ADR 0548 §2). +- **The merge turn.** Before it reads the checker's verdicts for an approved merge, the gate writes one key per + repository the merge goes into in its bucket `turns` (`turns..`): the deliveries that wait there in + the order of their Approve, the verdict key each waits for, and since when; for a delivery group, on every + member's repository from its first member handed until its last merged, so the group merges whole. The gate is + the key's one writer; the checker reads the bucket, reads it absent as empty, claims no check of a repository a + turn names, and claims on the key's delete, so an approved merge goes before a check not yet started. The key + goes when the last delivery in it merged or left (a Decline, a push, a refusal). A check claimed in the moment + between the gate's read and the merge is judged on the commit it read and carried once after, never restarted + (ADR 0548 §3). +- **The bound and the tools.** A turn waiting longer than 30 minutes is a stalled line naming the check it waits + for, when that check started and the step the checker says it is at, raised as the condition + `delivery..stalled` (ADR 0421 §7). A tool of the gate reads every turn with the check it waits for; another, + on the operator's word, hands a waiting merge at once, the check it waited for then carried (ADR 0548 §4). +- **What the operator sees.** While the check gates merges, an approved merge into a repository under a running + check merges when that check ends, at most one check's length later; `deliveries` and `show` say it is approved + and waits for the check of the named delivery on the named repository, running since a given time. While it + does not, an approved merge goes in at once and no merge waits for a check. Either way no check is thrown away + for another merge, and the Approve is asked once per set of heads (ADR 0548 §9, ADR 0551 §2). + +### The check from release 4 + +*ADR 0444, ADR 0445, ADR 0446, ADR 0447; research 058.* + +Until the status switch of release 4 (R4f) is delivered, the subsections above are the rule, with their two statuses. +From its delivery, a check is this: + +- **One status per change.** A pull request carries one required status, `mesh/check`, written by the checker + 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 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 + the check's full report, which names the modules the change moves too (ADR 0447 §3). +- **Tests by the language's convention.** The checker runs a change's tests through the build module of each + module's language (milestone 6's build modules, ADR 0405), with no manifest field and no script per + repository: in Go, `go test` is the unit level and the `integration` build tag the other, so the two levels + are apart by construction; in Node and TypeScript, the type-check and the package's `test` script, and its + `test:integration` script when it has one. A repository with no tests has none, and says so. Each level is + 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, 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 + runs what failed and what changed, never the whole suite; the change's own new and changed tests run first, + and the check stops at their first failure, before the rest and before the composition. A kept verdict is + written once, by the checker's credential alone, from the result it read, and is addressed by its digest; + each check builds with a cache of its own, so one pull request cannot poison another's verdict (ADR 0444, + issue 482). A trunk move asks no check but a carry, which runs only what the move changed (ADR 0548 §5). +- **The composition check is the assigner's.** Whether the machines still compose with a change is checked by + the part that owns what each machine should run. The assigner reads the modules, fingerprints and manifests + the indexer holds and the builds the builder holds; the machines, their roles, assignments and settings, and + the grants and credentials it reads from the grants part's state and the vault's accounts, and the rest from the buckets it + names in its manifest's `reads` (ADR 0237 §1, ADR 0408 §3, ADR 0547 §4). It clones no repository but at most the change's + own, and + composes only the machines that run or could run a module the change moves or adds, or whose grants or + providers it alters; for a group, once over every member's change, read from the indexer's candidates of the + group, and while the indexer holds none the group's check waits and says why. It composes with the function + every node agent composes with, mesh-sdk's composition, so the two agree for one mesh-sdk version; its verdict + names the mesh-sdk version it composed with. 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 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 + +*ADR 0421, ADR 0398 §3, ADR 0406 §1.* + +- **Asked as soon as the checks pass.** When a pull request's delivery reaches `ready` (both `mesh/merge-gate` and + `mesh/repo-check` passed at its head against its trunk's head), mesh-delivery asks the operator's Approve of its + merge into main at once, on a channel that proves who answered (ADR 0274; Telegram today). The ask names the + pull request, its issues, the reviewer's verdict, the modules it moves and the trunk commit the checks judged. A + verdict that names a trunk commit the trunk had already left raises no ask: the checker carries it to the + trunk's head, and the carried verdict raises the ask (ADR 0421 §1, as ADR 0548 §8 changes it). +- **A group is asked once.** One ask names every member and its head, once every member is `ready` and the + composed check passed at those heads; one Approve covers them all (ADR 0421 §2). From the delivery of issues 1781 to + 1783 the ask is the milestone's, made once when every one of its issues is ready, and a pull request linked to no + milestone is not asked (ADR 0493 §9, §11, which change ADR 0421 §1 and §2); the Approve it asks is also written + into the wait list of the session a `Waited-by: @` line on the pull request names, and only when the + pull request carries that line; without it the pull request is asked as today (ADR 0491 §3, §5). +- **The reviewer's verdict is attached, not a gate.** The review finishes before the push (playbook 07), and the + session that opens a pull request gives its review record: who reviewed it, the commit the review approved and + the verdict; from the delivery of issue 1782 a verb of the delivery seat records it at the head it judged, and no + description is read (ADR 0493 §5, §6). The ask carries it; with none, or one naming another commit, the ask says so in its first line and + the operator decides (ADR 0421 §5). +- **The merge.** mesh-delivery merges through the forge's merge tool at the heads the Approve names, only while + they are still the heads, their check passed against the trunk's head, and, while the check gates merges, no + check of other heads runs on a repository the merge goes into (*Nothing merges into a repository under check*, + above). When the trunk moved, the merge waits for the carried verdict, and the event of its line asks the merge + again; no timer asks it, no person rechecks, and the Approve is not asked again (ADR 0421 §3, §12, as ADR 0548 + §8 changes them). The merge tool still refuses a verdict naming a trunk commit other than the trunk's head and + asks nothing (ADR 0534 §4); the carry makes the verdict current. +- **A push or a Decline ends the Approve.** A new head ends its ask or its Approve, for a group every member's, and + is said once on the pull request; the new head is asked again once its checks pass. A Decline merges nothing and + is said once (ADR 0421 §6). +- **A failure is said, never merged past.** An approved head whose check failed is said once on the pull request + and listed by `stalled`; so is a merge the merge tool kept refusing for an hour (ADR 0421 §7). +- **Recorded.** The ask, the answer with its warrant and the review record are kept on the delivery, shown by + `show` and said in mesh-delivery's log; the merge's transition names the Approve (ADR 0421 §8). + +## After the merge: the source revision and the wants + +*ADR 0429 §1, §6, §11, ADR 0539 §5, §6, §8, §18.* + +- **A merge moves the source revision, and nothing collects it.** The gate writes a group's merged heads as one + revision of `source.main` in one write (ADR 0429 §11). There is no merge window, no batch and no plan: the + indexer writes each module's fingerprint at the merge, the builder claims each new fingerprint once, and the + assigner writes each want when its six rules hold. A merge is never refused, held or dropped once it is made. +- **Two merges close together are two revisions.** The second's fingerprints replace the first's in the + indexer's modules; a node gets the newer fingerprint's want once its rules hold, and a fingerprint nobody wants + any more is never applied. Nothing waits for a window to close. +- **Changes that share nothing never wait for each other.** Each module's want moves on its own build, canary + and providers, so a hotfix is never held by an unrelated feature's delivery. +- **A group reaches the nodes as one source revision.** Each of its modules gets its wants by the same rules, + and the group is delivered when its revision is (*A delivery* above). A member the forge refuses holds the + group in the gate before its source revision moves (ADR 0418 §4, ADR 0539 §17). +- **A failure is held on the canary and fixed forward.** A fingerprint that fails its health check on the + canary is held there and goes no further; the canary is set back, or for a module that keeps data the + operator is asked whether to restore it and lose newer writes or leave it stopped, and the condition names + the held fingerprint (ADR 0429 §6, ADR 0539 §18, §19). The fix is a new change; what other modules reached + stays. +- **The comparison with the send ended with the send.** From release 6 the assigner writes no difference; its + differences bucket is kept read-only as the evidence of acceptance test 17 (ADR 0539 §8). +- **Shown.** The view's wants page sets each node's want beside its have, with the controller's judgement of + the move; its tools `assigner_wants`, `assigner_want`, `assigner_differences` and `node_haves` read the same + keys, and its events `delivered`, `moving`, `settled` and `stuck` say them on the bus. + +## A backup before the apply + +*ADR 0214 §7, ADR 0233, ADR 0404, ADR 0429 §14, ADR 0539 §16.* + +Before the node agent applies a new fingerprint of a module that keeps data on its node, it asks that node's +`node-backup` seat to back up every item the module keeps there whose protection is a backup (a copy or a +dump), and waits for the backup holder's event that the backup ended, polling nothing. An item protected by +`redundancy`, one that says `backup: none`, and a cache are never asked, whatever their class. A node with no +backup holder backs up nothing, and the have says so. + +- **Nothing of the module changes before its backup.** The node agent applies the new composition only once + every backup it asked has answered done. +- **A failed backup holds that module on that node.** The module keeps running what it has; the node agent + writes the have phase `held` with the backup's own words and applies nothing of it. The assigner asks the + operator (back up again, or go on without a backup), raises the condition naming the node, the module and the + words, and on the answer writes the node's want for the module again naming it, so the node agent reads the + answer from its own want and from nothing else (ADR 0539 §16). +- **Nothing else waits.** Other modules, and the same module on other nodes, move on their own wants. +- **What it costs.** About 85% of data modules back up in under 30 s; the slowest measured, postgres on the home + server, about 165 s (research 055, finding 5). A module that keeps no backed-up item costs nothing more. + +## Compositions, wants and the apply + +*ADR 0360 §8, §9, ADR 0377, ADR 0429 §4, §6, ADR 0539 §9, §14 to §16, ADR 0547 §2 to §9.* + +- **Each node composes its own.** For each want `want..` the node agent composes that module for + its own node with mesh-sdk's composition from the manifest by digest the indexer wrote, the module's resolved + values read from mesh-config's resolved state filtered to its node, its secrets and bus credential asked of the + vault sealed to the node's key (ADR 0349 §11), its licences, and what its providers contribute; and the node's + own composition (adoption, firewall openings, accounts, the resolver, identities and groups) from the same + inputs. Of other nodes it reads only their public records and the manifests wanted on them, which its firewall + and resolver need. Nothing composed crosses the bus, so nothing composed is signed (ADR 0547 §2). +- **Composed again on events, never on a timer.** A changed input at the same fingerprint (a setting or given + value, a secret rotated or moved, a credential issued, a provider's contribution, a peer added to the hub) is + composed again when its event is heard, and applied as a revision of what runs (ADR 0547 §2). +- **Grants by the grants part.** A module's grant `grants..`, which the assigner's sixth + rule reads, is written by the grants part from ADR 0547's third milestone; its account is minted by the vault + (ADR 0547 §4, §5, §9). Until that milestone the issuer ADR 0539 §7 names writes both, so a want never waits on a + key nobody writes. +- **The bus's user list.** The grants part composes the list with mesh-sdk's bus composer when an input changes, + writes it as an object sealed to the bus node's key and writes the key `user-list` with its digest, revision, + user count and the users left out; it says `user-list-changed`. The node agent of the bus node writes the bus + module's user-list file alone, mode 0600, only when its digest differs, and the bus reloads it in place; no want + is needed, no fingerprint moves and the bus is not moved. It says the list it wrote in the bus module's have: + where it was taken from, the digest wanted and the digest on the machine, the users and any refusal. A list + without the controller's own user, or one that drops more than half the users of the file on the machine, is + refused and the file kept. The watchman mirrors `user-list` as owed by the bus node, with a pulse of 60 + seconds, so a list written and not loaded within a minute is stuck (ADR 0547 §7). Until the operator's + `take-over` the grants part runs in shadow, and the bus node takes the list from the bus module's composition + the running system writes for it, the user-list resource alone (ADR 0547 §8). +- **Memberships and assertions.** The grants part publishes each module's membership on the assignments stream + only when it differs from the one last published, and asserts every bucket, log and stream a manifest declares + when a new manifest digest is registered, with its limits; memberships are published from the first milestone + (the operator's one grant, 22:31 on 2026-10-25), while before `take-over` assertions are composed and compared + and nothing is asserted (ADR 0547 §5). +- **The want names the build.** A want `want..` names the fingerprint, the artifact's digest, the + source revision and whether the module holds data; the node's own want `want..node` names the node + (ADR 0547 §2). +- **The apply, one module at a time.** At its first start the node agent takes the declaration it kept last as + what each module runs and writes each have. Then it applies a module only when its want's fingerprint + differs from what runs or the composition it composed differs from the one it runs, and only that module, + removing only that module's own resources: a shared path stays while any composition declares it. It writes the have with the + fingerprint, the digest, the phase and the health after the module's start window. A failed apply is loud, + retried by the next change of the want or the composition, never by a clock, and fixed forward (ADR 0360 §8, + ADR 0539 §9). +- **Every want is owed.** The watchman mirrors every want as a pulse owed by that node's agent until its have + names the want's fingerprint, so a node that does not move is a stall (ADR 0539 §19). +- **A move a person must allow is judged by the node and asked by the assigner.** The node agent applies ADR 0377 + §3's tests to the composition it runs and the one it composed. For a module of policy `record`, a move judged to + recreate a container or move data, at a new fingerprint or a revision alike, is not applied: the node agent + writes its have with phase `waiting-yes` and the judgement, the assigner raises the ask, and on the operator's + yes, on the phone or in the view, writes the want again naming the yes, which the node agent reads from its own + want. A composition equal to what runs is no move and needs no ask. Each judgement and the way it went are shown + in the view and named in the ask (ADR 0547 §3). +- **The bus is never moved by the assigner.** A planned move of the bus is asked of the operator, who writes the + bus node's want with the operator's credential, recorded, until issue 1125 settles the bus's replacement (ADR + 0539 §15). A new user list is no move of the bus (ADR 0547 §7). +- **The witness stays.** The node-engine's witness still puts back a controller or tool runner that does not + come up (ADR 0360 §9). A new node-engine runs on trial under its launcher until it writes its first have under + its own build (ADR 0539 §9). + +## No stop of a shared plan + +*ADR 0539 §18, which supersedes ADR 0299.* + +With no plan there is nothing to stop and deliver again without the stopped change. A bad fingerprint is held +and its canary set back (ADR 0429 §6), and a module whose build is not done waits under the assigner's first +rule rather than named in a plan. Taking a change back off the nodes is a revert: its earlier fingerprints +have done builds and, while among their modules' five newest, kept compositions, so the assigner's wants +deliver it like any change, building nothing (ADR 0526 §1, ADR 0539 §4). + +## The delivery budget + +*ADR 0282, ADR 0304 §4, ADR 0313.* + +A delivery's **delivery time** runs from its merge until every node that runs a module it moves has a have +equal to its want, running and healthy, which the view says as `settled` for each node and module and +`delivered` for the source revision. A **leaf** delivery's budget is three minutes, a **core** delivery's ten. +A move held for a person, a failed backup among them, is shown and held to no budget. The **fix time** +(ADR 0313) runs from an issue's first move to `fixing` to its last fix delivered, and contains the delivery +time. Since release 6 nothing checks these budgets: the over-budget condition and the gate's `times` went with +the plans (ADR 0539 §17). Their check is a queued finding (issue 1945, note 10): a reader of the view's `settled` +and `delivered` events. + +## Builds without a toolchain image + +*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 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 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. 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 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. 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 + +*ADR 0408, ADR 0429 §17, ADR 0539 §4, §12, §13.* + +From release 6, module, source revision, build, want and have are one chain, each link answerable from the next +(ADR 0429 §17). Placement is the catalogue's, read onto the bus by the indexer (ADR 0549 §1, §2); the controller's +assignments bucket is kept equal to it until ADR 0547's third milestone and is a source of nothing from then +(ADR 0549 §5, §6). The rollouts bucket is kept read-only with its history: no rollout is written after release +5's last plan (ADR 0539 §12). + +| Link | Names | Written by | Bucket | +|---|---|---|---| +| build | its module, repository and commit, its reuse key, what it published: an artifact by digest or a package release by name and version | the controller | `mesh-controller_builds` | +| want | the fingerprint a node should run of a module, and the digest of the one composition it allows; the node's own want names its node composition | the assigner | `mesh-delivery-assigner_wants` | +| have | what the node runs of a module: the fingerprint, the composition's digest, the phase and the health | the node agent | `node-engine_have` | +| placement | each node's modules with their pins, host ports and bindings, from the catalogue's placement file of that node at each trunk merge of the catalogue | the indexer | the indexer's state `placement` | +| assignment | until ADR 0547's third milestone: the module and node, pinned or not, ended or not, written by `assign` and `unassign` after a merged placement change, so that it equals the catalogue (ADR 0549 §5) | the controller | `mesh-controller_assignments` | +| rollout | read-only from release 6: each plan of releases 1 to 5, its tiers, the builds it carried and the assignments it changed | nobody from release 6 | `mesh-controller_rollouts` | + +Beside the chain, on the bus too: deliveries and groups (the gate's `deliveries` and `groups`, ADR 0239 §37), one +key per open pull request (the gate), and the grants part's state (`user-list`, the memberships it last +published, and from ADR 0547's third milestone the grants). A composition is no key of the bus once nodes compose +their own (ADR 0547 §2); until then the compositions with their digests are derived state and no link of the chain +(ADR 0539 §4). The buckets of plan +state, the merge window's keys, the silent sends' keys and a building plan's hold, are deleted by the controller +at its first start of release 6 (ADR 0539 §13). + +- **The bus is the only source.** Each bucket is written by its one writer, by compare-and-set, and each part + reads the buckets it names to decide; a part resumes from its keys after a restart. No table of the + controller's store holds builds, rollouts, batches or assignments, so no record mirror, + audit or healer exists for them; the nodes, settings, declared-config and predefined-keys mirrors stay as + ADR 0289 and ADR 0356 have them. A module reads a bucket by naming it in `reads`. The assigner, the grants part, + mesh-config and the node agents read placement from the indexer's state, on its watch; from ADR 0547's second + milestone no part but the controller reads the assignments bucket, and the assigner until the third + (ADR 0549 §3). +- **No key of the chain is removed.** Each bucket keeps up to 64 earlier revisions of each key as its history. A + value is never cut to a summary and stays under the bus's largest message. The bus's storage is declared as data + protected by a backup, so it is backed up nightly and before a delivery moves the bus. +- **One read answers each question.** For any module on any node, one read of its have names the fingerprint and + the composition it runs, one read of its want what it should run, and the fingerprint names the build, its + commit and its artifact or package. +- **The tables went by a numbered migration** in milestone 5: builds, rollouts and assignments are written once into the buckets, + waiting build requests onto the work queues and an open batch into its keys; then the tables were dropped from the + controller's store and its consolidated schema. +- **The artifact store and the package registry keep the bytes**; the chain names them (ADR 0251). + +## What every transition does + +In this order, each by its owner: + +1. **kept**: mesh-delivery puts the delivery in its state by compare-and-set before anything else; what it owes + outside is kept with it and retried from ten seconds doubling to an hour, at most sixty times (ADR 0250); +2. **said**: the event `mesh-delivery.transition` (and `mesh-delivery.group` when a group's derived state + changes), with no secret or address; +3. **noted**: a line on the commit's note, appended by the forge module as the forge's own account; +4. **reflected**: the delivery's view on the pull request, `mesh/delivery` on the head and `mesh/delivery-group` + on every member's head. + +A doubled step is refused by compare-and-set and by the bus's message de-duplication, which also replaces the +forge module's files of what it announced (ADR 0406 §6). + +## The verbs + +**The gate's seat `mesh-delivery`** (ADR 0539 §17): + +| Verb | Answers or does | +|---|---| +| `deliveries` | every delivery not final, the final ones of the last day, and every one `stalled` still lists | +| `show` | one delivery or group whole: the Approve, transitions, the review record, the source revision its merge moved | +| `groups` | every group with its members and its derived state | +| `table` | the state table, with its bounds | +| `stalled` | every merge or check held past its bound | +| `recheck` | an exception verb, no step of the flow: a check asked again for a proposed delivery nothing else asks again, an inactive one or a failed verdict gone out of date (ADR 0395 §6, ADR 0421 §12) | +| `close` | the transition the table names for a stalled delivery | + +`times`, `what-if`, `release`, `stop` and `retire-history` went at release 6; each is answered that it no +longer exists. + +**The status view's tools** read the keys and change nothing: `assigner_wants`, `assigner_want`, +`assigner_differences` and `node_haves`, beside the indexer's and the builder's. Its events are `delivered`, +`moving`, `settled` and `stuck` (*A delivery* above). + +**The controller's verbs of plans** went at release 6 (ADR 0429 §8, ADR 0539 §11): `plans`, `plan`, `push`, +`deliver`, `delivery-stop`, `delivery-backups-again`, `delivery-walks`, `delivery-order` and the plan +subcommands of `upgrade`, each answered that it no longer exists by name. `bus upgrade` stays and ends by asking +the operator for the bus node's want (ADR 0539 §15). + +## The bootstrap + +*ADR 0526, ADR 0539 §9, §10, §11, §15.* + +- **The node-engine, the tool runner, the grants part and the gate are ordinary modules.** Each new fingerprint + gets its grant from the grants part, its account from the vault and its want from the assigner, and its node + composes it (ADR 0547 §2, §4, §5, §9). The node-engine's + witness puts back a controller or tool runner that does not come up (ADR 0360 §9), and a new node-engine is + rolled back by its launcher unless it writes its first have under its own build (ADR 0539 §9). The bus stays + outside the assigner (ADR 0539 §15). +- **A replaced controller or part carries on from the bus**: the keys, the streams and the durable consumers hold + every step, so nothing is lost or repeated (ADR 0406 §6). The way back to a replaced version is one want + naming its fingerprint, which its node composes again (ADR 0547 §2). +- **A build before its want.** Every module, the builder's own and the build modules among them, is wanted only + from the builder's done key whose artifacts all name a reference; a module whose key is not yet done + waits under the assigner's first rule and fails nothing (ADR 0524 §7, ADR 0539 §18). +- **History: the switch to release 6 was one plan, release 5's last** (it is delivered; ADR 0547 takes the composing, signing and issuing it gave the controller away again). Release 5's controller ran + the group's plan with the new node-engine and the new controller in one tier (issue 1942), so every node's last + declaration named the new node-engine and carried the operator's public key, and the control-node's named the new + controller too. Each node applied it with the node-engine it ran; the new controller wrote every composition at + its start, and the assigner's wants equalled what ran. For that plan one machine at a time yielded: each new + node-engine was judged by its launcher, and the new controller by the node-engine's witness. A node away during + the switch finds its last declaration on the bus's stream of declarations when it returns; the controller + deletes that stream and the nodes' consumers only once every node's have names the new node-engine. The merge + waited until every node was present (ADR 0539 §11). The switch worked only because the bus's user list that + release 5's controller composed already granted what release 6 uses: the controller writing compositions and + their digests, each node-engine reading its own compositions and wants, asking its backup holder and hearing the + backup's end, the assigner reading the digests, and the operator's account writing the hand path; that grant was + delivered first, as issue 1945's fix before the switch. The node-engine's setting `operator-signers` holds the + operator's public key, or no operator key is trusted and the hand path cannot work (ADR 0539 §3). At its first + start the new controller deleted the buckets of plan state (the merge window's, the silent sends' and the + building plan's hold) and said each deletion as its event (ADR 0539 §13). + +### 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); the assigner's want + names it and its node composes it (ADR 0547 §2). +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 part that cannot take its own fix** is put back by a want written at the terminal, never by a declaration sent by hand: the + operator's credential writes `want..` to a fingerprint whose build is done, and the node agent composes it for its node + as any want (ADR 0547 §2). Each act is recorded in the hand-act log, and the node-engine keeps no path that applies a whole + declaration (ADR 0526 §4). +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 + +What the build proves before this design is implemented (research 055's direction): + +1. A feature across the controller and the catalogue with a new seat verb is delivered as one group, with no + `after:` line, one composed check and one Approve: its merges move one source revision, each module it changes + is built once, and every want naming one of those fingerprints is written by the assigner's six rules, with + no order written by anyone. +2. An SDK change and a Go module using it are delivered as one group. +3. A data module is backed up by its node's backup seat before the node agent applies a new fingerprint of it, + and a failed backup holds that module on that node, its have in phase `held`, while every other want moves. +4. A build on any builder uses the installed toolchain with no container, and a toolchain update rebuilds only + modules of that language when its version changes. +5. Two merges into one repository within a minute leave each open pull request with one queued check, by the + stream's rule. +6. The operator's Approve is asked within seconds of the event of a pull request's checks passing, with no ready + mark, label or comment written by anyone. +7. A controller, an assigner or a node agent replaced while a source revision is being delivered carries on from + its keys: no composition, want or have is lost, and no module is applied again whose have already equals its + want. +8. Nothing collects merges: a merge moves the source revision in one write, and the assigner writes the wants it + allows with no window, no key's expiry and no timer between them. +9. A hotfix to a module that the source revision being delivered does not touch reaches the machines in minutes, + beside it: its wants move on its own build, canary and providers. +10. For any module on any node, one read of its have names the fingerprint and composition it runs, one read of + its want the same or what it should run, and the fingerprint names the source revision, the build and its + artifact or package. +11. Two merges into one repository while its modules are moving are two source revisions: a node is wanted the + newer fingerprint once its rules hold, a fingerprint nobody wants any more is never applied, and nothing is + released by a person or held in the gate. +12. No table in the controller's store holds builds, deliveries or assignments: builds are the builder's keys, + wants the assigner's, haves the node agents', and placement the catalogue's placement files, read onto the bus + by the indexer, with the controller's assignments bucket kept equal to them until ADR 0547's third milestone + (ADR 0549). + +Tests 1 to 12 are stated as they hold from release 6. Tests 1, 3, 7, 8, 9, 10 and 11 were first proved by +milestones 1 to 5 in the shape of their time, the controller's plans and what it sent (the milestones table keeps +it). Restated, each has its own proof, or says it has none yet: + +- **Tests 1 and 8, and the assigner's half of test 7:** test 18 (scenario A, three repositories through the keys + alone; scenario D, the assigner replaced, in the assigner's integration test + `TestTheAssignerReplacedResumesWithNothingLostOrRepeated`). **Tests 8 and 12:** test 19 (the window, the batches, + the plans and their verbs deleted from the code). +- **Test 3:** the node-engine's integration test `TestNatsADataModuleAsksItsBackupFirstAndIsHeldWhenItFails` + (`cmd/mesh-host/nodeagent_integration_test.go`, R6-A AC5) proves the backup first and the hold with phase `held`. + That every other want moves meanwhile is not yet proved by a test; it shows on the live mesh at the first failed + backup, in the view's wants beside the haves. +- **Test 7, the node agent's and the controller's halves:** a node agent restarted on its own haves applies nothing + (`internal/agent/agent_test.go`, `TestStartingOnItsOwnHaveAppliesNothing`, at the unit level), and a controller + writes a composition once and again only when it changes (`composed_writer_test.go`, + `TestACompositionIsWrittenOnceAndAgainOnlyWhenItChanges`). A controller replaced while a source revision is being + delivered is not yet proved; it shows at the next delivery of the controller, in the view. +- **Test 9:** not yet proved under release 6. Each rule holding a want back on its own is a unit test of the assigner + (`internal/assign/wants_test.go`, `TestEachRuleHoldsTheWantBack`); the hotfix beside a moving source revision shows + live at the first such pair, read in the view's wants and haves. +- **Test 10:** a live read on 2026-10-26 at 01:20: one node's have of `avahi` named its fingerprint and composition digest, + its want named the same, and the builder's build of that fingerprint (`builder_build`) named its artifact by digest. + In all, 303 wants equalled their haves in fingerprint and digest. +- **Test 11:** half proved. The node agent applies only the composition its current want names + (`internal/agent/agent_test.go`, `TestTheRevisionAWantNamesIsAppliedFromHistory`); two merges of one repository + while its modules move are not yet proved live, and show at the first such pair in the view. + +From ADR 0429 (research 063's done-when, and the live proof each of its releases names): + +13. The status view shows every module on every node from the have keys, and agrees with the controller's own + read of the nodes' reports. +14. A part stopped by hand shows stuck within two minutes, and no part holds a timer. +15. A group of two repositories merges as one source revision, and every module's fingerprint the indexer + writes equals the controller's source fingerprint. +16. A merge builds nothing its check built, and a hotfix's build waits for no other build. +17. A day of deliveries in release 5 showed no difference between the assigner's wants and what the controller + sent that the six rules then in force did not explain. That was proved in R4g; the differences bucket is kept + read-only as its evidence (ADR 0539 §8). +18. On the live mesh a milestone of three repositories reaches every node through the keys alone (research 063's + scenario A), and the assigner replaced in the middle of it resumes with nothing lost or repeated (scenario + D, once). +19. Plans, batches, the merge window, tiers, the plans hold and node declaration holds, the push of whole + declarations, their retries, takeovers and healers, and the node-engine's handling of whole declarations are + deleted from the code, and no verb that starts, stops, retries or closes a plan remains. +20. Each release's pull request reports the controller's code lines against research 063's baseline (111,386 + lines on 2026-10-18). + +From ADR 0444 to ADR 0447 (research 058's conclusion: a small fix delivered within four minutes after +development, the check's share about a minute): + +21. A leaf fix's check takes about a minute, measured as research 058 measured it: the check's log from its + first line to its verdict, plus the time from the change's announcement to its verdict in the delivery, + each part's time as the status's line says it. +22. A change that adds a failing test is answered failed with only that test's package run; a check asked again + after one failed package runs that package alone; a check whose inputs equal an earlier one's runs nothing + and takes every kept verdict. Measured on the status's line: the tests run and kept at each level, and the + failed test it names. +23. A library or catalogue change's composition check composes only the machines it touches, and clones no + repository but at most the change's own. Measured in the check's report: the machines composed, against the + machines that run or could run a module the change moves or adds, and the repositories cloned. +24. A pull request carries one required status, and a delivery group one verdict on every member's head, its + line naming what was proved and each part's time; no repository a module is built from holds a + `merge-check.sh`. + +From ADR 0548 and ADR 0551 (issue 568): + +25. **One check per delivery.** A delivery group of two repositories is under check, and a lone pull request into + one of them is approved during the run. While the check gates merges, it waits, `mesh-delivery.show` says + what it waits for, and it merges when the group's verdict is written; while it does not, it merges at once and + the group's running check ends on the commit it read. Either way the group's verdict is then carried to the + new trunk commit with no test run for an unchanged digest, and the group merges on its Approve with no second + check. Read from `mesh-delivery.show`, the checker's verdict (`carried-from`, the levels' run and kept counts) + and the forge's statuses; issue 568 is resolved only once this has passed on the mesh (ADR 0548 §10, ADR 0551 + §2). + +## What retires + +- The "release branches" and the controller's judge of them (ADR 0358, superseded by ADR 0401). +- The toolchain image and its module (ADR 0405). +- The window cutter, the merge catch-up timer, the rollout ticker, mesh-delivery's tick, reconcile and + ready-mark poll, the forge's poll as a trigger, the queue reordering, the forge-asking cancel, and the forge + module's announcement files (ADR 0406, ADR 0407). +- The ready mark: the label `mesh/ready`, the `ready at` comment, `mark-ready`, the forge module's + `ready-authors` setting, and mesh-delivery's subscriptions to labeled, unlabeled and commented events + (ADR 0421 §4, §11); a recheck by hand as a step of the flow (ADR 0421 §12). +- A check ended or asked again for a trunk move, and the re-ask of every open pull request's check at each move + (ADR 0395 §1, §3, as ADR 0548 §1 and §5 change them): a running check ends only at a new head, and a trunk + move carries a verdict by its package digests. +- Placement written only by the controller's `assign` and `unassign` (ADR 0539 §12, as ADR 0549 §1 changes it): + from ADR 0547's third milestone its tables and verbs are a source of nothing (ADR 0549 §6). +- A group's order: `after:` lines, the ranked rules and the order of the repositories' names (ADR 0419, which + supersedes ADR 0249). +- Delivery member by member, a stop of a member after a failed one, and the own path's separate batch for a + 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 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 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. +- At release 6's delivery (ADR 0429 §7, §8; ADR 0539): delivery plans, batches, the merge window, tiers and the + writing of rollouts; the plans hold and the node declaration holds; the push of whole declarations, its + retries, takeovers and healers; the controller's verbs `plans`, `plan`, `push`, `deliver`, `delivery-stop`, + `delivery-backups-again`, `delivery-walks`, `delivery-order` and the plan subcommands of `upgrade`; the + planner's edges and its engine-first default; the backups before a send; the gate's delivery states + (delivering, the backup hold, superseded, covered, stopped, retired and failed), its machine steps and its verbs + `times`, `what-if`, `release`, `stop` and `retire-history`; the messenger's hold on a plan, which holds on the + view's events instead; the assigner's comparison with the send; the node-engine's queue of whole + declarations, its orders, put-backs and five-minute reconcile; the buckets of plan state; the stop of a shared + plan (ADR 0299, superseded by ADR 0539 §18); and a declaration sent by hand (ADR 0526 §4, changed by ADR 0539 §10). + +## Milestones + +Milestones 1 to 6 were confirmed by the operator on 2026-10-17 (playbook 04), and 5b on 2026-10-18. Releases 1 +to 6 of ADR 0429 follow milestone 5b in its order, releases 1 and 2 first confirmed as one milestone (ADR 0429 §7 allows it: +they need nothing of each other) and then split by their release orders into R1a to R2d (below), and milestone 6 folded into release 4: confirmed by the operator on 2026-10-18 +(playbook 04 step 5). The issues of R1a to R2d are opened; each later release's are opened when it is prepared. Each milestone is one release of playbook 07; the next starts when the +previous one's delivery is done, and a release's clauses of ADR 0429 hold from its delivery (§21). Every +release reports the controller's code lines against research 063's baseline (acceptance test 20). + +| # | Outcome | Acceptance tests | Repositories | Issues | +|---|---|---|---|---| +| 1 | A merge during a running delivery is never dropped and needs no person's release; the merge window lives on the bus; a rollout that shares nothing with an open one runs beside it | 8, 9, 11 | mesh-controller | 973 (with the rest of 525) | +| 2 | A ready mark is acted on within seconds, and each pull request has at most one waiting check | 5, 6, 7 | mesh-controller, mesh-catalog (mesh-delivery, gitea) | 974, 975, 976 | +| 2b | build-agent and route-proxy live whole in mesh-controller (ADR 0420, accepted 2026-10-17): the controller accepts a reviewed move, mesh-delivery announces each merge with its Approve, and the two modules move; built as two releases before the rule that a milestone is one release | — | mesh-controller, mesh-catalog (mesh-delivery, build-agent, route-proxy) | release 1: 1025; release 2: 1019 | +| 3 | A feature with a new seat verb across the controller and the catalogue is one group, one batch, one delivery plan and one Approve: no order lines, no ready mark, each module built and sent once | 1, 6 | mesh-controller, mesh-catalog (mesh-delivery, gitea) | 977, 978, 1022, 895 (with 1046 folded into it: unit and integration tests split, each level within its bound); 605 (1021 closed as its duplicate) is resolved by this build (ADR 0419 §7) | +| 4 | A delivery backs up the data it moves first, and a failed backup holds it and is said | 3 | mesh-controller, mesh-catalog (mesh-delivery, the backup seat's holder) | 980, split into 1065, 1066, 1067, 1068 | +| 3 fix | A group approved together merges in one pass and goes out in one delivery plan, also when its members' tests read each other's repositories | 1 | mesh-catalog (mesh-delivery, gitea) | 1082, 1049, 1047, 1083, 1079 (1084 closed as its duplicate) | +| 5 | One read of a module on a node names the commit, build and rollout that put it there | 10, 12 | mesh-controller, mesh-catalog (mesh-config, nats) | 981 | +| 5b | Decided: building and delivering leave the controller for parts that watch and write keys in the repository `mesh-delivery`, landing in six releases (ADR 0429); the forge module makes that repository from its config, protected before any team reaches it (ADR 0428) | — (ADR 0429 names them: 13 to 20) | hq, mesh-catalog (gitea) | 1117, 1124, 1128 | +| 6 | Folded into release 4 by ADR 0429 §10 (the fetch over SSH in R4c, the cut-over in R4e): installed toolchains, builders fetching over SSH with their own key, and an SDK change shipping with its users are built in the builder's new home, so the builder is changed once | (2: proved by R4e; 4: in part by R4e, wholly by R4g) | — | 979, 971 (moved to release 4) | +| R1a | The node-engine, the controller and the parts can read one layout of the have, presence, pulse and stuck keys: mesh-sdk ships it before its users | — (13, 14 need it) | mesh-sdk | 1131 (key layouts) | +| R1b | Every node writes what it runs of each module, on its own keys and nobody else's, and the bus says to one part when a connection opens and closes | 13 in part | the node-engine (`mesh-host`), mesh-controller | 1132 (have keys), 1142 (the node-engine's check, its tests half), 1136 (the have bucket and its grants), 1145 (a module and a build per resource), 1146 (a new repository's first module registered at its merge's Approve), 1147 (the bus's connect events for the watchman), 1160 (a plan passes a machine silent at a middle tier), 1162 (the have bucket takes a node's facts) | +| R1c | The operator sees every module on every node, with its build, phase and health, in `mesh-delivery-cli` (aliased `nox-delivery`) and its web view | 13 | mesh-delivery (view) | 1134 (the view), 1137 (the alias, in the view's manifest) | +| R2a | The watchman and the controller can read one layout of the stuck keys: mesh-sdk ships it before its users | — (14 needs it) | mesh-sdk | 1153 (the stuck keys) | +| R2b | A part, node or check that stops moving is raised as a condition within two minutes, with no timer in a part | 14 | mesh-delivery (watchman), mesh-controller | 1135 (the watchman), 1154 (a bucket whose keys expire), 1155 (running checks readable, with their start), 1156 (stuck keys raised; the silent check from presence), 1157 (a refused send key raised), 1159 (a long check is not stuck) | +| 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 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) | +| R4a | The shared layouts of release 4 are on mesh-sdk's main before their users pin them: the catalogue, the validator and the composition over a plain input, the fingerprint's new form, and the key layouts of the builder, the checker, the kept verdicts, the manifests and the candidates; the node-engine reports the user namespaces capability (ADR 0453 clauses 1, 2, 3) | — (2 needs it) | mesh-sdk, the node-engine (`mesh-host`) | in mesh-sdk: 1353 to 1364, 1298 (the composition over a plain input); in the node-engine: 1308 (the user namespaces capability) | +| R4b | mesh-controller and the node-engine use the catalogue, the validator and the composition from mesh-sdk and delete their copies; the controller composes every node both ways on the live facts and the two agree before this is delivered; it writes the grants and credential bindings the assigner reads; its unit level meets its bound (ADR 0453 clauses 1, 3) | — | mesh-controller, the node-engine (`mesh-host`) | 1375 (to be split when it is prepared), 1337 (the unit level's bound) | +| 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 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 the compositions its own node agent composes (ADR 0547 §2; as released it applied the controller's signed ones), a new version reaches the module's canary first and the rest once it is healthy there, a move a person must allow is asked before its want, the view says what is moving, settled, stuck and delivered, and the plans are deleted from the code; switched over by release 5's last plan (ADR 0539) | 18, 19, 20 | mesh-sdk, the node-engine (`mesh-host`: the node agent), mesh-controller (plans deleted; as released also compositions and credentials, which ADR 0547 moves to the node agents, the grants part and the vault), mesh-delivery (assigner, watchman, view), mesh-catalog (the gate, the messenger, the issue tracker, the backup holder), hq | 1942 (the planner puts the node-engine and the controller in one tier for the switch; delivered first), 1945's fix before the switch (the bus's user list grants what release 6 uses; delivered first), 1943 (mesh-sdk: the composition's layout and signature, the want's digest, `mesh-hand-compose`, the verb tables without the plan verbs), 1944 (the node agent), 1945 (as released the controller writes compositions and grants, which ADR 0547 moves; the plans and the push deleted), 1946 (the gate keeps the merge's states; the messenger and the issue tracker hear the view), 1906 (the assigner's seventh rule and its asks, the watchman's pulses for wants, the view's wants and events), 1130 (hq) | + +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 is milestone R6, prepared on 2026-10-24 with ADR 0539. Its prepare step found six repositories where its row first +named three: the gate's code is in mesh-catalog, mesh-sdk holds the composition's layout and the seat verb tables, and hq the design +and the glossary. + +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 +operator on 2026-10-17), so the unit level holds its bound of ADR 0391; the integration level's sixty seconds +is issue 1055. Acceptance test 6 spoke of a ready mark when milestone 2 was delivered; restated for +the Approve, it is proved again by milestone 3. + +R4's prepare step on 2026-10-20 cut release 4 by its orders into R4a to R4g, each one release (ADR 0453, the operator's Approve): the SDK ships before its users, the new parts run dark before anything relies on them, the status switch follows a measured run, and the scripts and build-agent go last; +R4d, the four image reworks, has no order of its own and may be delivered at any point between R4a and R4e. Where a record dates a rule by release 4, it reads the R4 release that builds the rule (ADR 0453 clause 15). + +## What is not decided here + +- A web view of deliveries beyond the pull request's comment. +- Whether `mesh/delivery-group` is required on the trunks: the operator's setting, through the forge module's + protection tool. +- How a builder's public SSH key reaches the forge: the forge's and the SSH client's mechanism. diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/hq/03-DESIGN/01-to-be/53-the-issue-tracker.md b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/hq/03-DESIGN/01-to-be/53-the-issue-tracker.md new file mode 100644 index 00000000..dc1f155a --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/hq/03-DESIGN/01-to-be/53-the-issue-tracker.md @@ -0,0 +1,319 @@ +--- +layer: to-be +status: in-progress +code: [mesh-catalog modules/mesh-issues, mesh-catalog modules/claude-code, mesh-catalog modules/messenger, mesh-sdk] +updated: 2026-10-11 +decisions: + - "ADR 0275" + - "ADR 0278" + - "ADR 0279" + - "ADR 0281" + - "ADR 0284" + - "ADR 0285" + - "ADR 0291" + - "ADR 0294" + - "ADR 0300" +--- + +# 53 — The issue tracker + +**The mesh keeps its issues in a module of its own, `mesh-issues`, one instance on the control-node, holding the seat +`issue-tracker`. It hands out every +issue's number from one counter that two callers can never both advance, holds each issue's status and links, refuses +a change the development cycle would refuse, and announces every change on the bus. An issue lives whole in the tracker, its report, diagnosis and +resolution included; hq keeps no issue content.** +(ADR 0275, +from research 042; +ADR 0291 for an issue whole in the tracker.) + +An **issue** is a question about the mesh that someone must answer. The **issue tracker** is the module that holds +issues, named `mesh-issues`; the seat it holds is `issue-tracker`. The glossary holds both words. + +**Acceptance.** An agent opens an issue and gets the next number; two agents opening at the same moment never get the +same one. + +This document designs slice 1 in detail. The slices after it are listed in [§ 8](#8-the-slices-after-the-first), +each with what the operator sees when it is done. + +## 1. The parts + +``` + control-node + + mesh-issues (seat issue-tracker, held once for the mesh) + ├ its process, launched by the node runtime + ├ bucket issues one key per issue, by number; a short history per key + ├ bucket counters one key per sequence; slice 1 has `issue` only + ├ bucket grooming the backlog pass of the duplicate check (§ 9) + ├ setting owners the registered list `located-in` may name + ├ verbs open, get, list, move, note, link, import, groom, set, tracking, + │ served by the control-node's tool runner + ├ events opened, moved, noted, linked, imported, set, triaged, ready, ready-to-verify + └ consumes its own `opened` and `moved`: the triage (§ 9); + mesh-delivery's `transition`: when a fix is delivered (§ 10) + + claude-code (on the same node) + └ claude_code_judge one question answered by the agent with no tools (§ 9) + + any agent session, on any node + mesh MCP server ── mesh_search, mesh_describe, mesh_call ──▶ the verbs, over the bus +``` + +The module is written in Go against the SDK. It is built on the model of hits, a headless issue tracker on NATS, and +carries none of its code; its README says so. + +## 2. The seat and its verbs + +The module declares the seat `issue-tracker` at mesh scope and claims it exclusively +(ADR 0126): there is one tracker per mesh. Its verbs are +the seat's tools, served through the tool runner and announced on the bus +(ADR 0197), so an +agent finds them with `mesh_search` ("open an issue") and calls them as `issue-tracker.`. + +| Verb | Takes | Does | +|---|---|---| +| `open` | a title, the report as text, optionally `located-in` and a folder path | checks the issue, then takes the next number and writes it; returns the number. Refuses an empty title or report, and an owner not on the registered list | +| `get` | a number | the issue whole: fields, notes, links | +| `list` | optionally a status, an owner, a phrase | compact rows, newest first, short enough to read in a chat: number, status, owner, title, and the worker with since and whether it is stale | +| `move` | a number, a status, and what that status needs; `by` for `diagnosing` and `fixing`, and `take-over` with a reason to take an issue over from another worker | changes the status under the rules of § 5 | +| `note` | a number and a text | appends to the issue's notes; a note is never edited | +| `link` | a number, a kind and a target, or `remove` | adds or removes a typed link | +| `set` | a number and any of `severity`, `needs`, `suggested-owner` | changes them as a person decides (§ 10) | +| `tracking` | optionally condition keys | for every condition key an issue links, the issues that link it and where each stands: status, needs, worker, fix (§ 12) | +| `groom` | optionally `confirm` and a pass's id | the duplicate check over the backlog (§ 9): without `confirm`, starts a dry run in the background and answers its id; with the id alone, its progress and findings; with `confirm` and the id, applies exactly that pass's findings, each checked again | +| `import` (slice 3) | optionally `dry-run`, a commit | reads every issue folder of hq through the records module and writes each as an issue at its own number; creates what is absent, fills a status the tracker holds empty, never overwrites, never advances the counter; run again, changes nothing and says so | + +Each verb's description says in plain words what it does and what it refuses, without the mesh's internal names, so +an agent picks the right one from `mesh_search` alone. A refusal says what was wrong and what would be accepted. + +**The caller is the bus credential, never an argument.** The runtime hands the module the identity of each call's +caller; the module records that identity as who acted. No verb takes an actor. `by` on `move` is not one: it names +the issue's worker, who works on it, as the worker states it +(ADR 0284). + +## 3. What one issue holds + +- `number` (its key), `title`, `status` (`open`, `diagnosing`, `located`, `fixing`, `resolved`, `wontfix`), `opened`, + `closed`; +- `worker`, while it is `diagnosing` or `fixing`: who works on it as who@node, `since` when, and `active`, the last + time a caller did anything on it (§ 5); +- `located-in`: entries of the registered list of owners (repositories and modules), never free text; +- `fixed-by` (pull requests or commits), `amended-design`, `replay` or `replay-none`, and a reason for `wontfix`; +- `notes`: appended, each with who wrote it, when, and the text; +- `links`, typed: to another issue (`duplicates`, `relates-to`), a pull request, a decision record, a design, a + condition key (the issue **tracks** that condition, § 12) or a delivery; +- `documents`: the report, and a diagnosis and a resolution when written, each with its author and date, revised in + place with its history kept (ADR 0291); +- `record`: the path of the issue's folder in hq, while one exists; none is opened any more, and the folders are + imported into `documents` and removed (§ 8, slice 6); +- `severity` (critical, major, minor), `needs` (none, decision, operator), `suggested-owner`, `delivered` (the + pull requests of `fixed-by` the delivery said are delivered) and `triage` (the triage's judgement as made), and the link + kind `depends-on` (§ 10). + +**The registered list of owners** is a setting of the module, changed through the controller's settings verb like any +other module setting. Slice 1 starts it with the repositories in hq's `00-META/repos.md` and the modules the catalogue +registers. + +**An issue that has a folder in hq** kept its status in that folder's frontmatter until the import of slice 3 ran; +until then the tracker held its number, title and `record`, took its notes and links, and `move` refused it with the +reason: its status lives in its folder. Since the import, every issue is whole in the tracker, folder or not, and a +report in hq carries no status key. No status is written in two places for one issue. + +## 4. State, the counter and events + +**Buckets the module declares, the controller creates** +(ADR 0201). +The manifest's `state` names two buckets, `issues` and `counters`, with their options: `issues` keeps a short history +per key, `counters` one value per key, neither expires. The module never creates a bucket. One issue is one value; at +the 256 KiB a value may be, a note that would push an issue past it is refused with that reason (the largest issue +folder in hq is 25 KiB). + +**The counter is advanced by compare-and-set.** Beside `get`, `put`, `delete`, `keys` and `watch`, the runtime's state +verbs hold two writes that are refused when another writer was first, in the runtime and the SDK's state interface +together (ADR 0259): + +- `create` writes a key only if it does not exist; +- `update` writes a key only if its revision is still the one the caller read, and returns the new revision. + +The SDK's `Increment` reads a counter and writes the next number with `create` or `update` at the revision it read, +trying again a bounded number of times when another writer was first. The counter is started once, by `create` at +the number before the first, so it never starts twice. `open` then runs in this order: + +1. validate the issue; a refused issue spends no number; +2. read `counters/issue` and its revision; +3. `update` it to the next number at that revision; if another caller moved it first, the write is refused, and + `open` reads again and retries, a bounded number of times; +4. `create` the issue's key; an existing key is a fault the module reports loudly and never overwrites. + +So the counter does not rely on there being one instance: two instances, or two calls racing in one, still never hand +out one number twice. **The counter starts** above the highest issue number on `main` and on every open branch of hq +at the moment the module is switched on. Numbers already skipped stay unused, and a number never moves. + +**Every change is an event the module declares** under `emits` +(ADR 0041): `opened`, `moved`, `noted`, `linked`. Each carries +the number, what changed, and who changed it, so the events are the audit trail, and anything watching the bucket or +the events sees a change as it happens. `moved` also carries the worker a move names, as `by`, with `since`, and who +acted as `caller`; on a take-over `took-over-from` and the reason, and `released` when a move ends the work. No +event carries a secret, and an issue holds none. + +## 5. The cycle's rules, at the write + +`move` refuses what `cycle.py` refuses at merge today, and the rules playbook 03 states: + +| The issue moves to | Needs | +|---|---| +| any status | one of the six | +| `diagnosing` or `fixing` | `by`, the worker as who@node; refused while another worker is current, naming them, unless `take-over` gives a reason, which is noted; a worker with nothing done on the issue for `worker-stale-after` (24 hours) is stale and may be taken over from without one | +| any other status | no `by` and no `take-over`; the move ends the work | +| `located` | `located-in` names at least one registered owner | +| `resolved` | the issue is at `fixing` (or already `resolved`, to correct what it holds); `located-in`, and `fixed-by` saying what fixed it | +| `resolved`, for a core issue (its `located-in` names the controller, the node-engine, the tool runner, the SDK, or the catalogue's bus, forge or build agent) | `replay` (an id from mesh-lab's replays register) or `replay-none` saying why none is possible (ADR 0237) | +| `wontfix` | a reason | + +A refused move changes nothing and says which rule refused it. The rows for `diagnosing`, `fixing` and `resolved` are +ADR 0284's: +resolved only from `fixing` is the check that an agent said it started the fix. `cycle.py` checked the folders that carried status until +slice 3; since then it refuses a status key in any report, and judges no issue's status. + +## 6. How the operator and agents use it + +Through the mesh's tools first. An agent opening an issue calls `issue-tracker.open`, gets the number, and, for a design +or governance issue, writes `04-ISSUES/NNN-slug/` under it with the number in the report. Playbook 03's first step +changes to that the day slice 1 is live; nothing else in playbook 03 changes in slice 1. `list` answers "what is open" +in one call, from any session, and the responder on a channel answers from it once the seat exists. + +**An agent says when it starts.** Before it starts work on an issue, an agent moves it to `diagnosing` (to find where +it lives) or `fixing` (to fix it) with `by`, its own name and node. `list` then shows who works on what, and a second +agent is refused before it starts the same work. An agent that keeps working notes what it finds, which keeps it the +worker; one that stops moves the issue on, or is stale a day later. + +## 7. How it is checked + +| Rule | Checked by | +|---|---| +| Two opens at once never share a number | the module's test over a real bus: many concurrent `open` calls, from two instances, yield distinct consecutive numbers; the runtime's test that `update` at a stale revision is refused | +| A refused open spends no number | the module's test: an invalid open, then a valid one, gets the next number | +| An issue's key is never overwritten | the module's test: a `create` on an existing key is refused and reported | +| The cycle's rules hold at the write | the module's table test over § 5, one case per row, each refused case changing nothing | +| The module creates no bucket | the runtime refuses a module that tries (ADR 0201 §3); the controller's raise asserts both buckets | +| The caller is who acted | the module's test that the recorded actor is the call's identity, whatever the arguments say | +| A worker is named on a move to diagnosing or fixing, another is refused while it is current, a take-over is noted, a stale one may be taken over from, and resolved is reached only from fixing | the rows of ADR 0284's "How it is checked" | +| An issue's delivery line follows the delivery; ready to verify once; a stuck fix said once and cleared | the rows of ADR 0281's "How it is checked" | +| A linked pull request is a fix; a closed unmerged one is withdrawn; delivered and not resolved is said once; an abandoned issue is flagged | the rows of ADR 0294's "How it is checked" | +| The triage fills only what is empty, links no cycle, and an issue is ready only when its dependencies' fixes are delivered | the rows of ADR 0279's "How it is checked" | +| The duplicate check acts only on a clear duplicate, never on an older or a resolved issue, once per issue, loudly when it cannot judge | the rows of ADR 0278's "How it is checked" | +| An issue's documents are kept whole, with author, date and history, and the import of hq's folders loses nothing (ADR 0291) | the module's tests of `documents` and of the import, one case per document kind; the comparison of every issue with its folder before the folders are removed, written on issue 427. Tracked by issue 427 | +| Live | on the mesh, an agent calls `issue-tracker.open` and gets the next number; two sessions on two nodes open at the same moment and get two different numbers; `list` shows both | + +## 8. The slices after the first + +| Slice | Delivers | Done when the operator sees | +|---|---|---| +| 1 | The `mesh-issues` module: `open`, `get`, `list`, `move`, `note`, `link`; a counter by compare-and-set in the SDK; playbook 03 takes its numbers from it | an agent opens an issue and gets the next number; two agents opening at the same moment never get the same one | +| 2 | Decision and research numbers from the tracker, each its own key in `counters`; a number is reserved when a branch starts; the knowledge base's `reserve` asks for them ([to-be 55](55-the-knowledge-base.md) § 5). Tracked by issue 424 | two agents writing a decision at once get two numbers, and no hq pull request is renumbered | +| 3 | `import`: every issue folder in hq read through the records module and written as an issue with its own number, idempotent and showing what it would change before it does (built and run 2026-10-10; one pull request changed playbook 03 and `cycle.py` and added dated notes to ADR 0019, 0080 and 0275, and once every folder was checked against the tracker a second removed the status keys from every report, as § 3 says); then the four consolidation passes of research 042 (fixed, duplicates, owners, grouping; the duplicates pass is the duplicate check's `groom`, § 9, judged rather than by shared words), each a list confirmed before it is applied through `move`, `link` and `note`, and an audit of `fixed-by` against the forge | the not-closed list comes from one call and shrinks by what was already fixed or duplicated, each with its reason | +| 4 | Conditions and pull requests linked: an issue links a condition by its key, and every message about the condition says the issue and where it stands (§ 12; the controller does not show the link, ADR 0285); a merged pull request notes every issue it names; after a week of that, the controller opens one issue per `healer-wanted` condition key by itself | a condition's message names its issue, and a merge notes the issues it names | +| 5 | A read-only view onto the `issues` bucket over the bus | the operator reads issues in a browser without an agent | +| 6 | An issue whole (ADR 0291): the `documents` of § 3; an import of the 361 folders of hq into them, repeatable, keeping each issue's number and dates; once every issue is compared with its folder, one pull request rewrites the 765 path mentions in hq to "issue N", removes the folders, makes `records.py` refuse a new path into `04-ISSUES/`, and changes playbook 03; `open` loses `record`. Tracked by issue 427 | `issue-tracker.get` answers an issue's report and diagnosis, and hq has no `04-ISSUES/` | + +Once slice 1 runs, each slice after it is opened as an issue in the tracker, so the tracker carries its own work. + +## 9. The duplicate check + +**When an agent opens an issue that repeats one not resolved, the tracker closes the new one as its duplicate +within a few minutes, keeping its report on the older one; an issue that repeats nothing gains a note that it +was checked** (ADR 0278). + +- **Started by the tracker's own `opened` event**, which the module consumes. Only an issue still at `open` is + checked, against every older issue at `open`, `diagnosing`, `located` or `fixing`; resolved and wontfix issues are never + offered or changed. +- **Judged in two questions** to the agent module's `claude_code_judge` on the tracker's node: a shortlist of at + most five by titles, then a verdict on the reports (`duplicate`, `related` or `none`, whether it is certain, + and why). The judging session runs as the node's agent account under its licence, with no tools, in safe + mode, and answers within 25 seconds. +- **A certain duplicate**: the new issue is linked `duplicates`, its title and report become a note on the older + one, its verdict a note on itself, and it moves to `wontfix`, "duplicate of N". **Related, or not certain**: a + `relates-to` link and a note on both. **None**: one note. The older issue never moves. Each step goes through + the tracker's own `note`, `link` and `move`, and is recorded as by `duplicate-check`. +- **Once per issue.** The verdict note is written first, in a fixed first line; an event delivered again does + only the steps not yet done. +- **Loud when it cannot judge.** One note on the new issue, and the event is not taken: after five deliveries + the controller raises `max-deliveries` on the tracker's consumer. +- **The backlog**: `groom` runs the same judgement over every issue not resolved, as a dry run kept in the state + `grooming`, then applies that pass's findings on `confirm`. +- Settings: `duplicate-check` (`on`) and `judge-model` (`sonnet`). + +## 10. Triage, severity and readiness + +**An agent opening an issue need not know the backlog: the triage estimates its severity and what it needs, links +what it depends on, and the issue is said ready when every fix it waits for is delivered** +(ADR 0279). + +- **Triage** is the judgement of § 9 widened: its second question is always asked and also answers severity, + needs, a suggested owner when the issue names none, and the shortlisted issues it depends on or relates to, each + with a one-line reason. The judgement is kept on the issue first, then applied through the tracker's own verbs: + one note saying it all, each empty field filled, each link added (never one that closes a cycle, never to a + resolved or wontfix issue), the duplicate verdict last. It fills only what is empty; a value a person gave + stands, and a disagreement is a line in the note. Recorded as by `triage`. +- **Severity**: critical (something the operator relies on is broken, or data is at risk), major (a rule of the + mesh does not hold, or people work around it), minor (a contained fix; nothing the operator sees changes). + **Needs**: none, decision (a decision record first), operator (the operator's answer is required). +- **`depends-on`**: refused when it would close a cycle; `get` shows what an issue depends on and what it is + needed by. +- **Ready**: not resolved or wontfix, and every dependency wontfix (one closed as a duplicate counts as its + original), or resolved with every pull request its `fixed-by` names delivered; said once per spell. The tracker consumes `mesh-delivery.transition` and, at its own `moved` to resolved, asks + `mesh-delivery.checks` for what it has not heard; an issue made ready gains a note and the event `ready`. +- `open` takes severity and needs; `set` changes severity, needs and suggested owner; `list` filters on severity, + needs and ready. `groom` triages the backlog, dry run first. +- The values are optional keys of an issue's one JSON value: an issue kept before reads as having none, and the + key-value store has no schema to migrate. + +## 10a. The record is a log of operations + +**The tracker's record is its log of operations, and its bucket is that log's fold** (ADR 0300, resting on ADR 0297): +every write is one operation appended under the issue's number before the bucket is written; the bucket is rebuilt +from the log on start; the events announce and the log is the audit trail; `list` answers from memory with a ranked +search and a `before` cursor. Built under issues 501 and 502. + +## 11. Whether the fix is delivered + +**An issue knows whether its fix is on the machines, as the delivery says it; delivered is not resolved, and a fix +stuck after its merge is said** (ADR 0281). Since ADR 0294, an issue's fixes are the pull requests `fixed-by` +names and every pull request linked to it; a pull request closed without merging is withdrawn; an issue left in +fixing an hour after its last fix is delivered is said once, "fixed and delivered, not resolved"; and a stale worker +with no pull request open is **abandoned**. + +- For each pull request `fixed-by` names, the tracker keeps the state `mesh-delivery.transition` last said — merged, + held and why, covered, delivered — or asks `mesh-delivery.checks` once for one it has not heard of. Nothing sets it + by hand. +- `get` and each row of `list` carry **delivery**, one line: "fix not delivered: …", "held: …", "fix merged, not + delivered", "fix not merged", "nothing heard …", "delivered to every machine it should reach". +- When every fix of an issue not yet resolved is delivered, it gains a note and the event `ready-to-verify`, once; + it is resolved after the acceptance is proven live. A fix is named while the issue stays `fixing`: `move` to the + same status, with `by`, changes what the issue holds. +- A fix merged and not delivered after `stuck-after` (two hours) is said once: a note on the issue and a warning on + the operator's channel, cleared when it is delivered. It is not a controller condition: a module that runs only + its bundle cannot raise one. + +## 12. Which issue tracks a condition + +**Every message about a condition says which issue tracks it and where that issue stands, or that none does** +(ADR 0285). + +- **An issue tracks a condition** through a link of kind `condition` naming the condition's key exactly; set by + whoever opens the issue or works on it, never by the triage or the controller. An agent that opens an issue for a + condition links its key at once, and one that silences a condition because an issue tracks it links that issue + first. +- **`tracking`** answers, for every key an issue links or only the keys asked, the issues that link it, those not + closed first, at most 10 per key: number, status, severity, needs, the worker with since, active and stale, and + **fix** (`not-merged`, `merged`, `held`, `failed`, `unheard`, `delivered`, or empty), from the facts of § 11. It + carries no title, report or note, and changes nothing. +- **The router** (to-be 45 § 5) asks it over the bus, outside its work, at most once a minute, and says from it the + **tracking line** in every condition's message; the words and the verdict it changes are its own (to-be 45 § 5). + The controller learns nothing of issues. + +## What it leaves out + +Priorities beyond severity, notifications beyond the `ready` and `ready-to-verify` events and the stuck-fix warning, full-text or semantic search, the forge's own issue lists, and an index of any kind: +none of them is in any slice above, and each needs a decision of its own. Who works on an issue, listed here until +2026-10-10, is settled by ADR 0284. diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/hq/03-DESIGN/01-to-be/57-building-config-and-provisioning.md b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/hq/03-DESIGN/01-to-be/57-building-config-and-provisioning.md new file mode 100644 index 00000000..b252b3c8 --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/hq/03-DESIGN/01-to-be/57-building-config-and-provisioning.md @@ -0,0 +1,363 @@ +--- +layer: to-be +status: in-progress +code: [mesh-controller, mesh-host, mesh-sdk, mesh-catalog] +updated: 2026-10-17 +decisions: + - "ADR 0348" + - "ADR 0349" + - "ADR 0201" + - "ADR 0113" + - "ADR 0158" + - "ADR 0360" + - "ADR 0362" +--- + +# 57. Building config and provisioning + +**The work of ADR 0348 (config is a domain of its own) and ADR 0349 (provisioning), broken into +packages small enough that each ends at something that can be seen to work, in the order their +dependencies allow.** The two decision records are the authority on *what* is built; this document holds +only the packages, their order, their sizes and their proofs, and is wrong the moment it disagrees with +them. It is the shape [design 38](38-building-the-operators-machine.md) gave the operator's machine. + +## How this is built + +**Each package is one issue in the issue tracker, and one pull request per repository it touches.** The +issue carries the package's number here in its title, links the packages it waits for with `depends-on`, +and links ADR 0348 or ADR 0349. At most five packages are worked at once, each by its own agent in its +own worktree; a package starts once every package it depends on is merged. Every pull request is +reviewed independently at its head and merged only with its checks green. + +**It ships as one release.** The operator's decision of 2026-10-14: one delivery, not one per package. +Each of mesh-controller, `mesh-host`, mesh-sdk, mesh-catalog and hq has the branch +`feat/57-config-and-provisioning`, and every package's pull request merges into that branch, never into +main. When the last package is merged, the whole is rehearsed in the lab: the rollout, the moves of data in +their order, and every proof that is run live. Then each repository's branch reaches main in one pull +request, and the five, sharing their branch's name, are one delivery group (ADR 0239 §2), judged as one +future state. The five are merged within one merge window, so they are one batch and one rollout (ADR 0276 +§1, §2), in an order declared with `after:` lines in each pull request (ADR 0239 §11): mesh-sdk, then +`mesh-host`, then mesh-controller, then mesh-catalog, then hq. The release holds every package but T6, R1 and R2: +the media catalogue and the applications live in repositories of their own and move after it, and only +then can the old forms be refused (R1) and the record say what runs (R2). + +**The moves of data run after the delivery, in this order, each a verb that says what it did:** +placement out of the settings layers (F2), the settings layers into mesh-config (K5), the secrets into +the vault, nothing rotated (V3), then the providers one by one (B2, B3). Each is rehearsed in the lab +first, and each is checked by reading back through the module that owns the data before anything +points at it. + +**What a release costs.** A proof that is run live (dunst on a workstation, a secret reaching its file, a +database consumer connecting) is first seen in the lab rehearsal, and a fault found then holds the +release, not one package. A merge's builds go to every machine at once (ADR 0360), so the release has no +first node to hold it: after the pull requests' checks, the lab rehearsal is the only live check before it +reaches every node. A machine that fails to apply it, or whose provisioner reports a module's files failed +(ADR 0362), is named in the rollout's end and raises apply-failing, and is fixed forward with a new pull +request into main; one that says nothing within fifteen minutes is named silent and gets its builds from +the backlog rollout once it is heard from (ADR 0360 §10). + +**Nothing breaks on the way.** ADR 0349 clause 17: each file is filled the old way or the new way, never +both, by the form of its manifest, and the old forms stay accepted until the last module has moved. +Inside the release this still holds: the media catalogue and the applications kept in their own +repositories move after it, so the old forms stay accepted until they have (R1). + +**Where it stands, 2026-10-17** (the Milestones section below). The packages' branches were merged into +main of mesh-controller, the node-engine (`mesh-host`) and mesh-catalog on 2026-10-16. T6 moved after them, as +planned above, and is resolved. B3 was left out as well, so B3, R1 and R2 are what is left. + +## What exists already, measured + +Counted 2026-10-14 at mesh-controller `1601cb1`, the node-engine (`mesh-host`) `1a26778`, mesh-sdk `a2d2af0`, mesh-catalog +`ebf7f37`. + +| Piece | Today | Becomes | +|---|---|---| +| the controller's composer | `catalogue.Resolution.compose` fills every namespace but `${secret:}` in one loop (`declaration.go` ~1000–1085); the namespaces and their fields are tabled in `fillers` | fills placement only, for a module that has moved | +| settings | tables `settings`, `settings_history`; the `settings` verb (`modules.go` 407–760); proposals and asks (`proposals.go`, 975 lines); trusted files; `takes-settings` merging; about 4 100 lines and 2 500 of tests | the `mesh-config` module and its buckets; the verb `config` | +| placement kept as settings | `places`, `accesses` and the kept networks are read from settings layers | facts of an assignment, the controller's own | +| secrets | the controller mints, seals and accepts every secret (`internal/secrets`, `inventory/secrets.go` 1 125 lines; tables `secret`, `module_secret`, `secret_ask`, `operator_key`); the vault (`mesh-vault`, TypeScript) is read-only and never asked | the vault, in Go, holding and minting; handing out on request and reply | +| bindings | grant files and binding files composed by the controller (`declaration.go` 750–818); `${bound:}` 117 uses | each provider answers its consumers; the vault mints the pair's credential | +| contributions | `${contribution:}` (4 collectors), `${shell:}` (3), `${environment:}` (1); 22 contributors | collections, filled by the provisioner client | +| file text | 225 file resources in 88 modules, all inline (about 193 KB); 12 modules keep a second copy in 34 files with a test that the two agree (ADR 0349's context counted eleven; the twelfth is lemurs) | template files under `files/`, packed by the module's build | +| packing a directory | the builder's `archive` artifact packs a tree and publishes it by digest; the node-engine fetches and unpacks it | the same mechanism carries a module's templates | +| the node-engine's file writing | `applyFile`, `applyBlock`, `applyInto`, create-once, drift written back, `Keep` backups, restart-on and reload-on, container reads | the same code, reached also through a narrow local write verb | +| the node-engine's local verbs | one socket (`cli.sock`), peer-checked by uid, forwarding to the controller; no local write | a second, narrow socket for the provisioner client | +| bus identities | one principal per node for the tool runner, one per module with a process of its own | the provisioner client gets one of its own | +| the SDK | KV, watches, asks, caller proof; no sealing helper; the TypeScript `provisioner` is a database-provider harness | a seal box, provider helpers, and readers for config, bound values and secrets | + +**Two measurements decide the order.** The builder can already pack a module's directory, and the +controller already fills every placeholder at registration time from text; so template files can ship +first, the controller reading the text back from the packed `files/`, and nothing on a node changes. And +the node-engine's writing code needs no change in kind, only a second way in; so the provisioner client +reuses it rather than writing as root itself. + +## The order the work allows + +``` +package waits for package waits for +F1 — P1 — +F2 — P2 P1 +F3 — P3 F3 +F4 — P4 P3 +T1 — P5 P2, P4, T1, K3 +T2 T1 P6 P5 +T3 T4 T5 T2 P7 P6 +T6 T1 V1 F1 +K1 T1 (same file) V2 V1 +K2 K1 V3 V2 +K3 F3, K2 V4 V3, P5 +K4 K3 V5 V3, V4 +K5 F2, K4 B1 P5, V5 +K6 K1 B2 B1 +K7 K5, P6 B3 B2 +L1 P6 L2 L1 +S1 P6 R1 T5, T6, K6, K7, P7, V4, B3, L2, S1 +R2 R1 +``` +The issue tracker carries these as `depends-on` links (thirteen issues also link what they wait for +through another package), so the packages ready to start are those whose linked packages are all merged +into the release's branches. An issue's "Waits for: … delivered" means merged into the release's +branches: nothing is delivered before the release. The first wave touches different repositories and different files: +**T1, F1, F3, P1 and F4**. K1 waits for T1 because both change `internal/catalogue/manifest.go`; P3 +waits for F3 because both change `internal/broker`; F2 joins as soon as a slot is free. + +## 0 — Foundations + +**F1 (issue 650) — A seal box in the SDK.** *mesh-sdk. Half a day.* The ADR 0206 box (X25519, HKDF-SHA256, +AES-256-GCM) moves from the licence manager into `mesh-sdk/go/seal`, with a TypeScript twin, and the +licence manager uses it. **Proof:** a byte-for-byte test against the licence manager's existing sealed +values; the licence manager's tests pass unchanged. + +**F2 (issue 651) — Placement leaves the settings layers.** *mesh-controller. Two days.* `places`, `accesses` and the +kept networks become facts of an assignment, held by the controller beside the assignment, with a +migration moving each existing layer's keys; `Settle` stops letting a setting replace a provider's +served keys. **Proof:** composition of every node before and after the migration is byte-identical; +the settings layers hold no placement key afterwards. + +**F3 (issue 652) — The assignments and the modules' config declarations on the bus.** *mesh-controller. Two days.* +The controller keeps the bucket of assignments design 54 decides (which module on which node), and a +bucket of each registered module's declared config values, written when they change, with one event per +write. mesh-config needs both to resolve. **Proof:** a test against a real bus: an assignment and a +registration each change exactly one key and raise one event. + +**F4 (issue 653) — The words.** *hq. Half a day.* The glossary gains *config value*, *predefined key*, *provision* +and *provisioner client*, *collection*, and the ownership words *owned*, *seed* and *managed*. The word "setting" goes on *config +value*'s *Not:* line only in R2, once the 67 documents still using it are reworded; until then the +glossary says it is the older word. **Proof:** `words.py` passes. + +## 1 — Template files (ADR 0349 clause 2) + +**T1 (issue 654) — A file resource may name a template.** *mesh-controller. Three days; the largest package of the +stage.* A file resource takes `"template": "files/"` 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 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 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 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. + +**T4 (issue 657) — The servers' modules.** *mesh-catalog. Two days.* Every remaining module with a build. **Proof:** +as T2. + +**T5 (issue 658) — The ten modules without a build.** *mesh-catalog. A day.* ca-trust, de-spiegel, invoicing, +local-model-consumer, matrix, node-env, only-office, sshd, step-ca and supabase gain an archive build for +their templates. **Proof:** as T2; afterwards no manifest in the catalogue carries file text. + +**T6 (issue 659) — The media catalogue and the applications.** *mesh-media-catalog and each application's +repository. Two days.* **Proof:** as T2, per repository. + +## 2 — Config (ADR 0348) + +**K1 (issue 660) — A module declares its config.** *mesh-controller. Two days.* The manifest takes `config` (type, +description, validation, trusted, `predefined`, `default` or `required`), `takes-config` and +`${config:}`, with `settings`, `takes-settings` and `${setting:}` read as aliases; registration +refuses everything ADR 0348's "How it is checked" lists for registration: a value without type or +description; a value with neither a default nor `required`; a default failing its validation; an +object or list not trusted; a predefined key not on the list or of another type (with K2); an untrusted +value falling back to a trusted key; a value neither in a template nor read at runtime; a placeholder +naming an undeclared value; a `reload-on` or `restart-on` naming an undeclared file; a moved module's +mergeable file not naming its keys; a mesh word or `puid`/`pgid` in `takes-config`. **Proof:** the registration refusal tests, one per +rule; every manifest in the catalogue still registers. + +**K2 (issue 661) — The list of predefined keys.** *mesh-controller and mesh-catalog. A day.* `predefined.json` at the +catalogue's root, with `font-size` and `agent-managed-settings` first; the build validates it and every +manifest naming a key; the merge check refuses removing a key still named or holding a value; the newest +trunk build's list is published. **Proof:** the merge check refuses a planted removal; an older build's +list does not replace a newer one. + +**K3 (issue 662) — The mesh-config module.** *mesh-catalog. Four days.* A Go module holding the seat `mesh-config`: +the buckets `mesh-config_values`, `_resolved`, `_predefined`, `_proposals`; the four sources resolved from +F3's assignments and declarations; the verb `config` (set, clear, list, history, narrowed by module and +node); validation; the hand-act log; `config-changed`; only its account writes its buckets. Terminal-only +for trusted values until K4. **Proof:** its tests resolve each source in turn; a test against a real bus +refuses a put from another account. + +**K4 (issue 663) — Trusted values are proposed and approved.** *mesh-catalog. Two days.* mesh-config asks under its +own name, with its own grant and cap of open asks, binds each option to the level, key and value, records +the approver beside the value, and checks the proposal on Approve (ADR 0277's rules, ADR 0348 clause 8). +**Proof:** a proposal test end to end with a stand-in channel; an Approve against a changed value is +refused. + +**K5 (issue 664) — The values move, and `settings` forwards.** *mesh-controller and mesh-catalog. Two days.* Each +existing layer moves into mesh-config (a node layer to `module..`; a whole-mesh layer to +mesh config, a manifest default or each node, as ADR 0348's consequences say; claude-code's managed +settings to `agent-managed-settings`), recorded with why; for a module not yet handed to the provisioner +the controller reads `mesh-config_resolved` where it read its layers (ADR 0349 clause 17's old way, +an exception to ADR 0348 §9 that R1 removes), and `settings` answers by forwarding to `config`. **Proof:** composition of +every node before and after is byte-identical; `settings` and `config` list the same values. + +**K6 (issue 665) — The catalogue says config.** *mesh-catalog. A day.* The 15 modules using `${setting:}`, the 9 +declaring settings and the 4 with `takes-settings` move to the new names and the full declaration. +**Proof:** the catalogue's check finds no old name in a moved module. + +**K7 (issue 666) — The operator's acceptance on dunst.** *live. An hour.* With every level cleared the first +workstation's dunstrc says 10; a mesh font size, then that node's font size, then dunst's own value on +that node each appear in its dunstrc within the call's answer, and the history shows each change with who and why. Needs P6. + +## 3 — The provisioner (ADR 0349 clauses 3–5, 11, 13) + +**P1 (issue 667) — The node-engine knows who owns a placed file.** *`mesh-host`. A day.* `declaration.File` gains +`owner-at-runtime` (`owned`, `seed`, `managed`), mapped onto today's paths (drift written back, +create-once, `into`), and the declaration gains the list of files the provisioner fills. **Proof:** the +apply tests: each ownership behaves as its old form did. + +**P2 (issue 668) — The narrow local write verb.** *`mesh-host`. Two days.* A second unix socket accepts a write only from +the provisioner client's account (`SO_PEERCRED`) and only for a path the declaration names as filled, +writes through `applyFile`, `applyBlock` or `applyInto` with `Keep`, and fires restart-on, reload-on and +container reads as an apply does. **Proof:** tests: an undeclared path and another uid are refused; a +write under `reload-on` reloads its program once. + +**P3 (issue 669) — The provisioner client's own bus account.** *mesh-controller. A day.* A principal of its own per +node for `node-provisioner`, granted to read its node's keys in each domain's bucket, publish +`prov.applied..>` and ask the vault; nothing else. **Proof:** the grants test, and a test against a +real bus refusing a read of another node's keys. + +**P4 (issue 670) — The provisioner's server.** *mesh-catalog. Two days; after P3.* A Go module holding `mesh-provisioner` once: +the registry of which domain answers which kind, the buckets `_registry`, `_needs`, `_applied` and +`_collect`. **Proof:** its tests; the buckets listed live. + +**P5 (issue 671) — The provisioner's client.** *mesh-catalog. Four days.* A Go service on every node, outside the tool +runner: fetches a module's templates by digest, records needs, watches each owner's keys for its node, +fills only when every value is there, writes through P2, reports `prov.applied`, writes an `owned` file +back on drift, removes a module's files and needs when it leaves, fills from current values when its node +comes back. Its first owners are placement (from the declaration) and config (from +`mesh-config_resolved`). **Proof:** the client's test list in ADR 0349, against a real bus. + +**P6 (issue 672) — The controller hands a module to the provisioner, and the rollout waits for it.** *mesh-controller. +Two days.* For a module marked moved, the declaration carries placement, the templates' digest and the +filled-files list, and the controller runs no other filling pass for it; a rollout counts the module applied +on a machine only after the node-engine's report and that node's `prov.applied` for the new build, and a +failed `prov.applied` is a failed apply (ADR 0362). dunst moves first. **Proof:** composition and rollout +tests; dunst on the first workstation filled by the client, live. + +**P7 (issue 673) — The node-engine stops writing placed files from the declaration.** *`mesh-host`. A day.* For a moved +module the engine writes its filled files only through P2, and removes as an orphan only what the verb +placed. **Proof:** the apply tests; no placed file written twice in a day's journal on the +first workstation. + +## 4 — Secrets (ADR 0349 clauses 6–11, 16; issue 647) + +**V1 (issue 674) — The vault in Go, holding secrets.** *mesh-catalog. Four days.* `mesh-vault` is rewritten in Go: +its own store, a copy of each secret sealed to the operator key, `get` on request and reply sealed to the +asker's key (F1's box; the node-engine's key format is settled here and written down), `secret-changed` +without a value, no secret in any bucket. **Proof:** the vault's test list in ADR 0349. + +**V2 (issue 675) — The vault's verbs.** *mesh-catalog and mesh-controller. Three days.* `secret accept`, `secret-ask` +(with ADR 0277 §24–§27's record and bounds in the vault's store), `issued` (ADR 0305, the provider's tool +answering the vault alone) and `rotate` become the vault's; the controller's verbs forward. **Proof:** the +`secret-ask` and issuing provider tests in ADR 0349. + +**V3 (issue 676) — The secrets move to the vault.** *mesh-controller and mesh-catalog. Two days.* Every row of +`secret`, `module_secret`, `secret_ask` and the operator key's public half are copied into the vault as they are, +nothing rotated, and read back through the vault before anything points at it. **Proof:** for every +secret, the vault's sealed answer opens to the same value the controller's does. + +**V4 (issue 647) — A node's secrets come from the vault.** *mesh-catalog, mesh-controller, `mesh-host`. +Three days.* The client fills `${secret:}` from the vault's `get`; for a moved module the controller seals +nothing; the core's credentials travel sealed by the vault to their node and the node-engine opens them. +**Proof:** a secret rotated in the vault reaches its file without a new declaration, live. + +**V5 (issue 677) — The vault mints pair credentials.** *mesh-catalog. Two days.* One value per provider assignment and +provision, remade for all holders when the vault sees a consumer's need recorded or deleted (ADR 0158 +§4–§6). A need recorded for a bind the vault already holds from V3 adopts that value and remakes +nothing, so the move itself rotates no credential. **Proof:** the provider test in ADR 0349, with a stand-in provider. + +## 5 — Bindings (ADR 0349 clause 12) + +**B1 (issue 678) — A provider's helper in the SDK.** *mesh-sdk. Two days.* Go: watch the needs for a kind, write one +entry per consumer, refuse a derived value for a multi-holder consumer, answer from the vault's pair +credential. The TypeScript `provisioner` harness is renamed so the word means one thing. **Proof:** its +tests with a stand-in bus. + +**B2 (issue 679) — PostgreSQL answers its consumers.** *mesh-catalog and mesh-controller. Three days.* The first +provider moves; its grant files and binding files are retired. **Proof:** every database consumer on +every node connects after the move with the credential it had, adopted by the vault as V5 says, live. + +**B3 (issue 680) — The other providers.** *mesh-catalog. A week, one pull request per provider.* **Proof:** as B2, per +provider; afterwards the controller composes no grant file. + +## 6 — Collections (ADR 0349 clause 13) + +**L1 (issue 684) — A collection is declared and filled by the client.** *mesh-controller and mesh-catalog. Three days.* +A collector requires a collection; a contributor provides an item from a template; the client fills the +collector's template from `_collect`. `${contribution:}`, `${shell:}` and `${environment:}` read as +collections. **Proof:** zsh's file holds every placed contributor's item and loses one whose contributor +leaves. + +**L2 (issue 681) — The collectors and contributors move.** *mesh-catalog. Two days.* zsh first, then the bar, the +display session, the hotkeys, xorg, power and node-env, with their 22 contributors. **Proof:** each +collector's composed file is unchanged at the move. + +## 7 — A module's code asks at runtime (ADR 0349 clause 14) + +**S1 (issue 685) — The SDK's readers.** *mesh-sdk and mesh-catalog. Two days.* `config`, `bound` and their watches, +answered by the node's client for what the manifest declares. `secrets` refuses until the module runs +under a bus account of its own, which waits on issue 569 and its decision. **Proof:** the SDK's test in +ADR 0349; claude-code reads its managed settings through it. + +## 8 — The old forms go (ADR 0349 clause 17) + +**R1 (issue 682) — Refused and removed.** *mesh-controller, `mesh-host`, mesh-catalog. Three days.* Once every module of +every catalogue has moved: the catalogue's check refuses inline text, `settings`, `${setting:}` and the +old contribution forms; the controller's filling passes but placement, its settings, secret and binding +code and tables (a new migration) go; the node-engine keeps opening sealed values for the core's files +only; the controller stops reading `mesh-config_resolved`. **Proof:** the controller's test that its +code opens no secret outside genesis and reads no config value; the absence of the +removed code. + +**R2 (issue 683) — The record says what runs.** *hq. Two days.* The as-is designs are written, designs 27 and 32 are +amended, the 67 documents that still say "setting" in running prose are reworded so the glossary can +retire the word (F4 left it off the *Not:* line for them), design 49 lists the Config domain, ADR 0348's and 0349's built status is said, and this document's status becomes `implemented`. + +## Milestones + +The packages above were built as one delivery, not milestone by milestone; the milestones group them by what +each makes true. Whether a milestone's issues are delivered is read from the issue tracker (playbook 07). + +| # | Outcome | Packages | Issues | +|---|---|---|---| +| 1 | Config, provisioning, secrets, bindings, collections and runtime asks run the new way; the old forms are still accepted | every package but T6, B3, R1, R2 | 647, 650 to 679 but 659, 681, 684, 685 | +| 2 | The media catalogue and the applications carry their file text as templates | T6 | 659 | +| 3 | The other providers answer their consumers; the old forms are refused; the record says what runs | B3, R1, R2 | 680, 682, 683 | + +## What is deliberately not here + +- **Secrets in a module's own code** wait on issue 569 (a module's tools run as the tool runner's + account) and its decision; S1 ships without them. +- **Release separated from delivery** stays issue 644's, and **the bus's replicas** issue 645's. +- **Renaming "walk"** is issue 646's. + +## How this list is kept true + +Each package's proof that runs in its tests is run when it is merged into the release's branch, and its +line here gains the date and the commit; a proof that runs live is run in the lab rehearsal and again +after the release's delivery. A package whose proof fails is not reworded; the failure is recorded under it and the package +stays open. The issue tracker says which packages are worked now; this document says only the order. diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/hq/COMMIT b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/hq/COMMIT new file mode 100644 index 00000000..dbb7b86f --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/hq/COMMIT @@ -0,0 +1 @@ +3a5c4c7c4d9c4143044f9659d4b58cb1d531bcd7 diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/show-car-hunter-2.json b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/show-car-hunter-2.json new file mode 100644 index 00000000..08522637 --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/show-car-hunter-2.json @@ -0,0 +1 @@ +{"delivery": {"base": "main", "branch": "feat/current-mesh", "commit": "cc4c8ce12901ba4f4035bef7e660db049a2561f5", "id": "jschoubben/car-hunter@cc4c8ce12901", "issues": [1945], "number": 2, "repository": "jschoubben/car-hunter", "state": "ready", "title": "car-hunter on today's mesh: health check, templates, a network of its own, rebuildable data seeded at start"}, "waits": "its merge"} diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/tracker-rows.json b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/tracker-rows.json new file mode 100644 index 00000000..0a9ebb30 --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/tracker-rows.json @@ -0,0 +1,13 @@ +[ + {"delivery": "delivered to every machine it should reach", "needs": "none", "number": 1942, "owner": "mesh-controller cmd/mesh-controller/delivery_edges.go", "ready": false, "severity": "major", "status": "resolved", "title": "Today's planner cannot put the controller and the node-engine in one tier, which release 6's switch needs"}, + {"needs": "none", "number": 1943, "owner": "mesh-sdk go/delivery, go/catalogue, go/cmd/mesh-hand-compose", "ready": false, "severity": "major", "status": "resolved", "title": "mesh-sdk has no layout for a module's composition on a node, no node want, no composition digest in a want, and still lists the plan verbs"}, + {"needs": "none", "number": 1944, "owner": "mesh-host cmd/mesh-host, internal/agent", "ready": false, "severity": "major", "status": "resolved", "title": "The node-engine applies only whole declarations the controller sends, so no node can follow its wants"}, + {"needs": "none", "number": 1945, "owner": "mesh-controller cmd/mesh-controller", "ready": true, "severity": "major", "since": "2026-10-24T14:14:53Z", "status": "fixing", "title": "The controller still plans deliveries and pushes whole declarations, so nothing can deliver by wants", "worker": "dev-r6-interface@shanks"}, + {"delivery": "fix merged, not delivered: mesh-catalog#475", "needs": "none", "number": 1946, "owner": "mesh-catalog modules/mesh-delivery-gate", "ready": false, "severity": "major", "status": "resolved", "title": "The gate keeps a delivery's states, machine steps and plan calls that no longer exist once plans go"}, + {"delivery": "held: mesh-delivery#39: a person: merged without a passing check: only a person decides what follows", "needs": "none", "number": 1906, "owner": "mesh-delivery", "ready": false, "severity": "minor", "status": "resolved", "title": "Release 6 must bring what R5's assigner leaves out: wants mirrored as pulses, the restore ask for a data module and the condition for a held fingerprint"}, + {"needs": "none", "number": 1130, "owner": "hq 03-DESIGN/01-to-be/47, 03-DESIGN/00-as-is/04-delivery, 03-DESIGN/00-as-is/13-the-console", "ready": false, "severity": "minor", "status": "resolved", "title": "Design 47's sections still describe delivery plans, batches and the merge window, which ADR 0429 replaces release by release"}, + {"abandoned": true, "delivered-not-resolved": true, "delivery": "delivered to every machine it should reach", "needs": "none", "number": 1049, "owner": "mesh-catalog modules/gitea", "ready": true, "severity": "critical", "since": "2026-10-18T02:26:22Z", "stale": true, "status": "fixing", "title": "A group that moves a module delivers its first member before the second merges, so the plan builds the moved module from the old repository and fails", "worker": "fix-chain-cat@shanks"}, + {"needs": "decision", "number": 1126, "owner": "mesh-controller", "ready": false, "severity": "minor", "status": "open", "title": "About 49,800 controller code lines stay after the delivery model, against a first limit of 25,000"}, + {"needs": "none", "number": 774, "owner": "mesh-controller; mesh-catalog modules/mesh-vault", "ready": true, "severity": "minor", "status": "open", "title": "ADR 0113 clause 6 is not built: the controller, not the vault, mints the bus accounts of modules"}, + {"needs": "none", "number": 1021, "owner": "mesh-catalog modules/mesh-delivery", "ready": false, "severity": "minor", "status": "wontfix", "title": "An after: line added by editing a pull request's description is never read, though mesh-delivery's refusal says to add it there"} +] diff --git a/modules/mesh-knowledge/module.json b/modules/mesh-knowledge/module.json index f0abb798..5d792f79 100644 --- a/modules/mesh-knowledge/module.json +++ b/modules/mesh-knowledge/module.json @@ -32,7 +32,8 @@ "propose", "import", "return", - "withdraw" + "withdraw", + "progress" ] } ], @@ -52,7 +53,8 @@ "propose", "import", "return", - "withdraw" + "withdraw", + "progress" ] } ], @@ -82,7 +84,12 @@ "records.records_status", "records.records_list", "records.records_read", - "seat:operator-channel.notify" + "seat:operator-channel.notify", + "seat:issue-tracker.list", + "seat:issue-tracker.get", + "seat:mesh-delivery.deliveries", + "seat:mesh-delivery.show", + "seat:mesh-delivery.groups" ], "config": { "judge-model": {