From 87fe2c45cfedc01096c6da9913c0af965692dd75 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 17:02:47 +0100 Subject: [PATCH] Knowledge base: progress counts an issue done only once its fix is delivered, and says every gap (issue 1947, review round 1) A resolved issue whose delivery is held or failed is stuck, one merged and not delivered is moving; a row the tracker did not answer, an answer that does not read and linked issues cut by the list's limit are said as warnings instead of passing silently. --- .../cmd/mesh-knowledge/progress.go | 51 ++++++++++++++----- .../cmd/mesh-knowledge/progress_bus_test.go | 4 +- .../cmd/mesh-knowledge/progress_test.go | 26 +++++++--- 3 files changed, 60 insertions(+), 21 deletions(-) diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/progress.go b/modules/mesh-knowledge/cmd/mesh-knowledge/progress.go index 6bfd3b71..5edd5aaf 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/progress.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/progress.go @@ -208,9 +208,18 @@ func issueItem(r trackerRow) (Item, bool) { case r.Status == "wontfix": return it, false case r.Status == "resolved": - it.State, it.Why = StateDone, "resolved" - if r.Delivery != "" && !strings.HasPrefix(r.Delivery, "delivered") { - it.Why = "resolved · " + r.Delivery + // Done only when its fix is delivered, or it has nothing to deliver (ADR 0424 §2, §4; 1947/AC3). + switch d := r.Delivery; { + case d == "": + it.State, it.Why = StateDone, "resolved, nothing to deliver" + case strings.HasPrefix(d, "delivered"): + it.State, it.Why = StateDone, "resolved, delivered" + case held || strings.Contains(d, " stopped"): + it.State, it.Why = StateStuck, "resolved · "+d + case strings.HasPrefix(d, "fix merged, not delivered"): + it.State, it.Why = StateMoving, "resolved · "+d + default: + it.State, it.Why = StateWaiting, "resolved · "+d } case r.Abandoned || r.Stale: it.State, it.Why = StateStuck, fmt.Sprintf("%s, worker %s stale since %s", r.Status, r.Worker, when(r.Since)) @@ -259,6 +268,7 @@ func rollUp(parent *Item, children []Item) { func milestoneItems(d *Design, rows map[int]trackerRow, missing map[int]bool) ([]Item, []string) { var items []Item var warn []string + var unanswered []string for _, m := range d.Milestones { mi := Item{ID: m.ID, Title: m.Outcome} var kids []Item @@ -269,6 +279,7 @@ func milestoneItems(d *Design, rows map[int]trackerRow, missing map[int]bool) ([ } r, ok := rows[n] if !ok { + unanswered = append(unanswered, strconv.Itoa(n)) continue } if it, counts := issueItem(r); counts { @@ -279,6 +290,10 @@ func milestoneItems(d *Design, rows map[int]trackerRow, missing map[int]bool) ([ mi.Children = kids items = append(items, mi) } + if len(unanswered) > 0 { + warn = append(warn, fmt.Sprintf("the tracker neither answered nor named missing %d issues the milestones name, left out of the counts: %s", + len(unanswered), strings.Join(unanswered, ", "))) + } return items, warn } @@ -473,11 +488,15 @@ func (k *KB) progressRecord(p *Progress, kind string, n int) error { return fmt.Errorf("the issue tracker does not answer: %v", err) } var l struct { - Rows []trackerRow `json:"rows"` + Rows []trackerRow `json:"rows"` + Matched int `json:"matched"` } if err := json.Unmarshal(raw, &l); err != nil { return fmt.Errorf("issue-tracker.list answered something else: %v", err) } + if l.Matched > len(l.Rows) { + 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 it, counts := issueItem(r); counts { kids = append(kids, it) @@ -573,7 +592,9 @@ func (k *KB) readExtras(items []Item) (*extras, error) { Title string `json:"title"` } `json:"delivery"` } - _ = json.Unmarshal(raw, &s) + if err := json.Unmarshal(raw, &s); err != nil { + x.warnings = append(x.warnings, fmt.Sprintf("mesh-delivery.show %s answered something else: %v", d.ID, err)) + } d.Issues, d.Title = s.Delivery.Issues, s.Delivery.Title x.deliveries = append(x.deliveries, d) } @@ -617,7 +638,10 @@ func (k *KB) readExtras(items []Item) (*extras, error) { Target string `json:"target"` } `json:"links"` } - _ = json.Unmarshal(raw, &g) + if err := json.Unmarshal(raw, &g); err != nil { + x.warnings = append(x.warnings, fmt.Sprintf("issue-tracker.get %d answered something else: %v", n, err)) + continue + } ig := &issueGot{notes: len(g.Notes) + g.Left, last: "none"} if len(g.Notes) > 0 { last := g.Notes[len(g.Notes)-1] @@ -804,12 +828,19 @@ func fitProgress(p *Progress, bound int) { return } const raise = ": raise bound (at most 65536) or ask a smaller of, such as one milestone" - p.Cut = "the items, the tree's last lines and some warnings" + raise + p.Cut = "the items, some warnings and the tree's last lines" + raise var cut []string if len(p.Items) > 0 { p.Items = nil cut = append(cut, "the items (the tree says the same)") } + warned := len(p.Warnings) + for size(p) > bound && len(p.Warnings) > 0 { + p.Warnings = p.Warnings[:len(p.Warnings)-1] + } + if len(p.Warnings) < warned { + cut = append(cut, fmt.Sprintf("the last %d of %d warnings", warned-len(p.Warnings), warned)) + } lines := strings.Split(p.Tree, "\n") dropped := 0 for size(p) > bound && len(lines) > 1 { @@ -820,12 +851,6 @@ func fitProgress(p *Progress, bound int) { if dropped > 0 { cut = append(cut, fmt.Sprintf("the tree's last %d lines", dropped)) } - for size(p) > bound && len(p.Warnings) > 0 { - p.Warnings = p.Warnings[:len(p.Warnings)-1] - if !strings.Contains(strings.Join(cut, ""), "warnings") { - cut = append(cut, "some warnings") - } - } if exact := strings.Join(cut, "; ") + raise; len(exact) <= len(p.Cut) { p.Cut = exact } diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/progress_bus_test.go b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_bus_test.go index 95d3ffca..5a01c83d 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/progress_bus_test.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_bus_test.go @@ -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 6/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(strings.Join(p.Warnings, "\n"), "issue 1209 is in milestones R3b and R3c") { 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"}) @@ -53,7 +53,7 @@ func TestProgressOverTheBus(t *testing.T) { t.Fatal(err) } _ = json.Unmarshal(raw, &p) - for _, want := range []string{"▶ milestone 47/R6 · ", "▶ 1945 ", "jschoubben/car-hunter@cc4c8ce12901 ready since", "depends on 1943", "✓ 1946 "} { + for _, want := range []string{"✗ milestone 47/R6 · ", "▶ 1945 ", "jschoubben/car-hunter@cc4c8ce12901 ready since", "depends on 1943", "▶ 1946 "} { if !strings.Contains(p.Tree, want) { t.Errorf("detailed: no %q in\n%s", want, p.Tree) } diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go index b4cd61b6..b2895846 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go @@ -247,9 +247,10 @@ func TestIssueStatesAndTheirRollUp(t *testing.T) { why string counts bool }{ - {1942, StateDone, "resolved", true}, - {1946, StateDone, "resolved · fix merged, not delivered: mesh-catalog#475", true}, - {1906, StateDone, "resolved · held: mesh-delivery#39", true}, + {1942, StateDone, "resolved, delivered", true}, + {1943, StateDone, "resolved, nothing to deliver", true}, + {1946, StateMoving, "resolved · fix merged, not delivered: mesh-catalog#475", true}, + {1906, StateStuck, "resolved · held: mesh-delivery#39", true}, {1945, StateMoving, "fixing since", true}, {1049, StateStuck, "worker fix-chain-cat@shanks stale since", true}, {1126, StateWaiting, "open, needs a decision", true}, @@ -261,6 +262,16 @@ func TestIssueStatesAndTheirRollUp(t *testing.T) { t.Errorf("issue %d: %s %q (counts %v), want %s %q", c.n, it.State, it.Why, counts, c.state, c.why) } } + // The other delivery lines a resolved issue carries on the live tracker (981, 1290 and 1235 on 2026-10-26). + for line, state := range map[string]string{ + "fix not delivered: mesh-catalog#357 stopped:": StateStuck, + "fix not delivered: mesh-catalog#389 failed:": StateStuck, + "nothing heard from the delivery of mesh-delivery#5": StateWaiting, + } { + if it, _ := issueItem(trackerRow{Number: 1, Status: "resolved", Delivery: line}); it.State != state { + t.Errorf("resolved with %q: %s, want %s", line, it.State, state) + } + } at := func(h int) *time.Time { t := time.Date(2026, 10, 26, h, 0, 0, 0, time.UTC); return &t } for _, c := range []struct { name string @@ -298,11 +309,14 @@ func TestProgressOfADesignMinimalAndDetailed(t *testing.T) { if seats.asked["seat:issue-tracker.list"] != 1 || seats.asked["seat:mesh-delivery.deliveries"] != 0 || seats.asked["seat:issue-tracker.get"] != 0 { t.Fatalf("minimal asked %v", seats.asked) } + if w := strings.Join(p.Warnings, "\n"); !strings.Contains(w, "the tracker neither answered nor named missing") || !strings.Contains(w, "973, 525") { + t.Errorf("a row the tracker did not answer is not said: %v", p.Warnings) + } lines := strings.Split(p.Tree, "\n") if !strings.HasPrefix(lines[0], "✗ design 47 · Delivery: from a commit to delivered — ") { t.Fatalf("first line %q", lines[0]) } - for _, want := range []string{"▶ R6 6/7 1945 fixing since", "✗ 3 fix 0/1 1049 fixing, worker fix-chain-cat@shanks stale since"} { + 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) } @@ -311,7 +325,7 @@ func TestProgressOfADesignMinimalAndDetailed(t *testing.T) { if err != nil { t.Fatal(err) } - for _, want := range []string{"├── ✓ 1946 ", "resolved · fix merged, not delivered: mesh-catalog#475", + for _, want := range []string{"├── ▶ 1946 ", "resolved · fix merged, not delivered: mesh-catalog#475", "▶ 1945 The controller still plans", "jschoubben/car-hunter@cc4c8ce12901 ready since 10-26 17:05, waits for its merge", "depends on 1943", "97 notes; last 10-26 17:12 by shanks.node-tools"} { if !strings.Contains(d.Tree, want) { @@ -325,7 +339,7 @@ func TestProgressOfADesignMinimalAndDetailed(t *testing.T) { if err != nil { t.Fatal(err) } - if !strings.HasPrefix(m.Tree, "▶ milestone 47/R6 · ") || !strings.Contains(m.Tree, "✓ 1130") { + if !strings.HasPrefix(m.Tree, "✗ milestone 47/R6 · ") || !strings.Contains(m.Tree, "✓ 1130") { t.Fatalf("milestone:\n%s", m.Tree) } small, _ := k.Progress(ProgressAsk{Of: "design 47", Detail: DetailDetailed, Bound: LeastBound})