Knowledge base: progress counts an issue done only once its fix is delivered, and says every gap (issue 1947, review round 1)
mesh/delivery merged: source.main moved to novox/mesh-catalog@26908a89506f
mesh/delivery-group group feat/1947-progress merged: every member is merged
mesh/check pass 87fe2c45cfed on 26908a89506f | carried from 27a7757b970e, hq 3a5c4c7c4d9c | unit 2091 run, 2327 kept, 6 skipped | integration 2112 run…

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.
This commit is contained in:
2026-10-26 17:02:47 +01:00
parent 5baf8ad1d7
commit 87fe2c45cf
3 changed files with 60 additions and 21 deletions
@@ -208,9 +208,18 @@ func issueItem(r trackerRow) (Item, bool) {
case r.Status == "wontfix": case r.Status == "wontfix":
return it, false return it, false
case r.Status == "resolved": case r.Status == "resolved":
it.State, it.Why = StateDone, "resolved" // Done only when its fix is delivered, or it has nothing to deliver (ADR 0424 §2, §4; 1947/AC3).
if r.Delivery != "" && !strings.HasPrefix(r.Delivery, "delivered") { switch d := r.Delivery; {
it.Why = "resolved · " + 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: 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)) 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) { func milestoneItems(d *Design, rows map[int]trackerRow, missing map[int]bool) ([]Item, []string) {
var items []Item var items []Item
var warn []string var warn []string
var unanswered []string
for _, m := range d.Milestones { for _, m := range d.Milestones {
mi := Item{ID: m.ID, Title: m.Outcome} mi := Item{ID: m.ID, Title: m.Outcome}
var kids []Item var kids []Item
@@ -269,6 +279,7 @@ func milestoneItems(d *Design, rows map[int]trackerRow, missing map[int]bool) ([
} }
r, ok := rows[n] r, ok := rows[n]
if !ok { if !ok {
unanswered = append(unanswered, strconv.Itoa(n))
continue continue
} }
if it, counts := issueItem(r); counts { 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 mi.Children = kids
items = append(items, mi) 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 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) return fmt.Errorf("the issue tracker does not answer: %v", err)
} }
var l struct { var l struct {
Rows []trackerRow `json:"rows"` Rows []trackerRow `json:"rows"`
Matched int `json:"matched"`
} }
if err := json.Unmarshal(raw, &l); err != nil { if err := json.Unmarshal(raw, &l); err != nil {
return fmt.Errorf("issue-tracker.list answered something else: %v", err) 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 { for _, r := range l.Rows {
if it, counts := issueItem(r); counts { if it, counts := issueItem(r); counts {
kids = append(kids, it) kids = append(kids, it)
@@ -573,7 +592,9 @@ func (k *KB) readExtras(items []Item) (*extras, error) {
Title string `json:"title"` Title string `json:"title"`
} `json:"delivery"` } `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 d.Issues, d.Title = s.Delivery.Issues, s.Delivery.Title
x.deliveries = append(x.deliveries, d) x.deliveries = append(x.deliveries, d)
} }
@@ -617,7 +638,10 @@ func (k *KB) readExtras(items []Item) (*extras, error) {
Target string `json:"target"` Target string `json:"target"`
} `json:"links"` } `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"} ig := &issueGot{notes: len(g.Notes) + g.Left, last: "none"}
if len(g.Notes) > 0 { if len(g.Notes) > 0 {
last := g.Notes[len(g.Notes)-1] last := g.Notes[len(g.Notes)-1]
@@ -804,12 +828,19 @@ func fitProgress(p *Progress, bound int) {
return return
} }
const raise = ": raise bound (at most 65536) or ask a smaller of, such as one milestone" 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 var cut []string
if len(p.Items) > 0 { if len(p.Items) > 0 {
p.Items = nil p.Items = nil
cut = append(cut, "the items (the tree says the same)") 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") lines := strings.Split(p.Tree, "\n")
dropped := 0 dropped := 0
for size(p) > bound && len(lines) > 1 { for size(p) > bound && len(lines) > 1 {
@@ -820,12 +851,6 @@ func fitProgress(p *Progress, bound int) {
if dropped > 0 { if dropped > 0 {
cut = append(cut, fmt.Sprintf("the tree's last %d lines", dropped)) 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) { if exact := strings.Join(cut, "; ") + raise; len(exact) <= len(p.Cut) {
p.Cut = exact p.Cut = exact
} }
@@ -45,7 +45,7 @@ func TestProgressOverTheBus(t *testing.T) {
} }
lines := strings.Split(p.Tree, "\n") 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 — ") || 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) 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"}) 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) t.Fatal(err)
} }
_ = json.Unmarshal(raw, &p) _ = 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) { if !strings.Contains(p.Tree, want) {
t.Errorf("detailed: no %q in\n%s", want, p.Tree) t.Errorf("detailed: no %q in\n%s", want, p.Tree)
} }
@@ -247,9 +247,10 @@ func TestIssueStatesAndTheirRollUp(t *testing.T) {
why string why string
counts bool counts bool
}{ }{
{1942, StateDone, "resolved", true}, {1942, StateDone, "resolved, delivered", true},
{1946, StateDone, "resolved · fix merged, not delivered: mesh-catalog#475", true}, {1943, StateDone, "resolved, nothing to deliver", true},
{1906, StateDone, "resolved · held: mesh-delivery#39", 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}, {1945, StateMoving, "fixing since", true},
{1049, StateStuck, "worker fix-chain-cat@shanks stale since", true}, {1049, StateStuck, "worker fix-chain-cat@shanks stale since", true},
{1126, StateWaiting, "open, needs a decision", 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) 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 } at := func(h int) *time.Time { t := time.Date(2026, 10, 26, h, 0, 0, 0, time.UTC); return &t }
for _, c := range []struct { for _, c := range []struct {
name string 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 { 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) 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") lines := strings.Split(p.Tree, "\n")
if !strings.HasPrefix(lines[0], "✗ design 47 · Delivery: from a commit to delivered — ") { if !strings.HasPrefix(lines[0], "✗ design 47 · Delivery: from a commit to delivered — ") {
t.Fatalf("first line %q", lines[0]) 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) { if !strings.Contains(p.Tree, want) {
t.Errorf("no %q in\n%s", want, p.Tree) t.Errorf("no %q in\n%s", want, p.Tree)
} }
@@ -311,7 +325,7 @@ func TestProgressOfADesignMinimalAndDetailed(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) 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", "▶ 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"} { "depends on 1943", "97 notes; last 10-26 17:12 by shanks.node-tools"} {
if !strings.Contains(d.Tree, want) { if !strings.Contains(d.Tree, want) {
@@ -325,7 +339,7 @@ func TestProgressOfADesignMinimalAndDetailed(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) 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) t.Fatalf("milestone:\n%s", m.Tree)
} }
small, _ := k.Progress(ProgressAsk{Of: "design 47", Detail: DetailDetailed, Bound: LeastBound}) small, _ := k.Progress(ProgressAsk{Of: "design 47", Detail: DetailDetailed, Bound: LeastBound})