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":
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
}
@@ -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)
}
@@ -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})