diff --git a/modules/mesh-knowledge/README.md b/modules/mesh-knowledge/README.md index c684e722..fd6cda73 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. 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`) | +| `progress` | `of` (`mesh`, `design `, `milestone /`, `issue `, `decision ` or `ADR `, `research `), `detail` (`minimal` or `detailed`), `bound`, `since` (a window such as `60m`, at most `24h`) | 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`). With `since`, only what changed in the window under `of`, headed "N changes in the last 60 min": milestones moved (from what to what, from `issue-tracker.milestones` `one`), issues opened, moved, noted, linked or closed (from `issue-tracker.get`), pull requests opened, checked, merged, held or closed (from `mesh-delivery.show`'s transitions and check). It keeps no state; two things neither seat keeps are said in its warnings: an issue's earlier status (the tracker's log `changes` holds it, no verb reads it) and when a merge reached its machines | `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 8ab4e918..3c9d423b 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/design.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/design.go @@ -34,6 +34,8 @@ type Milestone struct { // Status is open or closed, with its Reason; a closed milestone holding no issue is done. Status string Reason string + // Moved is who last moved it and when, as the tracker says it; nil before its first move. + Moved *stamp } var ( diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/main.go b/modules/mesh-knowledge/cmd/mesh-knowledge/main.go index 9c009b8b..9c6ab06a 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/main.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/main.go @@ -1130,14 +1130,19 @@ func Tools(k *KB, caller func(map[string]any) string) []stdio.Tool { "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. 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.", + "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. " + + "With since, a window such as 60m or 5m (at most 24h), it says only the progress made since then, such as in the last hour: " + + "the milestones and issues that moved (from what to what where the tracker keeps it), were opened, noted or resolved, " + + "the pull requests opened, checked, merged or held and the deliveries that reached their machines, and what became stuck, " + + "headed by how many changes in the last N minutes.", Input: schema([]string{"of"}, map[string]any{ "of": text("what: mesh, design 47, milestone 47/R6, issue 1945, decision 493 (or ADR 0493) or research 72"), "detail": map[string]any{"type": "string", "enum": []string{DetailMinimal, DetailDetailed}, "description": "minimal (the default): one line per child; detailed: the tree with issues, pull requests and groups"}, - "bound": boundArg()}), + "bound": boundArg(), + "since": text("a window such as 60m, 5m or 2h, at most 24h: only what changed in it, from the tracker's and mesh-delivery's history")}), Run: func(m map[string]any) (any, error) { - a, err := readArgs("progress", m, "of", "detail", "bound") + a, err := readArgs("progress", m, "of", "detail", "bound", "since") if err != nil { return nil, err } @@ -1151,6 +1156,9 @@ func Tools(k *KB, caller func(map[string]any) string) []stdio.Tool { if p.Bound, err = a.num("bound", false); err != nil { return nil, err } + if p.Since, err = a.str("since"); err != nil { + return nil, err + } return k.Progress(p) }}, } diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/milestones.go b/modules/mesh-knowledge/cmd/mesh-knowledge/milestones.go index f34dfbc2..49e7d731 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/milestones.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/milestones.go @@ -7,6 +7,7 @@ package main import ( "encoding/json" "fmt" + "time" ) // MilestonesVerb is the tracker's verb that answers the milestones. @@ -20,6 +21,13 @@ type trackerMilestone struct { Status string `json:"status"` Reason string `json:"reason"` Issues []int `json:"issues"` + Moved *stamp `json:"moved"` +} + +// stamp is who did something and when, as the tracker says it. +type stamp struct { + By string `json:"by"` + At time.Time `json:"at"` } // withMilestones is each design with its milestones as the tracker holds them, in their table's order; design 0 asks @@ -41,7 +49,7 @@ func (k *KB) withMilestones(ds []*Design, design int) ([]*Design, error) { } 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}) + by[m.Design] = append(by[m.Design], Milestone{ID: m.ID, Outcome: m.Outcome, Issues: m.Issues, Status: m.Status, Reason: m.Reason, Moved: m.Moved}) } out := make([]*Design, 0, len(ds)) for _, d := range ds { diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/progress.go b/modules/mesh-knowledge/cmd/mesh-knowledge/progress.go index f8882f1d..ff2ff2c9 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/progress.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/progress.go @@ -56,6 +56,8 @@ type ProgressAsk struct { Of string Detail string Bound int + // Since is a window, such as 60m: the answer is then only what changed in it (progress_since.go). + Since string } // Item is one thing whose progress is said: a design, a milestone, an issue, a pull request. @@ -81,6 +83,11 @@ type Progress struct { Source string `json:"source"` ReadAt time.Time `json:"read-at"` Cut string `json:"cut,omitempty"` + // Since and Changes are said for an answer about a window: how long it is and how many changes it holds. + Since string `json:"since,omitempty"` + Changes int `json:"changes,omitempty"` + // scope is what the answer is about, kept for a window's answer; nil without since. + scope *sinceScope } // trackerRow is a row of `issue-tracker.list`. @@ -137,9 +144,15 @@ func (k *KB) Progress(a ProgressAsk) (*Progress, error) { if k.Seats == nil { return nil, errors.New("the knowledge base reaches no other seat here") } + window, err := parseSince(a.Since) + if err != nil { + return nil, err + } of := strings.Join(strings.Fields(a.Of), " ") p := &Progress{Of: of, Detail: detail, ReadAt: k.now()} - var err error + if window > 0 { + p.scope = &sinceScope{rows: map[int]trackerRow{}} + } switch { case strings.EqualFold(of, "mesh"): err = k.progressMesh(p) @@ -168,6 +181,9 @@ func (k *KB) Progress(a ProgressAsk) (*Progress, error) { if err != nil { return nil, err } + if window > 0 { + k.progressSince(p, window) + } // A cut takes the warnings from the last: the calls that failed are said before the answers that did not parse. callsFirst(p.Warnings) fitProgress(p, boundOf(a.Bound)) @@ -340,8 +356,9 @@ func (k *KB) progressDesign(p *Progress, number int, milestone string) error { } items, warn := milestoneItems(ms, rows, missing) p.Warnings = append(p.Warnings, warn...) + p.scope.hold(rows, ms) var extra *extras - if p.Detail == DetailDetailed { + if p.Detail == DetailDetailed && p.scope == nil { if extra, err = k.readExtras(items); err != nil { return err } @@ -394,6 +411,10 @@ func (k *KB) progressMesh(p *Progress) error { for _, n := range missingList { missing[n] = true } + p.scope.hold(rows, with...) + if p.scope != nil { + p.scope.every = true + } top := Item{ID: "mesh", Title: "the designs being built"} var kids []Item for _, d := range with { @@ -441,6 +462,11 @@ func (k *KB) progressIssue(p *Progress, n int) error { } it.ID = fmt.Sprintf("issue %d", n) p.Source = "the issue tracker and mesh-delivery" + if p.scope != nil { + p.scope.hold(rows) + p.State, p.Items = it.State, []Item{it} + return nil + } extra, err := k.readExtras([]Item{{Children: []Item{{ID: strconv.Itoa(n), State: StateMoving}}}}) if err != nil { return err @@ -513,6 +539,9 @@ func (k *KB) progressRecord(p *Progress, kind string, n int) error { p.Warnings = append(p.Warnings, fmt.Sprintf("%d issues are linked to %s; the first %d are counted", l.Matched, name, len(l.Rows))) } for _, r := range l.Rows { + if p.scope != nil { + p.scope.rows[r.Number] = r + } if it, counts := issueItem(r); counts { kids = append(kids, it) } 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 4bf42cb7..3d399143 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/progress_bus_test.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_bus_test.go @@ -58,12 +58,24 @@ func TestProgressOverTheBus(t *testing.T) { t.Errorf("detailed: no %q in\n%s", want, p.Tree) } } + // A window over the bus: since is taken and the answer says how many changes it holds (none moved in the captured rows). + raw, err = b.call(Seat+".progress", map[string]any{"of": "milestone 47/R6", "since": "60m"}) + if err != nil { + t.Fatal(err) + } + _ = json.Unmarshal(raw, &p) + if !strings.HasPrefix(p.Tree, "0 changes in the last 60 min under milestone 47/R6 (since ") || p.Since != "1h0m0s" { + t.Errorf("since over the bus:\n%s", p.Tree) + } + if _, err := b.call(Seat+".progress", map[string]any{"of": "design 47", "since": "2d"}); err == nil { + t.Error("a window of two days was taken") + } if _, err := b.call(Seat+".progress", map[string]any{"of": "design 47", "by": "someone@else"}); err == nil { t.Error("an argument naming the caller was taken") } seats.mu.Lock() defer seats.mu.Unlock() - if seats.asked["seat:issue-tracker.list"] != 2 || seats.asked["seat:mesh-delivery.groups"] != 1 || seats.asked[MilestonesVerb] != 2 { + if seats.asked["seat:issue-tracker.list"] != 3 || seats.asked["seat:mesh-delivery.groups"] != 1 || seats.asked[MilestonesVerb] != 3 { t.Fatalf("asked %v", seats.asked) } } diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/progress_since.go b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_since.go new file mode 100644 index 00000000..110bc77b --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_since.go @@ -0,0 +1,440 @@ +package main + +// progress with since: only what changed in a window under of (the operator, 2026-10-28: "can the progress tool also +// report the progress made since the last call, or maybe better, the last 60 minutes"). A window, not a caller's last +// call: the knowledge base keeps no state of its own, so it reads what the other seats keep, each a request and reply +// over the bus: +// +// - the issue tracker's milestones with their last move, and a moved one's history (`issue-tracker.milestones` +// with one): what it was before, what it is now, who moved it and when; +// - each issue that moved in the window whole (`issue-tracker.get`): when it was opened, its notes and links with who +// and when, its worker since when, and when it was closed; +// - mesh-delivery's deliveries and each one's transitions and its check (`mesh-delivery.deliveries`, `.show`). +// +// Two things the window needs are kept by neither seat, and each answer that meets one says so: the tracker keeps an +// issue's moves only in its log, which no verb reads, so a move is said by the status it reached (with its worker's +// since or its closed time), not from what it moved; and mesh-delivery keeps no time a merge reached its machines, so +// "on the machines" is as it reads now. + +import ( + "encoding/json" + "fmt" + "sort" + "strconv" + "strings" + "time" +) + +// MostSince is the longest window since asks: the deliveries mesh-delivery lists end a day back. +const MostSince = 24 * time.Hour + +// parseSince reads a window such as 60m, 5m or 2h; empty is none. +func parseSince(s string) (time.Duration, error) { + s = strings.TrimSpace(s) + if s == "" { + return 0, nil + } + d, err := time.ParseDuration(s) + if err != nil || d <= 0 || d > MostSince { + return 0, fmt.Errorf("since is a window such as 60m, 5m or 2h, more than nothing and at most %s, not %q", "24h", s) + } + return d, nil +} + +// sinceScope is what an answer about a window is about: the issues under of, as the tracker answered their rows, and +// the milestones; every says that of is the mesh, so every delivery counts. +type sinceScope struct { + rows map[int]trackerRow + milestones []sinceMilestone + every bool +} + +type sinceMilestone struct { + design int + m Milestone +} + +// hold keeps the rows and the designs' milestones for a window's answer; on a nil scope it does nothing. +func (s *sinceScope) hold(rows map[int]trackerRow, ds ...*Design) { + if s == nil { + return + } + for n, r := range rows { + s.rows[n] = r + } + for _, d := range ds { + for _, m := range d.Milestones { + s.milestones = append(s.milestones, sinceMilestone{design: d.Number, m: m}) + } + } +} + +// change is one thing that changed in the window, with its changes, newest last. +type change struct { + line string + last time.Time + events []event +} + +type event struct { + at time.Time + line string + // brief is the event in the change's one line, when it differs from line. + brief string +} + +// window is the time between from and to. +type window struct{ from, to time.Time } + +func (w window) holds(t time.Time) bool { return !t.IsZero() && !t.Before(w.from) && !t.After(w.to) } + +// progressSince replaces the answer's tree with what changed in the window under p.scope. +func (k *KB) progressSince(p *Progress, d time.Duration) { + w := window{from: p.ReadAt.Add(-d), to: p.ReadAt} + p.Since, p.Items = d.String(), nil + var milestones, issues, pulls []change + milestones = k.milestoneChanges(p, w) + issues = k.issueChanges(p, w) + pulls = k.pullChanges(p, w) + for _, cs := range [][]change{milestones, issues, pulls} { + sort.SliceStable(cs, func(i, j int) bool { return cs[i].last.After(cs[j].last) }) + } + all := append(append(milestones, issues...), pulls...) + for _, c := range all { + p.Changes += len(c.events) + } + var b strings.Builder + noun := "changes" + if p.Changes == 1 { + noun = "change" + } + fmt.Fprintf(&b, "%d %s in the last %d min under %s (since %s)\n", p.Changes, noun, int(d.Minutes()), p.Of, when(&w.from)) + for i, c := range all { + branch, under := "├── ", "│ " + if i == len(all)-1 { + branch, under = "└── ", " " + } + b.WriteString(branch + c.line + "\n") + if p.Detail != DetailDetailed { + continue + } + for j, e := range c.events { + br := "├── " + if j == len(c.events)-1 { + br = "└── " + } + b.WriteString(under + br + e.line + "\n") + } + } + p.Tree = strings.TrimRight(b.String(), "\n") + p.Source += "; the window from the tracker's milestones and their history, its issues' notes, links, workers and " + + "closes, and mesh-delivery's transitions and checks" +} + +// milestoneChanges is each milestone moved in the window, from its history: what it was and what it is. +func (k *KB) milestoneChanges(p *Progress, w window) []change { + var out []change + asked := 0 + for _, sm := range p.scope.milestones { + m := sm.m + if m.Moved == nil || !w.holds(m.Moved.At) { + continue + } + ref := fmt.Sprintf("%d/%s", sm.design, m.ID) + now := m.Status + if m.Reason != "" { + now += ": " + m.Reason + } + was := "a state its history does not say" + if asked < MostGets { + asked++ + if prev, err := k.milestoneBefore(ref); err != nil { + p.Warnings = append(p.Warnings, fmt.Sprintf("issue-tracker.milestones one %s: %v; said without what it was", ref, err)) + } else if prev != "" { + was = prev + } + } + mark := marks[StateMoving] + if m.Status == "closed" { + mark = marks[StateDone] + } + line := fmt.Sprintf("%s milestone %s %s → %s · by %s %s", mark, ref, was, now, m.Moved.By, when(&m.Moved.At)) + out = append(out, change{line: line, last: m.Moved.At, events: []event{{at: m.Moved.At, line: fmt.Sprintf("moved %s by %s", when(&m.Moved.At), m.Moved.By)}}}) + } + return out +} + +// milestoneBefore is a milestone's status before its last move, from the tracker's history of it. +func (k *KB) milestoneBefore(ref string) (string, error) { + raw, err := k.Seats(MilestonesVerb, map[string]any{"one": ref}) + if err != nil { + return "", err + } + var r struct { + History []struct { + Status string `json:"status"` + } `json:"history"` + } + if err := json.Unmarshal(raw, &r); err != nil { + return "", fmt.Errorf("answered something else: %v", err) + } + if len(r.History) < 2 { + return "", nil + } + return r.History[len(r.History)-2].Status, nil +} + +// issueGotWhole is the part of `issue-tracker.get` a window reads. +type issueGotWhole struct { + Status string `json:"status"` + Opened time.Time `json:"opened"` + OpenedBy string `json:"opened-by"` + Closed *time.Time `json:"closed"` + FixedBy []string `json:"fixed-by"` + Worker *struct { + By string `json:"by"` + Since time.Time `json:"since"` + } `json:"worker"` + Notes []struct { + At time.Time `json:"at"` + By string `json:"by"` + Text string `json:"text"` + } `json:"notes"` + Links []struct { + At time.Time `json:"at"` + By string `json:"by"` + Kind string `json:"kind"` + Target string `json:"target"` + } `json:"links"` +} + +// issueChanges is each issue under of that moved in the window, read whole: opened, moved, noted, linked, closed. +func (k *KB) issueChanges(p *Progress, w window) []change { + var moved []trackerRow + for _, r := range p.scope.rows { + if r.Moved != nil && w.holds(*r.Moved) { + moved = append(moved, r) + } + } + sort.Slice(moved, func(i, j int) bool { return moved[i].Moved.After(*moved[j].Moved) }) + if len(moved) > 0 { + p.Warnings = append(p.Warnings, "the issue tracker keeps no history of an issue's moves that a verb reads (only its "+ + "log changes holds them): a move is said by the status it reached, its worker's since and its closed time, not "+ + "from what it moved") + } + var out []change + for i, r := range moved { + it, _ := issueItem(r) + if it.State == "" { + it.State = StateDone + } + head := fmt.Sprintf("%s %d %s — ", marks[it.State], r.Number, clip(r.Title, 80)) + fallback := func(why string) { + p.Warnings = append(p.Warnings, why) + line := fmt.Sprintf("moved %s, now %s", when(r.Moved), r.Status) + if r.Worker != "" { + line += " (" + r.Worker + ")" + } + out = append(out, change{line: head + line, last: *r.Moved, events: []event{{at: *r.Moved, line: line}}}) + } + if i >= MostGets { + fallback(fmt.Sprintf("more than %d issues moved; the rest are said by their rows' moved time", MostGets)) + continue + } + raw, err := k.Seats("seat:issue-tracker.get", map[string]any{"number": r.Number}) + if err != nil { + fallback(fmt.Sprintf("issue-tracker.get %d: %v; said by its row's moved time", r.Number, err)) + continue + } + var g issueGotWhole + if err := json.Unmarshal(raw, &g); err != nil { + fallback(fmt.Sprintf("issue-tracker.get %d answered something else: %v; said by its row's moved time", r.Number, err)) + continue + } + var evs []event + var says []string + if w.holds(g.Opened) { + evs = append(evs, event{at: g.Opened, line: fmt.Sprintf("opened %s by %s", when(&g.Opened), g.OpenedBy)}) + says = append(says, "opened by "+g.OpenedBy+" "+when(&g.Opened)) + } + if g.Worker != nil && w.holds(g.Worker.Since) && (g.Status == "diagnosing" || g.Status == "fixing") { + s := fmt.Sprintf("→ %s by %s %s", g.Status, g.Worker.By, when(&g.Worker.Since)) + evs = append(evs, event{at: g.Worker.Since, line: s}) + says = append(says, s) + } + if g.Closed != nil && w.holds(*g.Closed) { + s := fmt.Sprintf("→ %s %s", g.Status, when(g.Closed)) + if len(g.FixedBy) > 0 { + s += ", fixed by " + strings.Join(g.FixedBy, ", ") + } + evs = append(evs, event{at: *g.Closed, line: s}) + says = append(says, s) + } + notes := 0 + var last string + for _, n := range g.Notes { + if w.holds(n.At) { + notes++ + last = fmt.Sprintf("the last %s by %s", when(&n.At), n.By) + evs = append(evs, event{at: n.At, line: fmt.Sprintf("noted %s by %s: %s", when(&n.At), n.By, clip(n.Text, 80))}) + } + } + if notes == 1 { + says = append(says, "1 note, "+last) + } else if notes > 1 { + says = append(says, fmt.Sprintf("%d notes, %s", notes, last)) + } + links := 0 + for _, l := range g.Links { + if w.holds(l.At) { + links++ + evs = append(evs, event{at: l.At, line: fmt.Sprintf("linked %s %s by %s %s", l.Kind, l.Target, l.By, when(&l.At))}) + } + } + if links > 0 { + says = append(says, fmt.Sprintf("%d links", links)) + } + if len(evs) == 0 { + // It moved by what the tracker records of nobody: its fix's delivery said a new state. + s := fmt.Sprintf("moved %s, now %s", when(r.Moved), it.Why) + evs, says = []event{{at: *r.Moved, line: s}}, []string{s} + } + if it.State == StateStuck { + says = append(says, "stuck: "+it.Why) + } + sort.SliceStable(evs, func(i, j int) bool { return evs[i].at.Before(evs[j].at) }) + out = append(out, change{line: head + strings.Join(says, "; "), last: *r.Moved, events: evs}) + } + return out +} + +// deliveryWhole is the part of `mesh-delivery.show` a window reads. +type deliveryWhole struct { + Delivery struct { + Repository string `json:"repository"` + Number int `json:"number"` + Issues []int `json:"issues"` + State string `json:"state"` + Check *struct { + At time.Time `json:"at"` + Gate string `json:"gate"` + Summary string `json:"summary"` + } `json:"check"` + Transitions []struct { + At time.Time `json:"at"` + Event string `json:"event"` + To string `json:"to"` + Why string `json:"why"` + } `json:"transitions"` + } `json:"delivery"` + Machines string `json:"machines"` + Waits string `json:"waits"` +} + +// pullChanges is each pull request naming an issue under of (every one, for the mesh) that was opened, checked, +// merged, held or closed in the window. +func (k *KB) pullChanges(p *Progress, w window) []change { + raw, err := k.Seats("seat:mesh-delivery.deliveries", map[string]any{}) + if err != nil { + p.Warnings = append(p.Warnings, fmt.Sprintf("mesh-delivery does not answer, so no pull request is said: %v", err)) + return nil + } + var dl struct { + Deliveries []deliveryRow `json:"deliveries"` + } + if err := json.Unmarshal(raw, &dl); err != nil { + p.Warnings = append(p.Warnings, fmt.Sprintf("mesh-delivery.deliveries answered something else, so no pull request is said: %v", err)) + return nil + } + var out []change + shown, reached := 0, false + for _, d := range dl.Deliveries { + // A delivery's since is its last transition; one not final may still have been checked since. + if final(d.State) && !w.holds(d.Since) { + continue + } + if shown == MostShows { + p.Warnings = append(p.Warnings, fmt.Sprintf("more than %d deliveries moved; the rest are not read", MostShows)) + break + } + shown++ + raw, err := k.Seats("seat:mesh-delivery.show", map[string]any{"id": d.ID}) + if err != nil { + p.Warnings = append(p.Warnings, fmt.Sprintf("mesh-delivery.show %s: %v", d.ID, err)) + continue + } + var s deliveryWhole + if err := json.Unmarshal(raw, &s); err != nil { + p.Warnings = append(p.Warnings, fmt.Sprintf("mesh-delivery.show %s answered something else: %v", d.ID, err)) + continue + } + under := p.scope.every + var names []string + for _, n := range s.Delivery.Issues { + names = append(names, strconv.Itoa(n)) + if _, ok := p.scope.rows[n]; ok { + under = true + } + } + if !under { + continue + } + var evs []event + for _, t := range s.Delivery.Transitions { + if !w.holds(t.At) { + continue + } + switch { + case t.Event == "announced": + evs = append(evs, event{at: t.At, line: "opened " + when(&t.At)}) + case t.To == "merged" || t.To == "closed" || strings.HasPrefix(t.To, "held") || t.To == "delivered": + evs = append(evs, event{t.At, fmt.Sprintf("%s %s: %s", t.To, when(&t.At), clip(t.Why, 80)), t.To + " " + when(&t.At)}) + } + } + if c := s.Delivery.Check; c != nil && w.holds(c.At) { + evs = append(evs, event{c.At, fmt.Sprintf("check %s %s: %s", c.Gate, when(&c.At), clip(c.Summary, 80)), fmt.Sprintf("check %s %s", c.Gate, when(&c.At))}) + } + if len(evs) == 0 { + continue + } + sort.SliceStable(evs, func(i, j int) bool { return evs[i].at.Before(evs[j].at) }) + var says []string + for _, e := range evs { + if e.brief != "" { + says = append(says, e.brief) + } else { + says = append(says, e.line) + } + } + state := s.Delivery.State + mark := marks[StateMoving] + switch { + case state == "merged" || state == "delivered": + mark = marks[StateDone] + case strings.HasPrefix(state, "held"): + mark = marks[StateStuck] + case state == "closed": + mark = marks[StateWaiting] + } + repo := s.Delivery.Repository + if i := strings.LastIndex(repo, "/"); i >= 0 { + repo = repo[i+1:] + } + line := fmt.Sprintf("%s %s#%d (%s) %s", mark, repo, s.Delivery.Number, strings.Join(names, ", "), strings.Join(says, " · ")) + switch { + case state == "merged" && s.Machines != "": + line += ", " + s.Machines + reached = true + case !final(state): + line += ", now " + state + if s.Waits != "" { + line += ", waits for " + s.Waits + } + } + out = append(out, change{line: line, last: evs[len(evs)-1].at, events: evs}) + } + if reached { + p.Warnings = append(p.Warnings, "mesh-delivery keeps no time a merge reached its machines: \"on the machines\" is as it reads now") + } + return out +} diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go index 7c26e289..a32b6666 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "fmt" "os" "path/filepath" "reflect" @@ -393,3 +394,134 @@ func TestProgressOfAnIssueAndARecord(t *testing.T) { t.Error("an issue the tracker does not hold was answered") } } + +// sinceWorld plays the tracker and the delivery seat from what they answered on 2026-10-28 (testdata/progress/since): +// a get or a show it holds no answer for is refused, as a seat that cannot answer. +func sinceWorld(t *testing.T) func(string, any) (json.RawMessage, error) { + t.Helper() + read := func(name string) json.RawMessage { return readFixture(t, filepath.Join("since", name)) } + var rows []json.RawMessage + if err := json.Unmarshal(read("rows.json"), &rows); err != nil { + t.Fatal(err) + } + byNumber := map[int]json.RawMessage{} + for _, r := range rows { + var n struct{ Number int } + _ = json.Unmarshal(r, &n) + byNumber[n.Number] = r + } + shows := map[string]string{"novox/mesh-catalog@470687ea4cce": "show-mesh-catalog-539.json", "novox/mesh-catalog@f5d4b2f86ed4": "show-mesh-catalog-537.json", + "novox/mesh-delivery@2b801f5bd49a": "show-mesh-delivery-94.json", "novox/de-spiegel@fba6dbe3259e": "show-de-spiegel-6.json"} + return func(key string, body any) (json.RawMessage, error) { + raw, _ := json.Marshal(body) + var a struct { + Numbers []int `json:"numbers"` + Number int `json:"number"` + ID string `json:"id"` + One string `json:"one"` + } + _ = json.Unmarshal(raw, &a) + switch key { + case MilestonesVerb: + return read("milestone-47-6.json"), nil + case "seat:issue-tracker.list": + var out []json.RawMessage + for _, n := range a.Numbers { + if r, ok := byNumber[n]; ok { + out = append(out, r) + } + } + return json.Marshal(map[string]any{"rows": out, "matched": len(out)}) + case "seat:issue-tracker.get": + if a.Number == 1422 || a.Number == 1956 { + return read(fmt.Sprintf("get-%d.json", a.Number)), nil + } + case "seat:mesh-delivery.deliveries": + return read("deliveries.json"), nil + case "seat:mesh-delivery.groups": + return json.RawMessage(`[]`), nil + case "seat:mesh-delivery.show": + if f, ok := shows[a.ID]; ok { + return read(f), nil + } + } + return nil, os.ErrNotExist + } +} + +// The operator, 2026-10-28: "can the progress tool also report the progress made since the last call, or maybe better, +// the last 60 minutes". With since, an answer lists only what changed in that window under of, read from the +// tracker's notes, links, workers and closes, its milestones' history and mesh-delivery's transitions and checks. +func TestProgressSinceAWindowSaysOnlyWhatChanged(t *testing.T) { + at := func(s string) *time.Time { v, _ := time.Parse(time.RFC3339, s); return &v } + k := &KB{Records: progressRecords(), Seats: sinceWorld(t), Now: func() time.Time { return *at("2026-10-28T11:13:30Z") }} + p, err := k.Progress(ProgressAsk{Of: "issue 1422", Since: "2h"}) + if err != nil { + t.Fatal(err) + } + lines := strings.Split(p.Tree, "\n") + if lines[0] != "17 changes in the last 120 min under issue 1422 (since "+when(at("2026-10-28T09:13:30Z"))+")" || p.Changes != 17 || p.Since != "2h0m0s" { + t.Fatalf("%d changes, since %q:\n%s", p.Changes, p.Since, p.Tree) + } + for _, want := range []string{ + "▶ 1422 " + clip("The catch-up reads the trunk 'main' of repositories the catalogue follows by another branch or by a commit, and the forge holds none", 80) + " — → fixing by fix-trunk-unknown@shanks " + when(at("2026-10-28T09:38:37Z")) + + "; 9 notes, the last " + when(at("2026-10-28T11:05:21Z")) + " by shanks.node-tools", + "✓ mesh-catalog#537 (1422) opened " + when(at("2026-10-28T10:20:56Z")) + " · check pass " + when(at("2026-10-28T10:30:45Z")) + " · merged " + when(at("2026-10-28T10:34:49Z")) + ", on the machines", + "✓ mesh-delivery#94 (1126, 1422) opened ", + "▶ mesh-catalog#539 (1422, 1956) opened " + when(at("2026-10-28T11:07:42Z")) + ", now ready, waits for its merge", + } { + if !strings.Contains(p.Tree, want) { + t.Errorf("no %q in\n%s", want, p.Tree) + } + } + if strings.Contains(p.Tree, "de-spiegel") || strings.Contains(p.Tree, "triage") { + t.Errorf("a change outside issue 1422 or the window is said:\n%s", p.Tree) + } + w := strings.Join(p.Warnings, "\n") + for _, want := range []string{"keeps no history of an issue's moves", "keeps no time a merge reached its machines"} { + if !strings.Contains(w, want) { + t.Errorf("no %q in the warnings: %v", want, p.Warnings) + } + } + d, err := k.Progress(ProgressAsk{Of: "issue 1422", Since: "2h", Detail: DetailDetailed}) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{"│ ├── noted " + when(at("2026-10-28T09:38:09Z")) + " by shanks.node-tools: lead-47@shanks 2026-10-28 09:40Z.", + " └── merged " + when(at("2026-10-28T10:34:49Z")) + ": on the trunk its modules follow: merged there"} { + if !strings.Contains(d.Tree, want) { + t.Errorf("detailed: no %q in\n%s", want, d.Tree) + } + } + // A milestone's issue the tracker's get cannot answer falls back to its row's moved time, said as such. + m, err := k.Progress(ProgressAsk{Of: "milestone 47/L2", Since: "60m"}) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{"6 changes in the last 60 min under milestone 47/L2", "▶ 1126 " + clip("About 49,800 controller code lines stay after the delivery model, against a first limit of 25,000", 80) + " — moved " + + when(at("2026-10-28T11:13:28Z")) + ", now fixing (lead-47@shanks)", "✓ de-spiegel#6 (1126) check pass ", "✓ mesh-delivery#94 (1126, 1422) opened "} { + if !strings.Contains(m.Tree, want) { + t.Errorf("milestone: no %q in\n%s", want, m.Tree) + } + } + if w := strings.Join(m.Warnings, "\n"); !strings.Contains(w, "issue-tracker.get 1126") { + t.Errorf("the fallback is not said: %v", m.Warnings) + } + if small, _ := k.Progress(ProgressAsk{Of: "issue 1422", Since: "2h", Detail: DetailDetailed, Bound: LeastBound}); small == nil || size(small) > LeastBound || !strings.Contains(small.Cut, "raise bound") { + t.Errorf("a window's answer is not kept within its bound: %+v", small) + } + // A milestone's move, from its history: what it was before, what it is now, who moved it and when. + k.Now = func() time.Time { return *at("2026-10-26T18:40:00Z") } + ds, err := k.Progress(ProgressAsk{Of: "design 47", Since: "60m"}) + if err != nil { + t.Fatal(err) + } + if ds.Tree != "1 change in the last 60 min under design 47 (since "+when(at("2026-10-26T17:40:00Z"))+")\n└── ✓ milestone 47/6 open → closed: folded into R4g · by shanks.node-tools "+when(at("2026-10-26T18:10:10Z")) { + t.Errorf("design:\n%s", ds.Tree) + } + for _, bad := range []string{"0m", "-5m", "25h", "a day", "5"} { + if _, err := k.Progress(ProgressAsk{Of: "issue 1422", Since: bad}); err == nil || !strings.Contains(err.Error(), "since") { + t.Errorf("since %q: %v", bad, err) + } + } +} 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 0978ba3c..1d84ad87 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/README.md +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/README.md @@ -8,3 +8,8 @@ Captured for the progress verb's tests (novox/hq issue 1947), never written by h - `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. +- `since/`: answers of the live tracker and delivery seat on 2026-10-28 between 11:10Z and 11:20Z, for `since` + (issue 1422 and its pull requests): `issue-tracker.list` rows, `issue-tracker.get` 1422 and 1956 (the report, + the triage and each note's text after its first sentence or two cut), `mesh-delivery.deliveries` (its first ten + rows) and `.show` of four deliveries (the ready record, the ask and the paths cut), and `issue-tracker.milestones` + with `one` 47/6, its milestones joined by 47/L2's row from `one` 47/L2. diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/deliveries.json b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/deliveries.json new file mode 100644 index 00000000..76e7b34f --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/deliveries.json @@ -0,0 +1,12 @@ +{"deliveries": [ + {"for": "4m8s", "id": "novox/mesh-catalog@470687ea4cce", "number": 539, "since": "2026-10-28T11:07:43Z", "state": "ready", "waits": "its merge"}, + {"for": "14m17s", "id": "novox/mesh-catalog@fc5335360967", "number": 538, "since": "2026-10-28T10:57:33Z", "state": "closed"}, + {"for": "26m23s", "id": "novox/de-spiegel@fba6dbe3259e", "machines": "on the machines", "number": 6, "since": "2026-10-28T10:45:28Z", "state": "merged"}, + {"for": "36m56s", "group": "fix/1422-trunk-unknown", "id": "novox/mesh-delivery@2b801f5bd49a", "machines": "on the machines", "number": 94, "since": "2026-10-28T10:34:54Z", "state": "merged"}, + {"for": "37m2s", "group": "fix/1422-trunk-unknown", "id": "novox/mesh-catalog@f5d4b2f86ed4", "machines": "on the machines", "number": 537, "since": "2026-10-28T10:34:49Z", "state": "merged"}, + {"for": "56m30s", "id": "novox/hq@dd36b7557ae6", "machines": "on the machines", "number": 489, "since": "2026-10-28T10:15:20Z", "state": "merged"}, + {"for": "1h3m4s", "id": "novox/mesh-host@6502ba6ad6e6", "machines": "on the machines", "number": 159, "since": "2026-10-28T10:08:47Z", "state": "merged"}, + {"for": "1h24m56s", "group": "fix/1126-seed-same", "id": "novox/mesh-host@c912bcab6e73", "machines": "on the machines", "number": 158, "since": "2026-10-28T09:46:55Z", "state": "merged"}, + {"for": "2h51m31s", "id": "novox/mesh-catalog@c2606f99178a", "number": 534, "since": "2026-10-28T08:20:20Z", "state": "closed"}, + {"for": "3h25m11s", "group": "fix/1126-shadow-bodies", "id": "novox/mesh-host@82d0e125b2c0", "machines": "on the machines", "number": 157, "since": "2026-10-28T07:46:40Z", "state": "merged"} +]} diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/get-1422.json b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/get-1422.json new file mode 100644 index 00000000..bf3fe926 --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/get-1422.json @@ -0,0 +1,31 @@ +{ + "fixing-from": "2026-10-28T09:38:37Z", + "links": [ + {"at": "2026-10-20T14:37:00Z", "by": "triage", "kind": "relates-to", "target": "1421"}, + {"at": "2026-10-20T14:37:00Z", "by": "triage", "kind": "relates-to", "target": "1420"} + ], + "located-in": ["mesh-controller cmd/mesh-controller/catch_up.go"], + "needs": "none", + "notes": [ + {"at": "2026-10-20T14:37:00Z", "by": "triage", "text": "Triage of this issue: none of 429 older issues not resolved (the 429 most recent of 493; the titles of the rest did not fit the question) asks the same question."}, + {"at": "2026-10-20T14:44:57Z", "by": "triage", "text": "Triage: issue 1424 relates to this one, opened later."}, + {"at": "2026-10-20T14:45:10Z", "by": "triage", "text": "Triage: issue 1425 relates to this one, opened later."}, + {"at": "2026-10-28T09:38:09Z", "by": "shanks.node-tools", "text": "lead-47@shanks 2026-10-28 09:40Z. New kind beside this issue (diag-despiegel-check@shanks): the checker judges a pull request only when the gate's source.main names its repository"}, + {"at": "2026-10-28T09:43:50Z", "by": "shanks.node-tools", "text": "fix-trunk-unknown@shanks, 2026-10-28. Quick fix: the checker says loudly that a pull request cannot be judged when source.main names no trunk head of its repository."}, + {"at": "2026-10-28T09:51:00Z", "by": "shanks.node-tools", "text": "fix-trunk-unknown@shanks, 2026-10-28, part (b) built locally, not pushed."}, + {"at": "2026-10-28T10:00:02Z", "by": "shanks.node-tools", "text": "lead-47@shanks 2026-10-28 10:05Z. de-spiegel to main (the operator, 09:58Z: 'yes use the main branch please')."}, + {"at": "2026-10-28T10:07:56Z", "by": "shanks.node-tools", "text": "lead-47@shanks 2026-10-28 10:10Z. Side finding (tools-gitea-base@shanks): modules/gitea guard.ts and gitea_api's refusal text cite 'ADR 0266'"}, + {"at": "2026-10-28T10:40:43Z", "by": "shanks.node-tools", "text": "lead-47@shanks 2026-10-28 10:41Z. Fix group delivered in part (gate 4c091b08, gitea a6cc3509 on novox at 10:39Z; indexer applying; bridge and checker pending)."}, + {"at": "2026-10-28T10:44:38Z", "by": "shanks.node-tools", "text": "lead-47@shanks 2026-10-28 10:46Z. Subtask (the operator asked why a retarget waits for a poll)"}, + {"at": "2026-10-28T10:45:16Z", "by": "shanks.node-tools", "text": "lead-47@shanks 2026-10-28 10:49Z. Correction to note 10"}, + {"at": "2026-10-28T11:05:21Z", "by": "shanks.node-tools", "text": "Gitea polling, quick versus thorough (fix-gitea-events@shanks, branch fix/1422-gitea-events, commit 470687ea)."} + ], + "number": 1422, + "opened": "2026-10-20T14:36:07Z", + "opened-by": "shanks.node-tools", + "ready": true, + "severity": "minor", + "status": "fixing", + "title": "The catch-up reads the trunk 'main' of repositories the catalogue follows by another branch or by a commit, and the forge holds none", + "worker": {"active": "2026-10-28T11:05:21Z", "by": "fix-trunk-unknown@shanks", "since": "2026-10-28T09:38:37Z"} +} diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/get-1956.json b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/get-1956.json new file mode 100644 index 00000000..5a349e95 --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/get-1956.json @@ -0,0 +1,19 @@ +{ + "links": [ + {"at": "2026-10-28T11:07:31Z", "by": "triage", "kind": "relates-to", "target": "1903"}, + {"at": "2026-10-28T11:07:31Z", "by": "triage", "kind": "relates-to", "target": "1327"}, + {"at": "2026-10-28T11:07:31Z", "by": "triage", "kind": "relates-to", "target": "1420"}, + {"at": "2026-10-28T11:07:31Z", "by": "triage", "kind": "relates-to", "target": "1184"} + ], + "needs": "decision", + "notes": [ + {"at": "2026-10-28T11:07:31Z", "by": "triage", "text": "Triage of this issue: not a duplicate; closest is 1903."} + ], + "number": 1956, + "opened": "2026-10-28T11:07:10Z", + "opened-by": "shanks.node-tools", + "ready": true, + "severity": "major", + "status": "open", + "title": "Parts of the mesh still poll or wait on timers instead of reacting to events on the bus" +} diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/milestone-47-6.json b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/milestone-47-6.json new file mode 100644 index 00000000..0f1a6699 --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/milestone-47-6.json @@ -0,0 +1,9 @@ +{"history": [ + {"design": 47, "id": "6", "made": {"at": "2026-10-26T18:09:47Z", "by": "shanks.node-tools"}, "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", "status": "open"}, + {"design": 47, "id": "6", "made": {"at": "2026-10-26T18:09:47Z", "by": "shanks.node-tools"}, "moved": {"at": "2026-10-26T18:10:10Z", "by": "shanks.node-tools"}, "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", "reason": "folded into R4g", "status": "closed"} + ], + "milestones": [ + {"design": 47, "done": 0, "id": "6", "issues": [], "made": {"at": "2026-10-26T18:09:47Z", "by": "shanks.node-tools"}, "moved": {"at": "2026-10-26T18:10:10Z", "by": "shanks.node-tools"}, "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", "reason": "folded into R4g", "ref": "47/6", "status": "closed"}, + {"design": 47, "done": 0, "id": "L2", "issues": [1126], "made": {"at": "2026-10-26T18:09:47Z", "by": "shanks.node-tools"}, "order": 31, "outcome": "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)", "ref": "47/L2", "status": "open"} + ], + "said": "2 milestones."} diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/rows.json b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/rows.json new file mode 100644 index 00000000..c7abdfc9 --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/rows.json @@ -0,0 +1,5 @@ +[ + {"moved": "2026-10-28T11:13:28Z", "needs": "decision", "number": 1126, "owner": "mesh-controller", "ready": false, "severity": "minor", "since": "2026-10-27T01:01:41Z", "status": "fixing", "title": "About 49,800 controller code lines stay after the delivery model, against a first limit of 25,000", "worker": "lead-47@shanks"}, + {"moved": "2026-10-28T11:05:21Z", "needs": "none", "number": 1422, "owner": "mesh-controller cmd/mesh-controller/catch_up.go", "ready": true, "severity": "minor", "since": "2026-10-28T09:38:37Z", "status": "fixing", "title": "The catch-up reads the trunk 'main' of repositories the catalogue follows by another branch or by a commit, and the forge holds none", "worker": "fix-trunk-unknown@shanks"}, + {"moved": "2026-10-28T11:07:31Z", "needs": "decision", "number": 1956, "owner": "mesh-host; mesh-catalog modules/gitea; mesh-delivery; mesh-controller", "ready": true, "severity": "major", "status": "open", "title": "Parts of the mesh still poll or wait on timers instead of reacting to events on the bus"} +] diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/show-de-spiegel-6.json b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/show-de-spiegel-6.json new file mode 100644 index 00000000..c4cbcd4e --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/show-de-spiegel-6.json @@ -0,0 +1,10 @@ +{"delivery": {"id": "novox/de-spiegel@fba6dbe3259e", "number": 6, "repository": "novox/de-spiegel", "issues": [1126], "state": "merged", "since": "2026-10-28T10:45:28.285947172Z", + "title": "Take the practice's public names from config, not the definition (issue 1126)", + "check": {"at": "2026-10-28T10:44:13.035037566Z", "gate": "pass", "head": "fba6dbe3259e", "status": "mesh/check", "summary": "pass fba6dbe3259e on 927d302714af | unit 0 run 0 kept | int 0 run 0 kept | clone 31s lint 177ms | composition passed", "trunk": "927d302714af"}, + "merged_at": "2026-10-28T10:45:00Z", + "transitions": [ + {"at": "2026-10-28T08:49:52.467925371Z", "event": "announced", "from": "", "to": "proposed", "why": "novox/de-spiegel#6's head announced"}, + {"at": "2026-10-28T08:49:54.046089933Z", "event": "accepted", "from": "proposed", "to": "ready", "why": "a verdict names this commit and the trunk's head as its trunk commit, the gate passed or warned, and the repository's own check did not fail; or the check gates no merge (check-gates-merge false)"}, + {"at": "2026-10-28T10:45:28.285947172Z", "event": "merged", "from": "ready", "to": "merged", "why": "on the trunk its modules follow: merged there"} + ]}, + "machines": "on the machines", "waits": ""} diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/show-mesh-catalog-537.json b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/show-mesh-catalog-537.json new file mode 100644 index 00000000..13c93d85 --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/show-mesh-catalog-537.json @@ -0,0 +1,10 @@ +{"delivery": {"id": "novox/mesh-catalog@f5d4b2f86ed4", "number": 537, "repository": "novox/mesh-catalog", "issues": [1422], "group": "fix/1422-trunk-unknown", "state": "merged", "since": "2026-10-28T10:34:49.628764012Z", + "title": "Gate seeds a never-merged repository's trunk head and follows a retargeted base; gitea announces a retarget and sets a default branch and a pull request's base (issue 1422; group fix/1422-trunk-unknown)", + "check": {"at": "2026-10-28T10:30:45.793798647Z", "gate": "pass", "head": "f5d4b2f86ed4", "status": "mesh/check", "summary": "pass f5d4b2f86ed4 on 9fd95693db54 | unit 1329 run, 3852 kept, 13 skipped | integration 1330 run, 3968 kept, 21 skipped | over bound: unit, …", "trunk": "9fd95693db54"}, + "merged_at": "2026-10-28T10:34:47Z", + "transitions": [ + {"at": "2026-10-28T10:20:56.142821047Z", "event": "announced", "from": "", "to": "proposed", "why": "novox/mesh-catalog#537's head announced"}, + {"at": "2026-10-28T10:20:58.259805356Z", "event": "accepted", "from": "proposed", "to": "ready", "why": "a verdict names this commit and the trunk's head as its trunk commit, the gate passed or warned, and the repository's own check did not fail; or the check gates no merge (check-gates-merge false)"}, + {"at": "2026-10-28T10:34:49.628764012Z", "event": "merged", "from": "ready", "to": "merged", "why": "on the trunk its modules follow: merged there"} + ]}, + "machines": "on the machines", "waits": ""} diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/show-mesh-catalog-539.json b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/show-mesh-catalog-539.json new file mode 100644 index 00000000..07178ce7 --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/show-mesh-catalog-539.json @@ -0,0 +1,8 @@ +{"delivery": {"id": "novox/mesh-catalog@470687ea4cce", "number": 539, "repository": "novox/mesh-catalog", "issues": [1422, 1956], "state": "ready", "since": "2026-10-28T11:07:43.555244108Z", + "title": "gitea announces from the forge's events, not a poll: an edited base and a close are announced at once, one pass at start (issue 1422)", + "check_state": "pending", "merged_at": "0001-01-01T00:00:00Z", + "transitions": [ + {"at": "2026-10-28T11:07:42.740718441Z", "event": "announced", "from": "", "to": "proposed", "why": "novox/mesh-catalog#539's head announced"}, + {"at": "2026-10-28T11:07:43.555244108Z", "event": "accepted", "from": "proposed", "to": "ready", "why": "a verdict names this commit and the trunk's head as its trunk commit, the gate passed or warned, and the repository's own check did not fail; or the check gates no merge (check-gates-merge false)"} + ]}, + "waits": "its merge"} diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/show-mesh-delivery-94.json b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/show-mesh-delivery-94.json new file mode 100644 index 00000000..94f089e3 --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/testdata/progress/since/show-mesh-delivery-94.json @@ -0,0 +1,10 @@ +{"delivery": {"id": "novox/mesh-delivery@2b801f5bd49a", "number": 94, "repository": "novox/mesh-delivery", "issues": [1126, 1422], "group": "fix/1422-trunk-unknown", "state": "merged", "since": "2026-10-28T10:34:54.759088217Z", + "title": "Say loudly what cannot be judged, read only a merge's changed directories, and move the taken key only by an applied merge (issues 1422, 1126; group fix/1422-trunk-unknown)", + "check": {"at": "2026-10-28T10:30:45.875693927Z", "gate": "pass", "head": "2b801f5bd49a", "status": "mesh/check", "summary": "pass 2b801f5bd49a on ae7ec702a5e9 | unit 1329 run, 3852 kept, 13 skipped | integration 1330 run, 3968 kept, 21 skipped | over bound: unit, …", "trunk": "ae7ec702a5e9"}, + "merged_at": "2026-10-28T10:34:51Z", + "transitions": [ + {"at": "2026-10-28T10:20:50.017669046Z", "event": "announced", "from": "", "to": "proposed", "why": "novox/mesh-delivery#94's head announced"}, + {"at": "2026-10-28T10:20:51.900542745Z", "event": "accepted", "from": "proposed", "to": "ready", "why": "a verdict names this commit and the trunk's head as its trunk commit, the gate passed or warned, and the repository's own check did not fail; or the check gates no merge (check-gates-merge false)"}, + {"at": "2026-10-28T10:34:54.759088217Z", "event": "merged", "from": "ready", "to": "merged", "why": "on the trunk its modules follow: merged there"} + ]}, + "machines": "on the machines", "waits": ""}