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
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:
@@ -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 <n>`, `milestone <design>/<id>`, `issue <n>`, `decision <n>` or `ADR <n>`, `research <n>`), `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 |
|
||||
| `progress` | `of` (`mesh`, `design <n>`, `milestone <design>/<id>`, `issue <n>`, `decision <n>` or `ADR <n>`, `research <n>`), `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 or moved from what to what (from `issue-tracker.changes`, the tracker's log), noted or linked (from `issue-tracker.get`), pull requests opened, checked, merged, held or closed (from `mesh-delivery.show`'s transitions and check). It keeps no state; its warnings say when the tracker cannot answer `changes` (a move is then said by the status it reached, not from what) and that mesh-delivery keeps no time 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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
+17
@@ -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)."
|
||||
}
|
||||
@@ -88,6 +88,7 @@
|
||||
"seat:issue-tracker.list",
|
||||
"seat:issue-tracker.get",
|
||||
"seat:issue-tracker.milestones",
|
||||
"seat:issue-tracker.changes",
|
||||
"seat:mesh-delivery.deliveries",
|
||||
"seat:mesh-delivery.show",
|
||||
"seat:mesh-delivery.groups"
|
||||
|
||||
Reference in New Issue
Block a user