diff --git a/.gitignore b/.gitignore index b6d1dc33..37078a82 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ modules/claude-code/claude-code modules/mesh-watcher/mesh-watcher modules/dnsmasq/dnsmasq modules/mesh-issues/cmd/mesh-issues/mesh-issues +modules/mesh-issues/mesh-issues modules/mounts/cmd/mounts/mounts modules/mesh-knowledge/cmd/mesh-knowledge/mesh-knowledge modules/mesh-knowledge/mesh-knowledge diff --git a/modules/mesh-issues/README.md b/modules/mesh-issues/README.md index 4ad24027..77def3b3 100644 --- a/modules/mesh-issues/README.md +++ b/modules/mesh-issues/README.md @@ -22,8 +22,11 @@ what it refuses. | `list` | optionally `status`, `owner`, `phrase`, `limit`, `severity`, `needs`, `ready`, `flag` (`delivered-not-resolved` or `abandoned`), `search` (words, ranked), `before` (a number, the cursor), `numbers` (only these issues, in the order given, naming any not an issue in `missing`), `worker` (only that worker's held issues), `linked` (only the issues with that link, as `kind:target`, such as `decision:0493`) | short rows, newest first, or best first for a `search`, or in the order given for `numbers`: number, status, owner, title, `moved` (when the issue last changed: opened, moved, noted, linked, worked on, or its fix's delivery said a new state), and each flag that holds; `next`, the `before` that reads on, when more match | | `move` | `number`, `status`, and what that status needs (`by` for `diagnosing` and `fixing`, optionally `take-over`) | changes the status under the development cycle's rules | | `note` | `number`, `text` | appends a note; a note is never edited | -| `link` | `number`, `kind`, `target`, optionally `remove` | adds or removes a typed link | -| `import` | optionally `dry-run`, `commit` | hq's issue folders, read through the records module, each written as an issue under its own number; see below | +| `link` | `number`, `kind`, `target`, optionally `remove` | adds or removes a typed link; kind `milestone` (design/id, such as 47/R6) at most once per issue and only to an open milestone, kind `commit` as repository@full sha (ADR 0493 §2 to §4) | +| `milestones` | optionally `design`, `status`, or `one` (design/id) | the milestones, in their design's table order, each with its issues and how many are done; one with its history | +| `milestone-set` | `design`, `id`, and `outcome` to make one; optionally `order`, `announce` | makes a milestone (state `milestones`, event `milestone-set`), changes its outcome or order, or says it again | +| `milestone-move` | `milestone`, `status` (`open` or `closed`), `reason` | opens or closes it (event `milestone-moved`); closing is refused while a linked issue is neither resolved nor wontfix | +| `import` | optionally `dry-run`, `commit`; or `milestones` (a design's number) | hq's issue folders, read through the records module, each written as an issue under its own number; see below. With `milestones`, seeds that design's milestone records from its table once (issue 1781): an issue two rows name, or that another milestone holds, is refused and named; a milestone whose issues are all done is closed; run again, it changes nothing | | `set` | `number`, any of `severity`, `needs`, `suggested-owner` | changes them as a person decides | | `groom` | optionally `pass`, `confirm` | the triage over the backlog: without arguments a dry run in the background, answering its pass id; with `pass` its progress and findings; with `confirm` and `pass` exactly that pass's findings applied, each checked again; see below | | `tracking` | optionally `conditions` (condition keys) | for every condition key an issue links (link kind `condition`), the issues that link it, those not closed first: status, severity, needs, worker with since, active and stale, and where the fix is (`fix`); the operator's channel says it in a condition's message (hq ADR 0285) | diff --git a/modules/mesh-issues/cmd/mesh-issues/check.go b/modules/mesh-issues/cmd/mesh-issues/check.go index b29b45a9..d584dad3 100644 --- a/modules/mesh-issues/cmd/mesh-issues/check.go +++ b/modules/mesh-issues/cmd/mesh-issues/check.go @@ -215,10 +215,18 @@ const ( LinkDesign = "design" LinkCondition = "condition" LinkDelivery = "delivery" + // LinkMilestone is the one milestone an issue belongs to, design/id (ADR 0493 §1, §2). + LinkMilestone = "milestone" + // LinkCommit is a commit that fixes the issue, repository@full sha (ADR 0493 §4). + LinkCommit = "commit" ) // LinkKinds is every kind, in the order they are told. -var LinkKinds = []string{LinkDuplicates, LinkRelatesTo, LinkDependsOn, LinkPullRequest, LinkDecision, LinkDesign, LinkCondition, LinkDelivery} +var LinkKinds = []string{LinkDuplicates, LinkRelatesTo, LinkDependsOn, LinkPullRequest, LinkDecision, LinkDesign, LinkCondition, LinkDelivery, + LinkMilestone, LinkCommit} + +// madeByPeople are the kinds only a person or a session links, never the triage (ADR 0493 §4). +var madeByPeople = []string{LinkPullRequest, LinkMilestone, LinkCommit} var ( pullRequest = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,99}#[1-9][0-9]{0,6}$`) @@ -280,6 +288,15 @@ func checkTarget(kind, target string) (string, error) { case LinkDelivery: ok, shape = deliveryID.MatchString(t), "a delivery's id as mesh-delivery names it: the repository as owner/name, @ and twelve "+ "characters of its commit, such as novox/mesh-controller@d2e9dc61590f" + case LinkMilestone: + var c string + c, ok = checkMilestoneTarget(t) + if ok { + t = c + } + shape = "a milestone as design/id, such as 47/R6" + case LinkCommit: + ok, shape = commitRef.MatchString(t), "a repository, @ and the commit's full 40-character id, such as mesh-catalog@"+strings.Repeat("0", 40) default: return "", fmt.Errorf("kind %q is not a kind of link: one of %s", kind, strings.Join(LinkKinds, ", ")) } diff --git a/modules/mesh-issues/cmd/mesh-issues/main.go b/modules/mesh-issues/cmd/mesh-issues/main.go index 40d880f2..c040c4fc 100644 --- a/modules/mesh-issues/cmd/mesh-issues/main.go +++ b/modules/mesh-issues/cmd/mesh-issues/main.go @@ -22,7 +22,8 @@ import ( const Seat = "issue-tracker" // Verbs are the seat's verbs, as the manifest declares and claims them. -var Verbs = []string{"open", "get", "list", "move", "note", "link", "import", "groom", "set", "tracking", "reserve", "fix-times"} +var Verbs = []string{"open", "get", "list", "move", "note", "link", "milestones", "milestone-set", "milestone-move", "import", + "groom", "set", "tracking", "reserve", "fix-times"} func logf(format string, a ...any) { fmt.Fprintf(os.Stderr, format+"\n", a...) } @@ -617,8 +618,9 @@ func Tools(t *Tracker) []stdio.Tool { Description: "Link an issue to something, or with remove, take a link away. Kinds: duplicates or relates-to " + "another issue (its number), pull-request (such as mesh-controller#246), decision (a decision record's " + "number, such as 0275), design (a design document's path), condition (a condition's key) and delivery " + - "(a delivery's id, such as novox/mesh-controller@d2e9dc61590f). Refused: a target not of its kind's shape, an issue that does not exist, a link it " + - "already has.", + "(a delivery's id, such as novox/mesh-controller@d2e9dc61590f), milestone (the one milestone it belongs to, design/id such as " + + "47/R6) and commit (a commit that fixes it, repository@full sha). Refused: a target not of its kind's shape, an issue that does not exist, a link it " + + "already has, a second milestone (remove the first to move it), a milestone that does not exist or is closed.", Input: schema([]string{"number", "kind", "target"}, map[string]any{ "number": number("the issue's number"), "kind": map[string]any{"type": "string", "enum": LinkKinds, "description": "what the target is"}, @@ -645,6 +647,98 @@ func Tools(t *Tracker) []stdio.Tool { } return t.link(t.callerOf(m), ask) }}, + {Name: Seat + ".milestones", + Description: "Read the milestones of the designs: each milestone (a design's milestone, design/id such as 47/R6) with " + + "its outcome, its place in the design's table, its status, who made and moved it, and the issues linked to it " + + "with how many are done. Narrow by design or status, or read one milestone with its history.", + Input: schema(nil, map[string]any{ + "design": number("only this design's milestones, such as 47"), + "status": map[string]any{"type": "string", "enum": MilestoneStatuses, "description": "only milestones with this status"}, + "one": text("one milestone, design/id such as 47/R6, with its history"), + }), + Run: func(m map[string]any) (any, error) { + a, err := readArgs("milestones", m, "design", "status", "one") + if err != nil { + return nil, err + } + var ask MilestonesAsk + d, err := a.number("design", false) + if err != nil { + return nil, fmt.Errorf("design is a design's number, such as 47") + } + ask.Design = int(d) + if ask.Status, err = a.str("status"); err != nil { + return nil, err + } + if ask.One, err = a.str("one"); err != nil { + return nil, err + } + return t.milestones(ask) + }}, + {Name: Seat + ".milestone-set", + Description: "Make a milestone of a design (its id as the design's table writes it, and its outcome), or change its " + + "outcome or its place in the table; with announce, say it again on the bus unchanged. Its issues are linked " + + "with link, kind milestone. Refused: a design that is no number, an id that is no short name, a new milestone without an outcome.", + Input: schema([]string{"design", "id"}, map[string]any{ + "design": number("the design's number, such as 47"), + "id": text("the milestone's id as the table writes it, such as R6 or 3 fix"), + "outcome": text("what the milestone delivers, in one line"), + "order": number("its place in the design's table, first 1"), + "announce": map[string]any{"type": "boolean", "description": "say the milestone again on the bus, as it is"}, + }), + Run: func(m map[string]any) (any, error) { + a, err := readArgs("milestone-set", m, "design", "id", "outcome", "order", "announce") + if err != nil { + return nil, err + } + var ask MilestoneSetAsk + d, err := a.number("design", true) + if err != nil { + return nil, fmt.Errorf("design is a design's number, such as 47") + } + o, err := a.number("order", false) + if err != nil { + return nil, fmt.Errorf("order is its place in the design's table, from 1") + } + ask.Design, ask.Order = int(d), int(o) + if ask.ID, err = a.str("id"); err != nil { + return nil, err + } + if ask.Outcome, err = a.str("outcome"); err != nil { + return nil, err + } + if ask.Announce, err = a.flag("announce"); err != nil { + return nil, err + } + return t.milestoneSet(t.callerOf(m), ask) + }}, + {Name: Seat + ".milestone-move", + Description: "Open or close a milestone (design/id such as 47/R6), with the reason. Closing is refused while an " + + "issue linked to it is neither resolved nor wontfix, naming each; a closed milestone takes no new issue.", + Input: schema([]string{"milestone", "status", "reason"}, map[string]any{ + "milestone": text("the milestone, design/id such as 47/R6"), + "status": map[string]any{"type": "string", "enum": MilestoneStatuses, "description": "its new status"}, + "reason": text("why, in one line"), + }), + Run: func(m map[string]any) (any, error) { + a, err := readArgs("milestone-move", m, "milestone", "status", "reason") + if err != nil { + return nil, err + } + ref, err := a.str("milestone") + if err != nil { + return nil, err + } + status, err := a.str("status") + if err != nil { + return nil, err + } + reason, err := a.str("reason") + if err != nil { + return nil, err + } + return t.milestoneMove(t.callerOf(m), ref, status, reason) + }}, {Name: Seat + ".import", Description: "Import hq's issue folders (04-ISSUES/NNN-slug/00-report.md), read through the records module at " + "the commit its checkout stands at, each as an issue under its own number with the status, owners, fix " + @@ -652,16 +746,39 @@ func Tools(t *Tracker) []stdio.Tool { "the tracker holds without a status; it never overwrites an issue, never advances the counter, refuses a " + "folder numbered past the counter or past the tracker's first number without an issue, and run again " + "changes nothing and says so. With dry-run it writes nothing and says what it would do; with commit it " + - "refuses a checkout standing elsewhere. The answer is a row per folder and the counts.", + "refuses a checkout standing elsewhere. The answer is a row per folder and the counts. With milestones and a " + + "design's number it seeds that design's milestones from its milestone table instead: makes each milestone not " + + "made yet, links each issue the table names that is in no milestone, refuses (and names) an issue two rows name " + + "or another milestone holds, closes a milestone whose issues are all done, and run again changes nothing.", Input: schema(nil, map[string]any{ - "dry-run": map[string]any{"type": "boolean", "description": "read everything, write nothing, say what would be done"}, - "commit": text("the commit of hq the record must stand at (a prefix is enough); refused when it stands elsewhere"), + "dry-run": map[string]any{"type": "boolean", "description": "read everything, write nothing, say what would be done"}, + "milestones": number("seed this design's milestones from its milestone table, such as 47"), + "commit": text("the commit of hq the record must stand at (a prefix is enough); refused when it stands elsewhere"), }), Run: func(m map[string]any) (any, error) { - a, err := readArgs("import", m, "dry-run", "commit") + a, err := readArgs("import", m, "dry-run", "commit", "milestones") if err != nil { return nil, err } + design, err := a.number("milestones", false) + if err != nil { + return nil, fmt.Errorf("milestones is a design's number, such as 47") + } + if design != 0 { + dry, err := a.flag("dry-run") + if err != nil { + return nil, err + } + commit, err := a.str("commit") + if err != nil { + return nil, err + } + records := t.Records + if records == nil { + records = busRecords{} + } + return t.importMilestones(t.callerOf(m), MilestoneImportAsk{Design: int(design), DryRun: dry, Commit: commit}, records) + } var ask ImportAsk if ask.DryRun, err = a.flag("dry-run"); err != nil { return nil, err diff --git a/modules/mesh-issues/cmd/mesh-issues/manifest_test.go b/modules/mesh-issues/cmd/mesh-issues/manifest_test.go index 68b60afa..fa116e3f 100644 --- a/modules/mesh-issues/cmd/mesh-issues/manifest_test.go +++ b/modules/mesh-issues/cmd/mesh-issues/manifest_test.go @@ -86,18 +86,18 @@ func TestItDeclaresAndHoldsTheIssueTrackerSeatOnceForTheMesh(t *testing.T) { t.Fatalf("module %q, seats %+v, claims %+v", m.Module, m.Seats, m.Claims) } if !reflect.DeepEqual(m.Emits, []string{EventOpened, EventMoved, EventNoted, EventLinked, EventImported, EventSet, - EventTriaged, EventReady, EventReadyToVerify, EventReserved}) { + EventTriaged, EventReady, EventReadyToVerify, EventReserved, EventMilestoneSet, EventMilestoneMoved}) { t.Fatalf("emits %v", m.Emits) } // The record is the log `changes` (ADR 0300 §1). Four states, none expiring: issues the fold of the log (§3), // still keeping its older values until the carry has read them (they go to one value in a follow-up), the // counters one value each, the backlog's pass of the duplicate check one value (ADR 0278 §6), each reserved - // number one value (issue 424). + // number one value (issue 424), and the milestones (issue 1781) a short history each, their trail beside their events. if !reflect.DeepEqual(m.Logs, []string{ChangesLog}) { t.Fatalf("logs %v", m.Logs) } d := m.stateDeclarations(t) - if len(d) != 4 || d[0].Name != IssuesState || d[0].History < 2 || d[0].TTLSeconds != 0 || + if len(d) != 5 || d[4].Name != MilestonesState || d[4].History < 2 || d[4].TTLSeconds != 0 || d[0].Name != IssuesState || d[0].History < 2 || d[0].TTLSeconds != 0 || d[1].Name != CountersState || d[1].History > 1 || d[1].TTLSeconds != 0 || d[2].Name != GroomingState || d[2].History > 1 || d[2].TTLSeconds != 0 || d[3].Name != ReservationsState || d[3].History > 1 || d[3].TTLSeconds != 0 { diff --git a/modules/mesh-issues/cmd/mesh-issues/memory_test.go b/modules/mesh-issues/cmd/mesh-issues/memory_test.go index 257a573b..a2b84353 100644 --- a/modules/mesh-issues/cmd/mesh-issues/memory_test.go +++ b/modules/mesh-issues/cmd/mesh-issues/memory_test.go @@ -28,7 +28,7 @@ type memory struct { } func newMemory() *memory { - return &memory{data: map[string]map[string]Entry{IssuesState: {}, CountersState: {}, GroomingState: {}, ReservationsState: {}}, fail: map[string]error{}, + return &memory{data: map[string]map[string]Entry{IssuesState: {}, CountersState: {}, GroomingState: {}, ReservationsState: {}, MilestonesState: {}}, fail: map[string]error{}, hist: map[string]map[string][]Entry{}} } diff --git a/modules/mesh-issues/cmd/mesh-issues/milestone.go b/modules/mesh-issues/cmd/mesh-issues/milestone.go new file mode 100644 index 00000000..3d869bf2 --- /dev/null +++ b/modules/mesh-issues/cmd/mesh-issues/milestone.go @@ -0,0 +1,441 @@ +package main + +// Milestones (novox/hq ADR 0493 §1 to §4, issue 1781, to-be 53 §13): each row of a design's milestone table is a record +// of the tracker, kept in the state `milestones` under its design's number and its id. An issue's milestone is the +// issue's own link of kind `milestone`, written in the issue's log like any link, so "an issue belongs to at most one +// milestone" (§2) is checked inside that issue's one write. A milestone's issues are read from the issues kept. + +import ( + "encoding/json" + "errors" + "fmt" + "regexp" + "sort" + "strconv" + "strings" + "time" + + stdio "git.novox.be/novox/mesh-sdk/go" +) + +const ( + // MilestonesState keeps one milestone per key, named by MilestoneKey. + MilestonesState = "milestones" + // EventMilestoneSet is said when a milestone is made or changed, or said again on request. + EventMilestoneSet = "milestone-set" + // EventMilestoneMoved is said when a milestone's status changes. + EventMilestoneMoved = "milestone-moved" + + MilestoneOpen = "open" + MilestoneClosed = "closed" + + // MostOutcome is the longest outcome a milestone keeps. + MostOutcome = 2048 + // MostMilestones is the most milestones one list answers. + MostMilestones = 500 +) + +// MilestoneStatuses is every status a milestone may have. Released is added by the one ask per milestone (issue 1783). +var MilestoneStatuses = []string{MilestoneOpen, MilestoneClosed} + +var ( + milestoneID = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$`) + milestoneRef = regexp.MustCompile(`^([1-9][0-9]{0,2})/(.+)$`) + commitRef = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,99}@[0-9a-f]{40}$`) +) + +// Stamp is who did something and when. +type Stamp struct { + By string `json:"by"` + At time.Time `json:"at"` +} + +// MilestoneSource is where a seeded milestone was read: hq's commit and the design's path. +type MilestoneSource struct { + Commit string `json:"commit"` + Path string `json:"path"` +} + +// Milestone is one milestone as the tracker keeps it. +type Milestone struct { + Design int `json:"design"` + ID string `json:"id"` + Outcome string `json:"outcome"` + // Order is its place in its design's table, first 1. + Order int `json:"order"` + Status string `json:"status"` + Reason string `json:"reason,omitempty"` + Made Stamp `json:"made"` + Moved *Stamp `json:"moved,omitempty"` + Imported *MilestoneSource `json:"imported,omitempty"` +} + +// Ref is the milestone as a link names it: design/id, such as 47/R6. +func (m *Milestone) Ref() string { return MilestoneRef(m.Design, m.ID) } + +// MilestoneRef is design/id. +func MilestoneRef(design int, id string) string { return fmt.Sprintf("%d/%s", design, id) } + +// MilestoneKey is a milestone's key in `milestones`: design.id, such as 47.R6. +func MilestoneKey(design int, id string) string { return fmt.Sprintf("%d.%s", design, id) } + +// canonicalID is a milestone's id as kept: the table's words joined by hyphens ("3 fix" is 3-fix). +func canonicalID(id string) (string, error) { + c := strings.Join(strings.Fields(id), "-") + if !milestoneID.MatchString(c) { + return "", fmt.Errorf("a milestone's id is a short name of letters, digits, - and _, such as R6 or 3-fix, not %q", id) + } + return c, nil +} + +// parseMilestoneRef reads design/id, the id said the one way it is kept. +func parseMilestoneRef(s string) (int, string, error) { + m := milestoneRef.FindStringSubmatch(strings.TrimSpace(s)) + if m == nil { + return 0, "", fmt.Errorf("a milestone is named design/id, such as 47/R6, not %q", s) + } + d, _ := strconv.Atoi(m[1]) + id, err := canonicalID(m[2]) + if err != nil { + return 0, "", err + } + return d, id, nil +} + +// checkMilestoneTarget is a milestone link's target, said the one way it is kept. +func checkMilestoneTarget(s string) (string, bool) { + d, id, err := parseMilestoneRef(s) + if err != nil { + return "", false + } + return MilestoneRef(d, id), true +} + +// readMilestone is a milestone and the revision it was read at; nil when there is none. +func (t *Tracker) readMilestone(design int, id string) (*Milestone, uint64, error) { + e, err := t.Store.Get(MilestonesState, MilestoneKey(design, id)) + if err != nil { + return nil, 0, fmt.Errorf("milestone %s cannot be read: %v", MilestoneRef(design, id), err) + } + if e == nil { + return nil, 0, nil + } + var m Milestone + if err := json.Unmarshal(e.Value, &m); err != nil { + return nil, 0, fmt.Errorf("milestone %s is kept in a shape the tracker cannot read: %v", MilestoneRef(design, id), err) + } + return &m, e.Revision, nil +} + +// milestoneDone says an issue is done for its milestone: wontfix, or resolved with every live fix delivered. +func milestoneDone(is *Issue) bool { + switch is.Status { + case StatusWontfix: + return true + case StatusResolved: + return len(liveFixes(is)) == 0 || allDelivered(is) + } + return false +} + +// milestoneOf is the milestone an issue is linked to, or "". +func milestoneOf(is *Issue) string { + for _, l := range is.Links { + if l.Kind == LinkMilestone { + return l.Target + } + } + return "" +} + +// MilestoneSetAsk is what milestone-set carries. +type MilestoneSetAsk struct { + Design int + ID string + Outcome string + Order int + Announce bool + imported *MilestoneSource +} + +// MilestoneSaid is the answer to milestone-set and milestone-move. +type MilestoneSaid struct { + Milestone Milestone `json:"milestone"` + Made bool `json:"made,omitempty"` + Changed []string `json:"changed,omitempty"` + Said string `json:"said"` + Announced bool `json:"announced"` + Warning string `json:"warning,omitempty"` +} + +// milestoneSet makes a milestone, or changes its outcome or order, at the revision read; with Announce it says the +// record again. +func (t *Tracker) milestoneSet(by string, a MilestoneSetAsk) (*MilestoneSaid, error) { + if a.Design < 1 || a.Design > 999 { + return nil, fmt.Errorf("design is a design's number, 1 to 999, not %d", a.Design) + } + id, err := canonicalID(a.ID) + if err != nil { + return nil, err + } + outcome := strings.TrimSpace(a.Outcome) + if outcome != "" { + if outcome, err = singleLine("outcome", outcome, MostOutcome); err != nil { + return nil, err + } + } + if a.Order < 0 { + return nil, fmt.Errorf("order is its place in its design's table, from 1, not %d", a.Order) + } + for attempt := 0; attempt < issueAttempts; attempt++ { + m, rev, err := t.readMilestone(a.Design, id) + if err != nil { + return nil, err + } + at := t.now() + out := &MilestoneSaid{} + if m == nil { + if outcome == "" { + return nil, fmt.Errorf("milestone %s does not exist; making it needs its outcome", MilestoneRef(a.Design, id)) + } + m = &Milestone{Design: a.Design, ID: id, Outcome: outcome, Order: a.Order, Status: MilestoneOpen, + Made: Stamp{By: by, At: at}, Imported: a.imported} + if _, err := t.Store.Create(MilestonesState, MilestoneKey(a.Design, id), m); err != nil { + if errors.Is(err, stdio.ErrChanged) { + continue + } + return nil, fmt.Errorf("milestone %s could not be written (%v); nothing was made", m.Ref(), err) + } + out.Made = true + out.Said = fmt.Sprintf("Made milestone %s: %s.", m.Ref(), clipText(outcome, 120)) + } else { + if outcome != "" && outcome != m.Outcome { + m.Outcome = outcome + out.Changed = append(out.Changed, "outcome") + } + if a.Order != 0 && a.Order != m.Order { + m.Order = a.Order + out.Changed = append(out.Changed, "order") + } + if len(out.Changed) == 0 && !a.Announce { + out.Milestone = *m + out.Said = fmt.Sprintf("Milestone %s already holds that; nothing changed.", m.Ref()) + return out, nil + } + if len(out.Changed) > 0 { + m.Moved = &Stamp{By: by, At: at} + if _, err := t.Store.Update(MilestonesState, MilestoneKey(a.Design, id), m, rev); err != nil { + if errors.Is(err, stdio.ErrChanged) { + continue + } + return nil, fmt.Errorf("milestone %s could not be written (%v); nothing changed", m.Ref(), err) + } + out.Said = fmt.Sprintf("Changed the %s of milestone %s.", strings.Join(out.Changed, " and "), m.Ref()) + } else { + out.Said = fmt.Sprintf("Said milestone %s again, unchanged.", m.Ref()) + } + } + out.Milestone = *m + out.Announced, out.Warning = t.emit(EventMilestoneSet, map[string]any{"milestone": m.Ref(), "record": m, + "made": out.Made, "changed": out.Changed, "by": by, "at": at}) + return out, nil + } + return nil, fmt.Errorf("milestone %s was changed by others %d times while this was written; nothing changed. Ask again", + MilestoneRef(a.Design, id), issueAttempts) +} + +// milestoneMove opens or closes a milestone, with a reason. Closing is refused while an issue linked to it is neither +// resolved nor wontfix. +func (t *Tracker) milestoneMove(by, ref, status, reason string) (*MilestoneSaid, error) { + design, id, err := parseMilestoneRef(ref) + if err != nil { + return nil, err + } + if status, err = oneOf("status", status, MilestoneStatuses); err != nil { + return nil, err + } + if reason, err = singleLine("reason", reason, MostTitle); err != nil { + return nil, fmt.Errorf("a milestone is moved with its reason: %v", err) + } + if status == MilestoneClosed { + all, err := t.allIssues() + if err != nil { + return nil, err + } + var open []string + for _, n := range sortedNumbers(all) { + is := all[n] + if milestoneOf(is) == MilestoneRef(design, id) && is.Status != StatusResolved && is.Status != StatusWontfix { + open = append(open, fmt.Sprintf("%d (%s)", n, is.Status)) + } + } + if len(open) > 0 { + return nil, fmt.Errorf("milestone %s keeps issues neither resolved nor wontfix: %s. Resolve them, or move them "+ + "to another milestone, before it is closed", MilestoneRef(design, id), strings.Join(open, ", ")) + } + } + for attempt := 0; attempt < issueAttempts; attempt++ { + m, rev, err := t.readMilestone(design, id) + if err != nil { + return nil, err + } + if m == nil { + return nil, fmt.Errorf("there is no milestone %s", MilestoneRef(design, id)) + } + if m.Status == status { + return &MilestoneSaid{Milestone: *m, Said: fmt.Sprintf("Milestone %s is %s already; nothing changed.", m.Ref(), status)}, nil + } + from, at := m.Status, t.now() + m.Status, m.Reason, m.Moved = status, reason, &Stamp{By: by, At: at} + if _, err := t.Store.Update(MilestonesState, MilestoneKey(design, id), m, rev); err != nil { + if errors.Is(err, stdio.ErrChanged) { + continue + } + return nil, fmt.Errorf("milestone %s could not be written (%v); nothing changed", m.Ref(), err) + } + out := &MilestoneSaid{Milestone: *m, Changed: []string{"status"}, + Said: fmt.Sprintf("Milestone %s moved from %s to %s: %s.", m.Ref(), from, status, reason)} + out.Announced, out.Warning = t.emit(EventMilestoneMoved, map[string]any{"milestone": m.Ref(), "from": from, + "to": status, "reason": reason, "by": by, "at": at}) + return out, nil + } + return nil, fmt.Errorf("milestone %s was changed by others %d times while this was written; nothing changed. Ask again", + MilestoneRef(design, id), issueAttempts) +} + +// MilestonesAsk narrows the milestones read. +type MilestonesAsk struct { + Design int + Status string + // One is one milestone, design/id: answered with its history. + One string +} + +// MilestoneRow is one milestone with its issues. +type MilestoneRow struct { + Milestone + Ref string `json:"ref"` + Issues []uint64 `json:"issues"` + ByStatus map[string]int `json:"by-status,omitempty"` + // Done is how many of its issues are wontfix, or resolved with their fixes delivered. + Done int `json:"done"` +} + +// MilestonesRead is the answer to milestones. +type MilestonesRead struct { + Milestones []MilestoneRow `json:"milestones"` + // History is every value one milestone's key keeps, oldest first, when one was asked. + History []json.RawMessage `json:"history,omitempty"` + Said string `json:"said"` +} + +// milestones reads the milestones, ordered by design and their place in its table, each with its issues. +func (t *Tracker) milestones(a MilestonesAsk) (*MilestonesRead, error) { + status := strings.TrimSpace(a.Status) + if status != "" { + var err error + if status, err = oneOf("status", status, MilestoneStatuses); err != nil { + return nil, err + } + } + var ms []*Milestone + out := &MilestonesRead{Milestones: []MilestoneRow{}} + if strings.TrimSpace(a.One) != "" { + d, id, err := parseMilestoneRef(a.One) + if err != nil { + return nil, err + } + m, _, err := t.readMilestone(d, id) + if err != nil { + return nil, err + } + if m == nil { + return nil, fmt.Errorf("there is no milestone %s", MilestoneRef(d, id)) + } + ms = append(ms, m) + hist, err := t.Store.History(MilestonesState, MilestoneKey(d, id)) + if err != nil { + return nil, fmt.Errorf("milestone %s's history cannot be read: %v", m.Ref(), err) + } + for _, h := range hist { + out.History = append(out.History, h.Value) + } + } else { + keys, err := t.Store.Keys(MilestonesState) + if err != nil { + return nil, fmt.Errorf("the milestones cannot be listed: %v", err) + } + for _, k := range keys { + ds, id, ok := strings.Cut(k, ".") + d, err := strconv.Atoi(ds) + if !ok || err != nil || (a.Design != 0 && d != a.Design) { + continue + } + m, _, err := t.readMilestone(d, id) + if err != nil { + return nil, err + } + if m == nil || (status != "" && m.Status != status) { + continue + } + ms = append(ms, m) + } + } + sort.Slice(ms, func(i, j int) bool { + if ms[i].Design != ms[j].Design { + return ms[i].Design < ms[j].Design + } + if ms[i].Order != ms[j].Order { + return ms[i].Order < ms[j].Order + } + return ms[i].ID < ms[j].ID + }) + if len(ms) > MostMilestones { + ms = ms[:MostMilestones] + } + all, err := t.allIssues() + if err != nil { + return nil, err + } + rows := map[string]*MilestoneRow{} + for _, m := range ms { + out.Milestones = append(out.Milestones, MilestoneRow{Milestone: *m, Ref: m.Ref(), Issues: []uint64{}, ByStatus: map[string]int{}}) + } + for i := range out.Milestones { + rows[out.Milestones[i].Ref] = &out.Milestones[i] + } + for _, n := range sortedNumbers(all) { + is := all[n] + r := rows[milestoneOf(is)] + if r == nil { + continue + } + r.Issues = append(r.Issues, n) + r.ByStatus[is.Status]++ + if milestoneDone(is) { + r.Done++ + } + } + out.Said = fmt.Sprintf("%d milestones.", len(out.Milestones)) + if len(ms) == MostMilestones { + out.Said += fmt.Sprintf(" At most %d are answered: narrow by design.", MostMilestones) + } + return out, nil +} + +// sortedNumbers is the issues' numbers, lowest first. +func sortedNumbers(all map[uint64]*Issue) []uint64 { + ns := make([]uint64, 0, len(all)) + for n := range all { + ns = append(ns, n) + } + sort.Slice(ns, func(i, j int) bool { return ns[i] < ns[j] }) + return ns +} + +func clipText(s string, n int) string { + if r := []rune(s); len(r) > n { + return string(r[:n-1]) + "…" + } + return s +} diff --git a/modules/mesh-issues/cmd/mesh-issues/milestone_bus_test.go b/modules/mesh-issues/cmd/mesh-issues/milestone_bus_test.go new file mode 100644 index 00000000..5965cadd --- /dev/null +++ b/modules/mesh-issues/cmd/mesh-issues/milestone_bus_test.go @@ -0,0 +1,84 @@ +//go:build integration + +package main + +import ( + "encoding/json" + "fmt" + "strings" + "sync" + "testing" +) + +// Issue 1781 (ADR 0493 §1, §2, §4), the module whole over a real bus: a milestone made by its verb is kept in the +// state the manifest declares, and two instances linking one issue to two milestones at the same moment leave it in +// exactly one; the other is refused naming the first, and the milestones and the list say the same. +func TestAnIssueRaceIntoTwoMilestonesOverTheBusEndsInOne(t *testing.T) { + b := newBus(t) + var events []event + var evMu sync.Mutex + settings := writeSettings(t, 365) + one, two := b.start(t, settings, &events, &evMu), b.start(t, settings, &events, &evMu) + must := func(raw json.RawMessage, err error) json.RawMessage { + t.Helper() + if err != nil { + t.Fatal(err) + } + return raw + } + must(one.call("open", map[string]any{"title": "a milestone's issue", "report": "seen"})) + must(one.call("milestone-set", map[string]any{"design": 47, "id": "R6", "outcome": "nodes apply their wants", "order": 1})) + must(two.call("milestone-set", map[string]any{"design": 47, "id": "3 fix", "outcome": "a group merges in one pass", "order": 2})) + errs := make([]error, 2) + var wg sync.WaitGroup + for k, c := range []struct { + i *instance + target string + }{{one, "47/R6"}, {two, "47/3-fix"}} { + wg.Add(1) + go func() { + defer wg.Done() + _, errs[k] = c.i.call("link", map[string]any{"number": 365, "kind": "milestone", "target": c.target}) + }() + } + wg.Wait() + if (errs[0] == nil) == (errs[1] == nil) { + t.Fatalf("both or neither linked: %v", errs) + } + refused := errs[0] + if refused == nil { + refused = errs[1] + } + if !strings.Contains(refused.Error(), "is in milestone") { + t.Fatalf("refused with %v", refused) + } + var read MilestonesRead + _ = json.Unmarshal(must(two.call("milestones", map[string]any{"design": 47})), &read) + if len(read.Milestones) != 2 || read.Milestones[0].ID != "R6" || read.Milestones[1].ID != "3-fix" || + len(read.Milestones[0].Issues)+len(read.Milestones[1].Issues) != 1 { + t.Fatalf("%+v", read.Milestones) + } + in := "47/R6" + if len(read.Milestones[1].Issues) == 1 { + in = "47/3-fix" + } + var l Listed + _ = json.Unmarshal(must(one.call("list", map[string]any{"linked": "milestone:" + in})), &l) + if len(l.Rows) != 1 || l.Rows[0].Number != 365 { + t.Fatalf("listed %+v", l) + } + var hist MilestonesRead + _ = json.Unmarshal(must(one.call("milestones", map[string]any{"one": "47/R6"})), &hist) + if len(hist.History) != 1 { + t.Fatalf("history %d values", len(hist.History)) + } + evMu.Lock() + defer evMu.Unlock() + var kinds []string + for _, e := range events { + kinds = append(kinds, e.Key) + } + if fmt.Sprint(kinds) != "[opened milestone-set milestone-set linked]" { + t.Fatalf("events %v", kinds) + } +} diff --git a/modules/mesh-issues/cmd/mesh-issues/milestone_test.go b/modules/mesh-issues/cmd/mesh-issues/milestone_test.go new file mode 100644 index 00000000..13ff1b50 --- /dev/null +++ b/modules/mesh-issues/cmd/mesh-issues/milestone_test.go @@ -0,0 +1,187 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" +) + +// Issue 1781 (ADR 0493 §1 to §4): a milestone is a record made by its verb; an issue is linked to at most one, only to +// an open one, and only by the verb; the triage links no milestone, pull request or commit. +func TestAMilestoneIsMadeByItsVerbAndAnIssueBelongsToAtMostOne(t *testing.T) { + h := newHarness(t) + a, b := h.open(t, "a"), h.open(t, "b") + for _, c := range []struct { + ask MilestoneSetAsk + says string + }{ + {MilestoneSetAsk{Design: 0, ID: "R6", Outcome: "x"}, "design's number"}, + {MilestoneSetAsk{Design: 47, ID: "R6/x", Outcome: "x"}, "short name"}, + {MilestoneSetAsk{Design: 47, ID: "R6"}, "needs its outcome"}, + {MilestoneSetAsk{Design: 47, ID: "R6", Outcome: "nodes apply their wants", Order: 2}, ""}, + {MilestoneSetAsk{Design: 47, ID: "3 fix", Outcome: "a group merges in one pass", Order: 1}, ""}, + {MilestoneSetAsk{Design: 47, ID: "R6", Outcome: "nodes apply their wants"}, "nothing changed"}, + } { + out, err := h.milestoneSet("lead@shanks", c.ask) + if c.says == "" && err != nil || c.says != "" && (err == nil || !strings.Contains(err.Error(), c.says)) && + (out == nil || !strings.Contains(out.Said, c.says)) { + t.Fatalf("%+v: want %q, got %+v %v", c.ask, c.says, out, err) + } + } + links := []struct { + l LinkAsk + says string + }{ + {LinkAsk{Number: a, Kind: LinkMilestone, Target: "47/R6"}, ""}, + {LinkAsk{Number: a, Kind: LinkMilestone, Target: "47/3 fix"}, "in milestone 47/R6 already"}, + {LinkAsk{Number: b, Kind: LinkMilestone, Target: "47/R9"}, "there is no milestone 47/R9"}, + {LinkAsk{Number: b, Kind: LinkMilestone, Target: "R6"}, "design/id"}, + {LinkAsk{Number: b, Kind: LinkCommit, Target: "mesh-catalog@" + strings.Repeat("a", 40)}, ""}, + {LinkAsk{Number: b, Kind: LinkCommit, Target: "mesh-catalog@abc123"}, "full 40-character"}, + {LinkAsk{Number: a, Kind: LinkMilestone, Target: "47/R6", Remove: true}, ""}, + {LinkAsk{Number: a, Kind: LinkMilestone, Target: "47/3-fix"}, ""}, + {LinkAsk{Number: b, Kind: LinkMilestone, Target: "47/R6"}, ""}, + } + for _, c := range links { + _, err := h.link("lead@shanks", c.l) + if c.says == "" && err != nil || c.says != "" && (err == nil || !strings.Contains(err.Error(), c.says)) { + t.Fatalf("%+v: want %q, got %v", c.l, c.says, err) + } + } + for _, kind := range madeByPeople { + target := map[string]string{LinkPullRequest: "mesh-catalog#1", LinkMilestone: "47/R6", LinkCommit: "mesh-catalog@" + strings.Repeat("b", 40)}[kind] + if _, err := h.link(GroomBy, LinkAsk{Number: a, Kind: kind, Target: target}); err == nil || !strings.Contains(err.Error(), "triage links no") { + t.Fatalf("the triage linked %s: %v", kind, err) + } + } + read, err := h.milestones(MilestonesAsk{Design: 47}) + if err != nil { + t.Fatal(err) + } + if len(read.Milestones) != 2 || read.Milestones[0].Ref != "47/3-fix" || read.Milestones[1].Ref != "47/R6" || + fmt.Sprint(read.Milestones[0].Issues) != fmt.Sprint([]uint64{a}) || fmt.Sprint(read.Milestones[1].Issues) != fmt.Sprint([]uint64{b}) { + t.Fatalf("%+v", read.Milestones) + } + one, err := h.milestones(MilestonesAsk{One: "47/R6"}) + if err != nil || len(one.Milestones) != 1 || len(one.History) == 0 || one.Milestones[0].Made.By != "lead@shanks" { + t.Fatalf("%+v %v", one, err) + } + // Closing waits for its issues; a closed milestone takes no new one; each change is said. + if _, err := h.milestoneMove("lead@shanks", "47/R6", MilestoneClosed, "delivered"); err == nil || !strings.Contains(err.Error(), fmt.Sprint(b)) { + t.Fatalf("closed over an open issue: %v", err) + } + if _, err := h.Move(MoveAsk{Number: b, Status: StatusWontfix, Reason: "not needed"}); err != nil { + t.Fatal(err) + } + if _, err := h.milestoneMove("lead@shanks", "47/R6", MilestoneClosed, "delivered"); err != nil { + t.Fatal(err) + } + if _, err := h.link("lead@shanks", LinkAsk{Number: a, Kind: LinkMilestone, Target: "47/R6", Remove: true}); err == nil { + t.Fatal("a removed a link it does not have") + } + c := h.open(t, "c") + if _, err := h.link("lead@shanks", LinkAsk{Number: c, Kind: LinkMilestone, Target: "47/R6"}); err == nil || !strings.Contains(err.Error(), "is closed") { + t.Fatalf("linked to a closed milestone: %v", err) + } + if out, err := h.milestoneSet("lead@shanks", MilestoneSetAsk{Design: 47, ID: "R6", Announce: true}); err != nil || !out.Announced { + t.Fatalf("announce: %+v %v", out, err) + } + var kinds []string + for _, e := range h.said() { + if strings.HasPrefix(e.Event, "milestone") { + kinds = append(kinds, e.Event+":"+fmt.Sprint(e.Body["milestone"])) + } + } + if strings.Join(kinds, " ") != "milestone-set:47/R6 milestone-set:47/3-fix milestone-moved:47/R6 milestone-set:47/R6" { + t.Fatalf("said %v", kinds) + } +} + +// listRecords is a record holding one design document. +type listRecords struct { + path, content string +} + +func (r listRecords) Standing() (string, error) { + return "0123456789abcdef0123456789abcdef01234567", nil +} +func (r listRecords) List(string) ([]string, []string, error) { + return nil, []string{"12-other.md", filepath.Base(r.path)}, nil +} +func (r listRecords) Read(string) (string, string, error) { return r.content, "", nil } + +// Issue 1781: the seed makes each row's milestone, links what one row names, refuses what two rows name or another +// milestone holds, closes a milestone whose issues are done, and run again changes nothing. +func TestTheSeedTakesADesignsTableOnce(t *testing.T) { + h := newHarness(t) + a, b, c, d := h.open(t, "a"), h.open(t, "b"), h.open(t, "c"), h.open(t, "d") + if _, err := h.Move(MoveAsk{Number: d, Status: StatusWontfix, Reason: "folded"}); err != nil { + t.Fatal(err) + } + table := fmt.Sprintf("# 47 — Delivery\n\n## Milestones\n\n| # | Outcome | Issues |\n|---|---|---|\n"+ + "| 1 | the first | %d, %d (ADR 0493 §2) |\n| 3 fix | a fix | %d, %d, 999 |\n| R4d | none yet | opened when it is prepared |\n"+ + "| 6 | done | %d |\n\n## Next\n\n| a | Issues |\n|---|---|\n| x | %d |\n", a, b, c, b, d, a) + rec := listRecords{path: "03-DESIGN/01-to-be/47-delivery.md", content: table} + dry, err := h.importMilestones("lead@shanks", MilestoneImportAsk{Design: 47, DryRun: true}, rec) + if err != nil { + t.Fatal(err) + } + if dry.Made != 4 || dry.Linked != 3 || dry.Refused != 2 || dry.Closed != 1 || len(dry.Rows[1].Missing) != 1 { + t.Fatalf("dry run %+v", dry) + } + if read, _ := h.milestones(MilestonesAsk{}); len(read.Milestones) != 0 { + t.Fatalf("a dry run wrote %+v", read.Milestones) + } + got, err := h.importMilestones("lead@shanks", MilestoneImportAsk{Design: 47}, rec) + if err != nil { + t.Fatal(err) + } + if fmt.Sprintf("%d %d %d %d", got.Made, got.Linked, got.Refused, got.Closed) != "4 3 2 1" { + t.Fatalf("%+v", got) + } + read, _ := h.milestones(MilestonesAsk{Design: 47}) + var say []string + for _, m := range read.Milestones { + say = append(say, fmt.Sprintf("%s:%s:%v", m.ID, m.Status, m.Issues)) + } + if want := fmt.Sprintf("1:open:[%d] 3-fix:open:[%d] R4d:open:[] 6:closed:[%d]", a, c, d); strings.Join(say, " ") != want { + t.Fatalf("got %s, want %s", strings.Join(say, " "), want) + } + if read.Milestones[3].Imported == nil || read.Milestones[3].Imported.Path != rec.path { + t.Fatalf("%+v", read.Milestones[3]) + } + again, err := h.importMilestones("lead@shanks", MilestoneImportAsk{Design: 47}, rec) + if err != nil || again.Made+again.Linked+again.Closed != 0 || !strings.HasPrefix(again.Said, "Changed nothing") { + t.Fatalf("again %+v %v", again, err) + } + if _, err := h.importMilestones("lead@shanks", MilestoneImportAsk{Design: 48}, rec); err == nil { + t.Fatal("seeded a design the record does not hold") + } +} + +// Issue 1781 S1: design 47's table, as hq's branch writes it, names every issue in one row and nothing in words the +// seed would guess. +func TestDesign47sTableNamesEachIssueOnce(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("testdata", "milestones", "47-delivery-from-commit-to-delivered.md")) + if err != nil { + t.Fatal(err) + } + rows, warn := parseTable(string(raw)) + if len(rows) != 33 || len(warn) != 0 { + t.Fatalf("%d rows, warnings %v", len(rows), warn) + } + seen := map[uint64]string{} + for _, r := range rows { + if _, err := canonicalID(r.ID); err != nil { + t.Error(err) + } + for _, n := range r.Issues { + if other, ok := seen[n]; ok { + t.Errorf("issue %d is in rows %s and %s", n, other, r.ID) + } + seen[n] = r.ID + } + } +} diff --git a/modules/mesh-issues/cmd/mesh-issues/milestoneimport.go b/modules/mesh-issues/cmd/mesh-issues/milestoneimport.go new file mode 100644 index 00000000..c582a2d6 --- /dev/null +++ b/modules/mesh-issues/cmd/mesh-issues/milestoneimport.go @@ -0,0 +1,313 @@ +package main + +// The seed of a design's milestones (novox/hq issue 1781, to-be 53 §13): its `## Milestones` table, read once through +// the records module, made into milestone records with their issues linked. The table parse moved here from the +// knowledge base's progress (issue 1947), which reads the records since. The Issues cell is prose, so what the parse +// cannot read for certain it says, and an issue two rows name is refused, never placed by a guess (ADR 0493 §2). + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" +) + +// DesignsFolder is where the designs being built are, in hq. +const DesignsFolder = "03-DESIGN/01-to-be" + +// TableRow is one row of a design's milestone table. +type TableRow struct { + ID string + Outcome string + Issues []uint64 +} + +var ( + designFile = regexp.MustCompile(`^([0-9]{2,3})-[a-z0-9-]+\.md$`) + // 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 + +// parseTable reads a design's milestone table: its rows in order, and what it could not read for certain. +func parseTable(content string) ([]TableRow, []string) { + var rows []TableRow + var warn []string + lines := strings.Split(content, "\n") + inMilestones, header := false, -1 + issueCol, outcomeCol := -1, -1 + for i, raw := range lines { + l := strings.TrimSpace(raw) + 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 + } + r := TableRow{ID: cells[0]} + if outcomeCol >= 0 && outcomeCol < len(cells) { + r.Outcome = cells[outcomeCol] + } + var w []string + r.Issues, w = issuesOf(cells[issueCol]) + for _, x := range w { + warn = append(warn, fmt.Sprintf("milestone %s: %s", r.ID, x)) + } + rows = append(rows, r) + } + return rows, warn +} + +// 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) ([]uint64, []string) { + var warn []string + s := notAnIssue.ReplaceAllString(cell, " ") + var nums []uint64 + 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, uint64(n)) + } + } + but := map[uint64]bool{} + for _, m := range butOne.FindAllStringSubmatch(s, -1) { + n, _ := strconv.Atoi(m[1]) + but[uint64(n)] = true + } + s = rangeWords.ReplaceAllString(s, " ") + s = butOne.ReplaceAllString(s, " ") + for _, m := range issueNum.FindAllString(s, -1) { + n, _ := strconv.Atoi(m) + nums = append(nums, uint64(n)) + } + out, have := []uint64{}, map[uint64]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 +} + +// MilestoneImportAsk is what a seed of one design's milestones carries. +type MilestoneImportAsk struct { + Design int + DryRun bool + Commit string +} + +// MilestoneImportRow is what the seed did, or would do, for one row of the table. +type MilestoneImportRow struct { + Milestone string `json:"milestone"` + Made bool `json:"made,omitempty"` + Linked []uint64 `json:"linked,omitempty"` + // Kept is the issues linked to it already. + Kept []uint64 `json:"kept,omitempty"` + // Refused is each issue not linked, and why. + Refused []string `json:"refused,omitempty"` + // Missing is the numbers the table names that are no issue. + Missing []uint64 `json:"missing,omitempty"` + Closed bool `json:"closed,omitempty"` +} + +// MilestonesImported is the answer to a seed. +type MilestonesImported struct { + DryRun bool `json:"dry-run"` + Design int `json:"design"` + Path string `json:"path"` + Commit string `json:"commit"` + Rows []MilestoneImportRow `json:"rows"` + Made int `json:"made"` + Linked int `json:"linked"` + Closed int `json:"closed"` + Refused int `json:"refused"` + Warnings []string `json:"warnings,omitempty"` + Said string `json:"said"` +} + +// importMilestones seeds one design's milestones from its table. +func (t *Tracker) importMilestones(by string, a MilestoneImportAsk, records Records) (*MilestonesImported, error) { + if a.Design < 1 || a.Design > 999 { + return nil, fmt.Errorf("milestones is a design's number, 1 to 999, not %d", a.Design) + } + commit, err := records.Standing() + if err != nil { + return nil, fmt.Errorf("the record cannot be read (%v); nothing was imported", err) + } + want := strings.TrimSpace(a.Commit) + if want != "" && !commitPrefix.MatchString(want) { + return nil, errors.New("commit is a commit's id or a prefix of it, 7 to 40 hex digits; nothing was imported") + } + if want != "" && !strings.HasPrefix(commit, want) { + return nil, fmt.Errorf("the record stands at hq commit %s, not %s: bring it up to date (records_sync), or "+ + "name the commit it stands at. Nothing was imported", short(commit), want) + } + _, docs, err := records.List(DesignsFolder) + if err != nil { + return nil, fmt.Errorf("the record's %s cannot be listed (%v); nothing was imported", DesignsFolder, err) + } + path := "" + for _, d := range docs { + if m := designFile.FindStringSubmatch(d); m != nil { + if n, _ := strconv.Atoi(m[1]); n == a.Design { + path = DesignsFolder + "/" + d + } + } + } + if path == "" { + return nil, fmt.Errorf("no design %d is in %s at hq %s; nothing was imported", a.Design, DesignsFolder, short(commit)) + } + content, _, err := records.Read(path) + if err != nil { + return nil, fmt.Errorf("%s cannot be read (%v); nothing was imported", path, err) + } + table, warn := parseTable(content) + if len(table) == 0 { + return nil, fmt.Errorf("%s has no milestone table with an Issues column; nothing was imported", path) + } + out := &MilestonesImported{DryRun: a.DryRun, Design: a.Design, Path: path, Commit: commit, Rows: []MilestoneImportRow{}, Warnings: warn} + + // An issue two rows name is placed by nobody's guess (ADR 0493 §2). + rowsOf := map[uint64][]string{} + for _, r := range table { + for _, n := range r.Issues { + rowsOf[n] = append(rowsOf[n], r.ID) + } + } + all, err := t.allIssues() + if err != nil { + return nil, err + } + for i, r := range table { + id, err := canonicalID(r.ID) + if err != nil { + out.Warnings = append(out.Warnings, fmt.Sprintf("row %d: %v; skipped", i+1, err)) + continue + } + ref := MilestoneRef(a.Design, id) + row := MilestoneImportRow{Milestone: ref} + m, _, err := t.readMilestone(a.Design, id) + if err != nil { + return nil, err + } + if m == nil { + row.Made = true + out.Made++ + if !a.DryRun { + outcome := r.Outcome + if outcome == "" { + outcome = "(the table gives no outcome)" + } + if _, err := t.milestoneSet(by, MilestoneSetAsk{Design: a.Design, ID: id, Outcome: clipText(outcome, MostOutcome), + Order: i + 1, imported: &MilestoneSource{Commit: commit, Path: path}}); err != nil { + return nil, fmt.Errorf("%v; the rows before %s are kept, and an import run again goes on from there", err, ref) + } + } + m = &Milestone{Design: a.Design, ID: id, Status: MilestoneOpen} + } + var members []*Issue + for _, n := range r.Issues { + is := all[n] + switch { + case is == nil: + row.Missing = append(row.Missing, n) + case len(rowsOf[n]) > 1: + row.Refused = append(row.Refused, fmt.Sprintf("%d: rows %s all name it; an issue belongs to at most one", n, strings.Join(rowsOf[n], ", "))) + case milestoneOf(is) == ref: + row.Kept = append(row.Kept, n) + members = append(members, is) + case milestoneOf(is) != "": + row.Refused = append(row.Refused, fmt.Sprintf("%d: in milestone %s already", n, milestoneOf(is))) + case m.Status != MilestoneOpen: + row.Refused = append(row.Refused, fmt.Sprintf("%d: the milestone is %s", n, m.Status)) + default: + if !a.DryRun { + if _, err := t.link(by, LinkAsk{Number: n, Kind: LinkMilestone, Target: ref}); err != nil { + row.Refused = append(row.Refused, fmt.Sprintf("%d: %v", n, err)) + continue + } + } + row.Linked = append(row.Linked, n) + members = append(members, is) + } + } + out.Linked += len(row.Linked) + out.Refused += len(row.Refused) + if m.Status == MilestoneOpen && len(members) > 0 && len(row.Refused) == 0 { + done := true + for _, is := range members { + done = done && milestoneDone(is) + } + if done { + row.Closed = true + out.Closed++ + if !a.DryRun { + if _, err := t.milestoneMove(by, ref, MilestoneClosed, + fmt.Sprintf("seeded closed: every issue done (hq %s)", short(commit))); err != nil { + return nil, err + } + } + } + } + out.Rows = append(out.Rows, row) + } + verb := map[bool]string{true: "Would make", false: "Made"}[a.DryRun] + if out.Made+out.Linked+out.Closed == 0 { + out.Said = fmt.Sprintf("Changed nothing: design %d's %d milestones hold what its table names", a.Design, len(out.Rows)) + } else { + out.Said = fmt.Sprintf("%s %d milestones of design %d, linked %d issues and closed %d, from %s at hq %s", + verb, out.Made, a.Design, out.Linked, out.Closed, path, short(commit)) + } + if out.Refused > 0 { + out.Said += fmt.Sprintf("; refused %d issues, each named in its row", out.Refused) + } + out.Said += "." + return out, nil +} diff --git a/modules/mesh-issues/cmd/mesh-issues/testdata/milestones/47-delivery-from-commit-to-delivered.md b/modules/mesh-issues/cmd/mesh-issues/testdata/milestones/47-delivery-from-commit-to-delivered.md new file mode 100644 index 00000000..b0109599 --- /dev/null +++ b/modules/mesh-issues/cmd/mesh-issues/testdata/milestones/47-delivery-from-commit-to-delivered.md @@ -0,0 +1,1146 @@ +--- +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** (issue 774; what remains of issue 1126, the homes of the controller's + other jobs, carries into it by a note on 1126 and is not one of its issues): 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) | — | its issue is R4g's (the installed toolchains); the builder's own forge key moved to release 4 (R4c) | +| 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 R3c's fingerprint issue | +| 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 (its issue is R3d's); 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 (their issue is R4g's) | +| 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, 1354, 1355, 1356, 1357, 1358, 1359, 1360, 1361, 1362, 1363, 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), the assigner's composition check (its issue is R4g's), 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 of R4g's issue on the old checks, 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 | the installed toolchains (their issue is R4g's), 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) | 1300, 1301, 1302 (one status); the composition check deleted (closing issue 709, the replays' socket), ADR 0482 §8 and the controller's deletions are R4g's issues | +| 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), the bus's user list granting what release 6 uses (delivered first, under L1's issue), 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), 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) | +| L1 | The controller leaves build and delivery, first milestone: the bus node follows the user list alone, the grants part composes in shadow and the vault holds the bus accounts' hashes, so the operator's take-over can make the grants part's list the one loaded (ADR 0547 §8, §11; ADR 0550 §4) | — | mesh-controller, mesh-delivery, mesh-catalog (the vault, the grants part), the node-engine (`mesh-host`), mesh-sdk | 1945 | +| L2 | Nodes compose: each node agent composes its own modules, and no node applies a composition made elsewhere but the bus's user list (ADR 0547 §11) | — | the node-engine (`mesh-host`), mesh-sdk, mesh-controller | 1126 | +| L3 | Nothing is left with the controller: the vault mints bus accounts and signs, the grants part writes grants, the try is the checker's own verb, placement is read from the catalogue alone, and the lease passes to a holder that is no controller (ADR 0547 §11, ADR 0549 §5, §6, ADR 0550 §2); the rest of the controller's code lines is the issue of L2 | — | mesh-controller, mesh-catalog (the vault, the grants part), mesh-delivery | 774 | +| M1 | A milestone is a record of the issue tracker: each row of a design's milestone table is a milestone record its issues are linked to by `issue-tracker.link`, an issue to at most one, and pull requests and commits to issues the same way; the knowledge base's `progress` reads the milestones from the tracker, not from this table (ADR 0493 §1 to §4) | — (ADR 0493 §13 in part) | hq, mesh-catalog (mesh-issues, mesh-knowledge) | 1781 | + +An issue is in one row only (ADR 0493 §2): where a row named an issue another row names too, the last row keeps it and the +earlier row says so in words (the operator, 2026-10-26). Milestone M1 makes each row a record of the issue tracker +(issue 1781): once its seed has run, the tracker is the only list of a milestone's issues, `issue-tracker.milestones` +reads them, and this table keeps its rows without the Issues column (the operator, 2026-10-26). + +Milestones L1 to L3 are the three of "The controller leaves build and delivery" above (ADR 0547 §11), given rows on +2026-10-26 so that a reader of this table, the knowledge base's `progress` among them (issue 1947), sees the work that +runs; 1945 is L1's and 1126 L2's only, as an issue belongs to at most one milestone (ADR 0493 §2); R6, delivered, +names the parts of 1945 it carried in words. 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-issues/cmd/mesh-issues/tracker.go b/modules/mesh-issues/cmd/mesh-issues/tracker.go index dd8fc4ac..2f06b304 100644 --- a/modules/mesh-issues/cmd/mesh-issues/tracker.go +++ b/modules/mesh-issues/cmd/mesh-issues/tracker.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "math/big" + "slices" "sort" "strings" "sync" @@ -650,6 +651,22 @@ func (t *Tracker) link(by string, a LinkAsk) (*Linked, error) { } } } + if by == GroomBy && slices.Contains(madeByPeople, kind) { + return nil, fmt.Errorf("the triage links no %s: that link is made by the one who did the work (ADR 0493 §4)", kind) + } + if kind == LinkMilestone && !a.Remove { + d, id, _ := parseMilestoneRef(target) + m, _, err := t.readMilestone(d, id) + if err != nil { + return nil, err + } + if m == nil { + return nil, fmt.Errorf("there is no milestone %s: it is made from its design's table (milestone-set, or import with milestones)", target) + } + if m.Status != MilestoneOpen { + return nil, fmt.Errorf("milestone %s is %s: a fix found on the way joins its design's open milestone, or a milestone of its own (ADR 0493 §3)", target, m.Status) + } + } at := t.now() is, err := t.change(a.Number, Act{Op: EventLinked, By: by}, func(is *Issue) error { for i, l := range is.Links { @@ -665,6 +682,12 @@ func (t *Tracker) link(by string, a LinkAsk) (*Linked, error) { if a.Remove { return fmt.Errorf("issue %d has no %s link to %s to remove", is.Number, kind, target) } + if kind == LinkMilestone { + if in := milestoneOf(is); in != "" { + return fmt.Errorf("issue %d is in milestone %s already, and an issue belongs to at most one (ADR 0493 §2): "+ + "remove that link first to move it", is.Number, in) + } + } if len(is.Links) >= MostLinks { return fmt.Errorf("issue %d has %d links, the most one issue keeps", is.Number, MostLinks) } diff --git a/modules/mesh-issues/module.json b/modules/mesh-issues/module.json index ee14150a..df1e62f8 100644 --- a/modules/mesh-issues/module.json +++ b/modules/mesh-issues/module.json @@ -13,6 +13,9 @@ "move", "note", "link", + "milestones", + "milestone-set", + "milestone-move", "import", "groom", "set", @@ -33,6 +36,9 @@ "move", "note", "link", + "milestones", + "milestone-set", + "milestone-move", "import", "groom", "set", @@ -52,7 +58,9 @@ "triaged", "ready", "ready-to-verify", - "reserved" + "reserved", + "milestone-set", + "milestone-moved" ], "consumes": [ "mesh-issues.opened", @@ -81,7 +89,11 @@ }, "counters", "grooming", - "reservations" + "reservations", + { + "name": "milestones", + "history": 5 + } ], "config": { "first-number": { diff --git a/modules/mesh-knowledge/README.md b/modules/mesh-knowledge/README.md index 943c8c89..c684e722 100644 --- a/modules/mesh-knowledge/README.md +++ b/modules/mesh-knowledge/README.md @@ -27,7 +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` | +| `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. A design's title and state are read through the records module, cached by hq's commit; its milestones and their issues are the issue tracker's milestone records (`issue-tracker.milestones`, issue 1781), asked once per answer; a design with none is refused, naming the seed (`issue-tracker.import` with `milestones`) | `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 index adffa13b..8ab4e918 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/design.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/design.go @@ -1,15 +1,9 @@ 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. +// A design as the progress verb reads it (novox/hq issue 1947): its title, its state as its frontmatter says it and +// the decisions it names, read through the records module until ADR 0423 makes designs records of the knowledge base +// (issue 428). Its milestones are the issue tracker's records (ADR 0493 §1, issue 1781), read in milestones.go: the +// table parse that guessed them from prose moved to the tracker's seed and is gone from here. import ( "fmt" @@ -30,31 +24,24 @@ type Design struct { Status string Decisions []string Milestones []Milestone - Warnings []string } -// Milestone is one row of a design's milestone table. +// Milestone is one milestone of a design, as the issue tracker keeps it. type Milestone struct { ID string Outcome string Issues []int + // Status is open or closed, with its Reason; a closed milestone holding no issue is done. + Status string + Reason string } 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} @@ -78,117 +65,14 @@ func parseDesign(path, content string) *Design { } } } - 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]) - } + for i := body; i < len(lines) && d.Title == ""; i++ { + if m := titleLine.FindStringSubmatch(strings.TrimSpace(lines[i])); 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:] @@ -196,11 +80,12 @@ func lastPart(p string) string { return p } -// milestone finds a milestone by its id, as the table writes it, without regard to case or spaces. +// milestone finds a milestone by its id, without regard to case, or to spaces against hyphens. func (d *Design) milestone(id string) *Milestone { - want := strings.ToLower(strings.Join(strings.Fields(id), " ")) + // The tracker keeps an id's words joined by hyphens (3 fix is 3-fix); either is taken. + 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 { + if strings.ToLower(strings.Join(strings.Fields(d.Milestones[i].ID), "-")) == want { return &d.Milestones[i] } } diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/main.go b/modules/mesh-knowledge/cmd/mesh-knowledge/main.go index 4c0cc099..9c009b8b 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/main.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/main.go @@ -1129,9 +1129,8 @@ func Tools(k *KB, caller func(map[string]any) string) []stdio.Tool { "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.", + "for, for about every thirty. A design's milestones and their issues are the issue tracker's milestone " + + "records (its milestones); a design with none is refused, naming the seed. 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}, diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/manifest_test.go b/modules/mesh-knowledge/cmd/mesh-knowledge/manifest_test.go index 8043c502..ce79ec6f 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/manifest_test.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/manifest_test.go @@ -80,11 +80,11 @@ func TestItDeclaresAndHoldsTheKnowledgeBaseSeatOnceForTheMesh(t *testing.T) { } 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", "seat:issue-tracker.list", "seat:issue-tracker.get", - "seat:mesh-delivery.deliveries", "seat:mesh-delivery.show", "seat:mesh-delivery.groups"}) { + "seat:issue-tracker.milestones", "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", "progress.go"} { + for _, f := range []string{"main.go", "hq.go", "progress.go", "milestones.go"} { src += readFile(t, f) } for _, inv := range m.Invokes { diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/milestones.go b/modules/mesh-knowledge/cmd/mesh-knowledge/milestones.go new file mode 100644 index 00000000..f34dfbc2 --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/milestones.go @@ -0,0 +1,53 @@ +package main + +// A design's milestones are records of the issue tracker (novox/hq ADR 0493 §1, issue 1781): progress asks +// `issue-tracker.milestones` once per answer, on the bus, for each milestone with its issues, instead of reading the +// design's table through the records module. + +import ( + "encoding/json" + "fmt" +) + +// MilestonesVerb is the tracker's verb that answers the milestones. +const MilestonesVerb = "seat:issue-tracker.milestones" + +// trackerMilestone is a row of `issue-tracker.milestones`. +type trackerMilestone struct { + Design int `json:"design"` + ID string `json:"id"` + Outcome string `json:"outcome"` + Status string `json:"status"` + Reason string `json:"reason"` + Issues []int `json:"issues"` +} + +// withMilestones is each design with its milestones as the tracker holds them, in their table's order; design 0 asks +// every design's. The designs are copies: the cache keeps what hq says, never what the tracker said. +func (k *KB) withMilestones(ds []*Design, design int) ([]*Design, error) { + body := map[string]any{} + if design != 0 { + body["design"] = design + } + raw, err := k.Seats(MilestonesVerb, body) + if err != nil { + return nil, fmt.Errorf("the issue tracker does not answer its milestones: %v", err) + } + var l struct { + Milestones []trackerMilestone `json:"milestones"` + } + if err := json.Unmarshal(raw, &l); err != nil { + return nil, fmt.Errorf("issue-tracker.milestones answered something else: %v", err) + } + by := map[int][]Milestone{} + for _, m := range l.Milestones { + by[m.Design] = append(by[m.Design], Milestone{ID: m.ID, Outcome: m.Outcome, Issues: m.Issues, Status: m.Status, Reason: m.Reason}) + } + out := make([]*Design, 0, len(ds)) + for _, d := range ds { + c := *d + c.Milestones = by[d.Number] + out = append(out, &c) + } + return out, nil +} diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/progress.go b/modules/mesh-knowledge/cmd/mesh-knowledge/progress.go index 5edd5aaf..ec7b655c 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/progress.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/progress.go @@ -5,8 +5,9 @@ package main // 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 +// It keeps no state of its own. It reads, each a request and reply over the bus: the designs' titles and states through +// the records module (design.go, cached by hq's commit), their milestones from the issue tracker (milestones.go, +// issue 1781), 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 ( @@ -287,6 +288,9 @@ func milestoneItems(d *Design, rows map[int]trackerRow, missing map[int]bool) ([ } } rollUp(&mi, kids) + if m.Status == "closed" && len(kids) == 0 { + mi.State, mi.Why = StateDone, "closed: "+m.Reason + } mi.Children = kids items = append(items, mi) } @@ -303,9 +307,15 @@ func (k *KB) progressDesign(p *Progress, number int, milestone string) error { if err != nil { return err } + if ds, err = k.withMilestones(ds, number); 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...) + if len(d.Milestones) == 0 { + return fmt.Errorf("design %d has no milestone records in the issue tracker: seed them from its table with "+ + "issue-tracker.import milestones %d", number, number) + } + p.Source = fmt.Sprintf("the issue tracker's milestone records of design %d, and %s at hq %s", number, d.Path, short(commit)) ms := d if milestone != "" { m := d.milestone(milestone) @@ -354,7 +364,10 @@ func (k *KB) progressMesh(p *Progress) error { if err != nil { return err } - p.Source = fmt.Sprintf("the designs in %s at hq %s", DesignsFolder, short(commit)) + if ds, err = k.withMilestones(ds, 0); err != nil { + return err + } + p.Source = fmt.Sprintf("the issue tracker's milestone records, and the designs in %s at hq %s", DesignsFolder, short(commit)) var with []*Design var without []string for _, d := range ds { diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/progress_bus_test.go b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_bus_test.go index 5a01c83d..4bf42cb7 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/progress_bus_test.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_bus_test.go @@ -12,12 +12,12 @@ import ( ) // 1947/AC1: the knowledge base running whole, as the runtime launches it, answers progress over the bus: it reads -// design 47 through the records module and asks the issue tracker and mesh-delivery on their seats' subjects, +// design 47 through the records module, its milestones from the issue tracker (issue 1781), and asks the issue tracker and mesh-delivery on their seats' subjects, // played here from what the live seats answered (testdata/progress). func TestProgressOverTheBus(t *testing.T) { w := newBusWorld(t) seats := newSeatsWorld(t) - for _, key := range []string{"seat:issue-tracker.list", "seat:issue-tracker.get", "seat:mesh-delivery.deliveries", + for _, key := range []string{"seat:issue-tracker.list", "seat:issue-tracker.get", MilestonesVerb, "seat:mesh-delivery.deliveries", "seat:mesh-delivery.groups", "seat:mesh-delivery.show"} { key := key if _, err := w.nc.Subscribe("ask."+key, func(m *nats.Msg) { @@ -45,7 +45,7 @@ func TestProgressOverTheBus(t *testing.T) { } lines := strings.Split(p.Tree, "\n") if len(raw) > DefaultBound || p.Detail != DetailMinimal || !strings.HasPrefix(lines[0], "✗ design 47 · Delivery: from a commit to delivered — ") || - !strings.Contains(p.Tree, "✗ R6 4/7 1945 fixing since") || !strings.Contains(strings.Join(p.Warnings, "\n"), "issue 1209 is in milestones R3b and R3c") { + !strings.Contains(p.Tree, "✗ R6 4/7 1945 fixing since") || !strings.Contains(p.Source, "the issue tracker's milestone records of design 47") { t.Fatalf("%d bytes:\n%s\n%v", len(raw), p.Tree, p.Warnings) } raw, err = b.call(Seat+".progress", map[string]any{"of": "milestone 47/R6", "detail": "detailed"}) @@ -63,7 +63,7 @@ func TestProgressOverTheBus(t *testing.T) { } seats.mu.Lock() defer seats.mu.Unlock() - if seats.asked["seat:issue-tracker.list"] != 2 || seats.asked["seat:mesh-delivery.groups"] != 1 { + if seats.asked["seat:issue-tracker.list"] != 2 || seats.asked["seat:mesh-delivery.groups"] != 1 || seats.asked[MilestonesVerb] != 2 { t.Fatalf("asked %v", seats.asked) } } diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go index b2895846..0b6cec47 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go @@ -45,8 +45,10 @@ type seatsWorld struct { mu sync.Mutex rows map[int]json.RawMessage linked map[string][]int - asked map[string]int - files map[string]json.RawMessage + // milestones is the tracker's milestone records (issue 1781). + milestones json.RawMessage + asked map[string]int + files map[string]json.RawMessage } func newSeatsWorld(t *testing.T) *seatsWorld { @@ -61,6 +63,7 @@ func newSeatsWorld(t *testing.T) *seatsWorld { _ = json.Unmarshal(r, &n) s.rows[n.Number] = r } + s.milestones = readFixture(t, "tracker-milestones-47.json") for _, f := range []string{"deliveries.json", "groups.json", "show-car-hunter-2.json"} { s.files[f] = readFixture(t, f) } @@ -86,9 +89,15 @@ func (s *seatsWorld) ask(key string, body any) (json.RawMessage, error) { Linked string `json:"linked"` Number int `json:"number"` ID string `json:"id"` + Design int `json:"design"` } _ = json.Unmarshal(raw, &a) switch key { + case MilestonesVerb: + if a.Design != 0 && a.Design != 47 { + return json.RawMessage(`{"milestones": []}`), nil + } + return s.milestones, nil case "seat:issue-tracker.list": numbers := a.Numbers if a.Linked != "" { @@ -131,31 +140,37 @@ func (c *countingRecords) Read(p string) (string, string, error) { 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) { +// 1947/AC2, as issue 1781 changed it: a design's title, state and decisions are hq's; its milestones are the issue +// tracker's records, ordered as its table, an id found with spaces or hyphens, and a closed one holding no issue done. +func TestADesignsMilestonesAreTheTrackersRecords(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 d.Number != 47 || d.Title != "Delivery: from a commit to delivered" || d.Status != "in-progress" || !contains(d.Decisions, "ADR 0276") || + len(d.Milestones) != 0 { + t.Fatalf("design %d %q %q %v, %d milestones read from hq", d.Number, d.Title, d.Status, d.Decisions, len(d.Milestones)) } - if len(d.Milestones) != 29 { - t.Fatalf("%d milestones", len(d.Milestones)) + seats := newSeatsWorld(t) + k := &KB{Seats: seats.ask} + ds, err := k.withMilestones([]*Design{d}, 47) + if err != nil { + t.Fatal(err) + } + if len(ds[0].Milestones) != 29 || len(d.Milestones) != 0 || seats.asked[MilestonesVerb] != 1 { + t.Fatalf("%d milestones; the cached design holds %d", len(ds[0].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) + m := ds[0].milestone(c.id) if m == nil { t.Fatalf("no milestone %s", c.id) } @@ -163,25 +178,10 @@ func TestTheDesignReaderReadsTheMilestoneTable(t *testing.T) { 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) + closed := &Design{Milestones: []Milestone{{ID: "6", Status: "closed", Reason: "folded into release 4"}, {ID: "R4d", Status: "open"}}} + items, _ := milestoneItems(closed, nil, nil) + if items[0].State != StateDone || items[1].State != StateWaiting { + t.Fatalf("%+v", items) } } @@ -207,8 +207,8 @@ func TestDesignsAreReadAgainOnlyWhenHqMoves(t *testing.T) { 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") { + if !strings.Contains(p.Tree, "3 designs in progress, 1 with milestones") || !strings.Contains(p.Tree, "design 47 · Delivery: from a commit to delivered") || + !strings.Contains(p.Tree, "2 designs in progress name no milestones (issue 845): 53, 57") { t.Fatalf("mesh:\n%s", p.Tree) } for range 2 { @@ -303,7 +303,7 @@ func TestProgressOfADesignMinimalAndDetailed(t *testing.T) { t.Fatal(err) } raw, _ := json.Marshal(p) - if len(raw) > DefaultBound || p.Detail != DetailMinimal || !strings.Contains(p.Source, "issue 1781") { + if len(raw) > DefaultBound || p.Detail != DetailMinimal || !strings.Contains(p.Source, "the issue tracker's milestone records of design 47") { 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 { @@ -316,7 +316,7 @@ func TestProgressOfADesignMinimalAndDetailed(t *testing.T) { 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 4/7 1945 fixing since", "✗ 3 fix 0/1 1049 fixing, worker fix-chain-cat@shanks stale since"} { + for _, want := range []string{"✗ R6 4/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) } @@ -346,7 +346,7 @@ func TestProgressOfADesignMinimalAndDetailed(t *testing.T) { 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"}} { + for _, bad := range []ProgressAsk{{Of: "design"}, {Of: "milestone 47/R99"}, {Of: "design 47", Detail: "full"}, {Of: "design 57"}} { if _, err := k.Progress(bad); err == nil { t.Errorf("%+v was answered", bad) } diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/README.md b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/README.md index 9ca111c3..0978ba3c 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/README.md +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/README.md @@ -5,3 +5,6 @@ Captured for the progress verb's tests (novox/hq issue 1947), never written by h 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. +- `tracker-milestones-47.json`: the answer of `issue-tracker.milestones` for design 47 (issue 1781), made by running + the tracker's own table parse (mesh-issues `parseTable`, its seed) over `hq/`'s design 47, each issue two rows name + left in neither, as the seed refuses it; the tracker held no milestone records yet to capture from. diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/tracker-milestones-47.json b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/tracker-milestones-47.json new file mode 100644 index 00000000..e38002e3 --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/tracker-milestones-47.json @@ -0,0 +1,418 @@ +{ + "milestones": [ + { + "design": 47, + "id": "1", + "issues": [ + 973, + 525 + ], + "order": 1, + "outcome": "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", + "ref": "47/1", + "status": "open" + }, + { + "design": 47, + "id": "2", + "issues": [ + 974, + 975, + 976 + ], + "order": 2, + "outcome": "A ready mark is acted on within seconds, and each pull request has at most one waiting check", + "ref": "47/2", + "status": "open" + }, + { + "design": 47, + "id": "2b", + "issues": [ + 1025, + 1019 + ], + "order": 3, + "outcome": "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", + "ref": "47/2b", + "status": "open" + }, + { + "design": 47, + "id": "3", + "issues": [ + 977, + 978, + 1022, + 895, + 1046, + 605, + 1021 + ], + "order": 4, + "outcome": "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", + "ref": "47/3", + "status": "open" + }, + { + "design": 47, + "id": "4", + "issues": [ + 980, + 1065, + 1066, + 1067, + 1068 + ], + "order": 5, + "outcome": "A delivery backs up the data it moves first, and a failed backup holds it and is said", + "ref": "47/4", + "status": "open" + }, + { + "design": 47, + "id": "3-fix", + "issues": [ + 1082, + 1049, + 1047, + 1083, + 1079, + 1084 + ], + "order": 6, + "outcome": "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", + "ref": "47/3-fix", + "status": "open" + }, + { + "design": 47, + "id": "5", + "issues": [ + 981 + ], + "order": 7, + "outcome": "One read of a module on a node names the commit, build and rollout that put it there", + "ref": "47/5", + "status": "open" + }, + { + "design": 47, + "id": "5b", + "issues": [ + 1117, + 1124, + 1128 + ], + "order": 8, + "outcome": "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)", + "ref": "47/5b", + "status": "open" + }, + { + "design": 47, + "id": "6", + "issues": [], + "order": 9, + "outcome": "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", + "ref": "47/6", + "status": "open" + }, + { + "design": 47, + "id": "R1a", + "issues": [ + 1131 + ], + "order": 10, + "outcome": "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", + "ref": "47/R1a", + "status": "open" + }, + { + "design": 47, + "id": "R1b", + "issues": [ + 1132, + 1142, + 1136, + 1145, + 1146, + 1147, + 1160, + 1162 + ], + "order": 11, + "outcome": "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", + "ref": "47/R1b", + "status": "open" + }, + { + "design": 47, + "id": "R1c", + "issues": [ + 1134, + 1137 + ], + "order": 12, + "outcome": "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", + "ref": "47/R1c", + "status": "open" + }, + { + "design": 47, + "id": "R2a", + "issues": [ + 1153 + ], + "order": 13, + "outcome": "The watchman and the controller can read one layout of the stuck keys: mesh-sdk ships it before its users", + "ref": "47/R2a", + "status": "open" + }, + { + "design": 47, + "id": "R2b", + "issues": [ + 1135, + 1154, + 1155, + 1156, + 1157, + 1159 + ], + "order": 14, + "outcome": "A part, node or check that stops moving is raised as a condition within two minutes, with no timer in a part", + "ref": "47/R2b", + "status": "open" + }, + { + "design": 47, + "id": "R2c", + "issues": [ + 1183 + ], + "order": 15, + "outcome": "The controller says when it last heard each machine from presence alone, and no longer hears the alive message", + "ref": "47/R2c", + "status": "open" + }, + { + "design": 47, + "id": "R2d", + "issues": [ + 1158 + ], + "order": 16, + "outcome": "The node-engine sends no alive message", + "ref": "47/R2d", + "status": "open" + }, + { + "design": 47, + "id": "R3a", + "issues": [ + 1200, + 1201, + 1202, + 1203, + 1204, + 1208 + ], + "order": 17, + "outcome": "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)", + "ref": "47/R3a", + "status": "open" + }, + { + "design": 47, + "id": "R3b", + "issues": [ + 1210, + 1211, + 1212, + 1213, + 1214, + 1215, + 1216, + 1217, + 1218 + ], + "order": 18, + "outcome": "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)", + "ref": "47/R3b", + "status": "open" + }, + { + "design": 47, + "id": "R3c", + "issues": [ + 1235, + 1236, + 1237, + 1238, + 1239, + 1240, + 1241, + 1234, + 1223, + 1233, + 1242 + ], + "order": 19, + "outcome": "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)", + "ref": "47/R3c", + "status": "open" + }, + { + "design": 47, + "id": "R3d", + "issues": [ + 1321, + 1322, + 1323, + 1324, + 1325, + 1326, + 1317, + 1314, + 1315, + 1316 + ], + "order": 20, + "outcome": "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)", + "ref": "47/R3d", + "status": "open" + }, + { + "design": 47, + "id": "R3e", + "issues": [], + "order": 21, + "outcome": "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)", + "ref": "47/R3e", + "status": "open" + }, + { + "design": 47, + "id": "R4a", + "issues": [ + 1353, + 1354, + 1355, + 1356, + 1357, + 1358, + 1359, + 1360, + 1361, + 1362, + 1363, + 1364, + 1298, + 1308 + ], + "order": 22, + "outcome": "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)", + "ref": "47/R4a", + "status": "open" + }, + { + "design": 47, + "id": "R4b", + "issues": [ + 1375, + 1337 + ], + "order": 23, + "outcome": "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)", + "ref": "47/R4b", + "status": "open" + }, + { + "design": 47, + "id": "R4c", + "issues": [ + 1309, + 1287, + 1288, + 1289, + 1290, + 1291, + 1292, + 1251, + 1293 + ], + "order": 24, + "outcome": "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)", + "ref": "47/R4c", + "status": "open" + }, + { + "design": 47, + "id": "R4d", + "issues": [], + "order": 25, + "outcome": "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)", + "ref": "47/R4d", + "status": "open" + }, + { + "design": 47, + "id": "R4e", + "issues": [ + 1304 + ], + "order": 26, + "outcome": "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)", + "ref": "47/R4e", + "status": "open" + }, + { + "design": 47, + "id": "R4f", + "issues": [ + 709, + 1300, + 1301, + 1302 + ], + "order": 27, + "outcome": "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)", + "ref": "47/R4f", + "status": "open" + }, + { + "design": 47, + "id": "R4g", + "issues": [ + 1887, + 1295, + 1296, + 1303 + ], + "order": 28, + "outcome": "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)", + "ref": "47/R4g", + "status": "open" + }, + { + "design": 47, + "id": "R6", + "issues": [ + 1942, + 1945, + 1943, + 1944, + 1946, + 1906, + 1130 + ], + "order": 29, + "outcome": "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)", + "ref": "47/R6", + "status": "open" + } + ], + "said": "generated" +} \ No newline at end of file diff --git a/modules/mesh-knowledge/module.json b/modules/mesh-knowledge/module.json index 5d792f79..6b07a2ff 100644 --- a/modules/mesh-knowledge/module.json +++ b/modules/mesh-knowledge/module.json @@ -87,6 +87,7 @@ "seat:operator-channel.notify", "seat:issue-tracker.list", "seat:issue-tracker.get", + "seat:issue-tracker.milestones", "seat:mesh-delivery.deliveries", "seat:mesh-delivery.show", "seat:mesh-delivery.groups"