issue-tracker.changes reads each issue's moves over a window from the tracker's own log, so progress since says from what each issue moved, not only where it is
mesh/delivery merged: source.main moved to novox/mesh-catalog@9cdac09ad962
mesh/check being carried to dfe490d9e245 on 9cdac09ad962 | from 25f615456423

This commit is contained in:
2026-10-28 12:33:01 +01:00
parent 4f317201c9
commit dfe490d9e2
13 changed files with 431 additions and 25 deletions
@@ -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:issue-tracker.milestones", "seat:mesh-delivery.deliveries", "seat:mesh-delivery.show", "seat:mesh-delivery.groups"}) {
"seat:issue-tracker.milestones", "seat:issue-tracker.changes", "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", "milestones.go"} {
for _, f := range []string{"main.go", "hq.go", "progress.go", "progress_since.go", "milestones.go"} {
src += readFile(t, f)
}
for _, inv := range m.Invokes {
@@ -5,16 +5,17 @@ package main
// 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 moves over the window, from its own log (`issue-tracker.changes`): each issue opened or
// moved, from what to what, by whom and when;
// - 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.
// When the tracker cannot say its moves (a tracker without `changes`), a move is said by the status it reached, with its
// worker's since or its closed time, not from what it moved, and the answer says so. mesh-delivery keeps no time a
// merge reached its machines, so "on the machines" is as it reads now, said too.
import (
"encoding/json"
@@ -217,10 +218,13 @@ func (k *KB) issueChanges(p *Progress, w window) []change {
}
}
sort.Slice(moved, func(i, j int) bool { return moved[i].Moved.After(*moved[j].Moved) })
var moves map[int][]trackerMove
var merr error
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")
if moves, merr = k.trackerMoves(w); merr != nil {
p.Warnings = append(p.Warnings, fmt.Sprintf("issue-tracker.changes: %v; a move is said by the status it reached, "+
"its worker's since and its closed time, not from what it moved", merr))
}
}
var out []change
for i, r := range moved {
@@ -253,16 +257,35 @@ func (k *KB) issueChanges(p *Progress, w window) []change {
}
var evs []event
var says []string
if w.holds(g.Opened) {
if merr == nil {
for _, m := range moves[r.Number] {
if m.Kind == "opened" || m.Kind == "imported" {
evs = append(evs, event{at: m.At, line: fmt.Sprintf("%s %s by %s", m.Kind, when(&m.At), m.By)})
says = append(says, m.Kind+" by "+m.By+" "+when(&m.At))
continue
}
who, line := m.By, fmt.Sprintf("%s → %s %s by %s", m.From, m.To, when(&m.At), m.By)
if m.Worker != "" {
who, line = m.Worker, line+", worker "+m.Worker
}
short := fmt.Sprintf("%s → %s by %s %s", m.From, m.To, who, when(&m.At))
if (m.To == "resolved" || m.To == "wontfix") && len(g.FixedBy) > 0 {
short += ", fixed by " + strings.Join(g.FixedBy, ", ")
}
evs = append(evs, event{at: m.At, line: line})
says = append(says, short)
}
}
if merr != nil && 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") {
if merr != nil && 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) {
if merr != nil && 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, ", ")
@@ -438,3 +461,38 @@ func (k *KB) pullChanges(p *Progress, w window) []change {
}
return out
}
// trackerMove is one change of `issue-tracker.changes`: an issue opened or moved, from what to what, by whom and when.
type trackerMove struct {
Number int `json:"number"`
Kind string `json:"kind"`
From string `json:"from"`
To string `json:"to"`
By string `json:"by"`
Worker string `json:"worker"`
At time.Time `json:"at"`
}
// trackerMoves asks the tracker for every issue's moves in the window, from its own log, by issue, oldest first.
func (k *KB) trackerMoves(w window) (map[int][]trackerMove, error) {
raw, err := k.Seats(ChangesVerb, map[string]any{"since": w.to.Sub(w.from).String()})
if err != nil {
return nil, err
}
var r struct {
Changes []trackerMove `json:"changes"`
}
if err := json.Unmarshal(raw, &r); err != nil {
return nil, fmt.Errorf("answered something else: %v", err)
}
out := map[int][]trackerMove{}
for _, m := range r.Changes {
if w.holds(m.At) {
out[m.Number] = append(out[m.Number], m)
}
}
return out, nil
}
// ChangesVerb is the tracker's verb that answers its issues' moves over a window.
const ChangesVerb = "seat:issue-tracker.changes"
@@ -2,6 +2,7 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
@@ -396,8 +397,10 @@ func TestProgressOfAnIssueAndARecord(t *testing.T) {
}
// 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) {
// a get or a show it holds no answer for is refused, as a seat that cannot answer. The tracker's changes answers what
// mesh-issues' verb made of 1422's open and move to fixing (testdata/progress/README.md); without changes it is
// refused, as by a tracker that has no such verb yet.
func sinceWorld(t *testing.T, changes bool) 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
@@ -432,6 +435,11 @@ func sinceWorld(t *testing.T) func(string, any) (json.RawMessage, error) {
}
}
return json.Marshal(map[string]any{"rows": out, "matched": len(out)})
case "seat:issue-tracker.changes":
if changes {
return read("changes.json"), nil
}
return nil, errors.New("mesh-issues has no tool issue-tracker.changes")
case "seat:issue-tracker.get":
if a.Number == 1422 || a.Number == 1956 {
return read(fmt.Sprintf("get-%d.json", a.Number)), nil
@@ -454,7 +462,7 @@ func sinceWorld(t *testing.T) func(string, any) (json.RawMessage, error) {
// 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") }}
k := &KB{Records: progressRecords(), Seats: sinceWorld(t, true), 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)
@@ -464,7 +472,7 @@ func TestProgressSinceAWindowSaysOnlyWhatChanged(t *testing.T) {
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")) +
"▶ 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) + " — open → 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 ",
@@ -478,16 +486,25 @@ func TestProgressSinceAWindowSaysOnlyWhatChanged(t *testing.T) {
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)
}
if !strings.Contains(w, "keeps no time a merge reached its machines") || strings.Contains(w, "issue-tracker.changes") {
t.Errorf("the warnings: %v", p.Warnings)
}
// A tracker that cannot say its moves: each is said by the status it reached, and that is said.
old := &KB{Records: progressRecords(), Seats: sinceWorld(t, false), Now: k.Now}
o, err := old.Progress(ProgressAsk{Of: "issue 1422", Since: "2h"})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(o.Tree, " — → fixing by fix-trunk-unknown@shanks "+when(at("2026-10-28T09:38:37Z"))) || o.Changes != 17 ||
!strings.Contains(strings.Join(o.Warnings, "\n"), "issue-tracker.changes: mesh-issues has no tool issue-tracker.changes; a move is said by the status it reached") {
t.Errorf("without changes:\n%s\n%v", o.Tree, o.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.",
for _, want := range []string{"│ ├── open → fixing " + when(at("2026-10-28T09:38:37Z")) + " by shanks.node-tools, worker fix-trunk-unknown@shanks",
"│ ├── 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)
@@ -13,3 +13,7 @@ Captured for the progress verb's tests (novox/hq issue 1947), never written by h
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.
- `since/changes.json`: the answer of mesh-issues' `issue-tracker.changes` (window 2h at 2026-10-28 11:13:30Z) made by
the tracker's own code over its log, replaying issue 1422's open (10-20 14:36:07Z) and its move to fixing by
fix-trunk-unknown@shanks (10-28 09:38:37Z) through its verbs: the live verb was not delivered yet to capture from, and
the status 1422 moved from (open) is the replay's, as no verb read the live log then. Recapture once it is delivered.
@@ -0,0 +1,17 @@
{
"since": "2026-10-28T09:13:30Z",
"changes": [
{
"number": 1422,
"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",
"kind": "moved",
"from": "open",
"to": "fixing",
"by": "shanks.node-tools",
"worker": "fix-trunk-unknown@shanks",
"at": "2026-10-28T09:38:37Z"
}
],
"entries-read": 4,
"said": "1 changes of status since 2026-10-28T09:13:30Z, from the tracker's log (4 entries read)."
}