mesh-issues: review — the verdict note has its own marker, ready is said once per spell and follows a duplicate to its original, a delivery that cannot be asked is not taken, fixed-by read in any case and as a page; no built binary (hq ADR 0279)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds mesh-issues → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)

This commit is contained in:
jochen
2026-10-10 17:54:46 +02:00
parent 745985fd23
commit e6fb779eb5
7 changed files with 202 additions and 36 deletions
+1
View File
@@ -13,3 +13,4 @@ modules/messenger/messenger
modules/claude-code/claude-code
modules/mesh-watcher/mesh-watcher
modules/mesh-delivery/mesh-delivery
modules/mesh-issues/cmd/mesh-issues/mesh-issues
+9 -6
View File
@@ -129,8 +129,11 @@ func unresolved(is *Issue) bool {
return is.Status == StatusOpen || is.Status == StatusDiagnosing || is.Status == StatusLocated
}
// notePrefix starts the triage's note on the issue it judged.
const notePrefix = "Triage: "
// notePrefix starts every note the triage writes; verdictPrefix the one on the issue it judged.
const (
notePrefix = "Triage: "
verdictPrefix = "Triage of this issue: "
)
var (
legacyLine = regexp.MustCompile(`^Duplicate check: (duplicate of|related to) ([0-9]+)\.`)
@@ -455,15 +458,15 @@ func triageNote(is *Issue, v *Verdict) string {
switch {
case v.closes():
fmt.Fprintf(&b, "%sduplicate of %d. %s This issue is closed as wontfix and its report is kept as a note on %d, "+
"which stays open.\n", notePrefix, v.Issue, v.Reason, v.Issue)
"which stays open.\n", verdictPrefix, v.Issue, v.Reason, v.Issue)
case v.Verdict == VerdictNone:
most := ""
if v.Older > v.Checked {
most = fmt.Sprintf(" (the %d most recent of %d; the titles of the rest did not fit the question)", v.Checked, v.Older)
}
fmt.Fprintf(&b, "%snone of %d older issues not resolved%s asks the same question.\n", notePrefix, v.Checked, most)
fmt.Fprintf(&b, "%snone of %d older issues not resolved%s asks the same question.\n", verdictPrefix, v.Checked, most)
default:
fmt.Fprintf(&b, "%snot a duplicate; closest is %d.\n", notePrefix, v.Issue)
fmt.Fprintf(&b, "%snot a duplicate; closest is %d.\n", verdictPrefix, v.Issue)
}
value := func(name, given, judged, why string) {
if judged == "" {
@@ -551,7 +554,7 @@ func (t *Tracker) apply(newer uint64, v *Verdict) ([]string, error) {
}
// 1. One note saying the whole judgement.
if !noted(is, notePrefix) {
if !noted(is, verdictPrefix) {
if _, err := t.note(GroomBy, newer, triageNote(is, v)); err != nil {
return did, err
}
@@ -91,6 +91,12 @@ type Issue struct {
SuggestedOwner string `json:"suggested-owner,omitempty"`
// Delivered is the pull requests of FixedBy the delivery said are delivered.
Delivered []string `json:"delivered,omitempty"`
// Undelivered is the pull requests of FixedBy asked of the delivery at the move to resolved and not yet
// delivered then; their own delivery event marks them.
Undelivered []string `json:"undelivered,omitempty"`
// ReadyFor is the issues it depended on when it was last said ready (ADR 0279 §3), so one spell of
// readiness is said once however many dependencies let it go at one delivery.
ReadyFor []uint64 `json:"ready-for,omitempty"`
// Triage is the triage's judgement as it was made, kept before it is applied.
Triage *Verdict `json:"triage,omitempty"`
// Opening is the id of the call that opened it, so a write whose answer was lost is known as its own.
Binary file not shown.
+102 -20
View File
@@ -118,6 +118,21 @@ func (t *Tracker) set(by string, a SetAsk, onlyEmpty bool) (*SetDone, error) {
var errNothing = errors.New("nothing to change")
// MostDeliveryAsks is how many deliveries one handling of a `moved` event asks of mesh-delivery.
const MostDeliveryAsks = 3
func equalNumbers(a, b []uint64) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// dependsOn is the issues an issue depends on.
func dependsOn(is *Issue) []uint64 {
var out []uint64
@@ -152,14 +167,23 @@ func reaches(all map[uint64]*Issue, from, to uint64) bool {
return false
}
// pullRequestRef is a fixed-by entry naming a pull request: `<repository>#<number>`, optionally with the owner.
var pullRequestRef = regexp.MustCompile(`^(?:[A-Za-z0-9._-]+/)?([a-z0-9][a-z0-9._-]{0,99})#([1-9][0-9]{0,6})$`)
// pullRequestRef is a fixed-by entry naming a pull request: `<repository>#<number>`, optionally with the owner;
// pullRequestPage the forge's page of one.
var (
pullRequestRef = regexp.MustCompile(`^(?:[a-z0-9._-]+/)?([a-z0-9][a-z0-9._-]{0,99})#([1-9][0-9]{0,6})$`)
pullRequestPage = regexp.MustCompile(`^https?://\S+/([a-z0-9][a-z0-9._-]{0,99})/pulls?/([1-9][0-9]{0,6})/?$`)
)
// pullRequests is the pull requests an issue's fixed-by names, as `<repository>#<number>`.
// pullRequests is the pull requests an issue's fixed-by names, as `<repository>#<number>`, lower-case.
func pullRequests(is *Issue) []string {
var out []string
for _, f := range is.FixedBy {
if m := pullRequestRef.FindStringSubmatch(strings.TrimSpace(f)); m != nil {
f = strings.ToLower(strings.TrimSpace(f))
m := pullRequestRef.FindStringSubmatch(f)
if m == nil {
m = pullRequestPage.FindStringSubmatch(f)
}
if m != nil && !contains(out, m[1]+"#"+m[2]) {
out = append(out, m[1]+"#"+m[2])
}
}
@@ -175,11 +199,29 @@ func contains(list []string, s string) bool {
return false
}
// done says whether an issue another depends on lets it go: wontfix, or resolved with every pull request its
// fix names delivered.
func done(dep *Issue) bool {
if dep.Status == StatusWontfix {
return true
// done says whether an issue another depends on lets it go: resolved with every pull request its fix names
// delivered, or wontfix — but a wontfix closed as the duplicate of another lets go only when that one does, as
// the work moved there (ADR 0279 §3).
func done(all map[uint64]*Issue, dep *Issue) bool {
seen := map[uint64]bool{}
for dep != nil && dep.Status == StatusWontfix {
if seen[dep.Number] {
return false
}
seen[dep.Number] = true
original := uint64(0)
for _, l := range dep.Links {
if l.Kind == LinkDuplicates {
original, _ = numberOfKey(l.Target)
}
}
if original == 0 {
return true
}
dep = all[original]
}
if dep == nil {
return false
}
if dep.Status != StatusResolved {
return false
@@ -196,7 +238,7 @@ func done(dep *Issue) bool {
func waitsFor(all map[uint64]*Issue, is *Issue) []uint64 {
var out []uint64
for _, n := range dependsOn(is) {
if dep := all[n]; dep == nil || !done(dep) {
if dep := all[n]; dep == nil || !done(all, dep) {
out = append(out, n)
}
}
@@ -278,24 +320,48 @@ func (t *Tracker) letGo(dep uint64) error {
return err
}
d := all[dep]
if d == nil || !done(d) {
if d == nil || !done(all, d) {
return nil
}
for _, w := range neededBy(all, dep) {
// Those that depend on it, and on anything closed as its duplicate.
waiting := neededBy(all, dep)
for n, is := range all {
for _, l := range is.Links {
if l.Kind == LinkDuplicates && l.Target == keyOf(dep) {
waiting = append(waiting, neededBy(all, n)...)
}
}
}
for _, w := range waiting {
is := all[w]
if is == nil || !isReady(all, is) {
continue
}
prefix := fmt.Sprintf("Ready: issue %d", dep)
if noted(is, prefix) {
// Said once per spell of readiness: the dependencies it was said ready for are kept, claimed by
// compare-and-set before the note, so a second dependency letting it go at the same delivery, or the
// same event again, says nothing.
deps := dependsOn(is)
sort.Slice(deps, func(i, j int) bool { return deps[i] < deps[j] })
claimed := false
if _, err := t.change(w, func(i *Issue) error {
claimed = false
if equalNumbers(i.ReadyFor, deps) {
return nil
}
i.ReadyFor, claimed = deps, true
return nil
}); err != nil {
return err
}
if !claimed {
continue
}
why := "is resolved and its fix is delivered"
if d.Status == StatusWontfix {
why = "was closed as wontfix"
}
if _, err := t.note(GroomBy, w, fmt.Sprintf("%s, which this one depends on, %s; nothing it depends on is left "+
"waiting, so it is ready. (novox/hq ADR 0279)", prefix, why)); err != nil {
if _, err := t.note(GroomBy, w, fmt.Sprintf("Ready: issue %d, which this one depends on, %s; nothing it depends "+
"on is left waiting, so it is ready. (novox/hq ADR 0279)", dep, why)); err != nil {
return err
}
t.emit(EventReady, map[string]any{"number": w, "after": dep, "at": t.now()})
@@ -375,21 +441,37 @@ func (t *Tracker) OnMoved(e stdio.Envelope) error {
return err
}
if body.To == StatusResolved && t.Deliveries != nil {
// At most a few asks per handling, each up to the bus's 30 seconds, under the runtime's two minutes; one
// that fails, or more left to ask, has the event offered again, so a delivery not asked is never silent:
// past the last offer the controller raises max-deliveries.
asked := 0
for _, pr := range pullRequests(is) {
if contains(is.Delivered, pr) {
if contains(is.Delivered, pr) || contains(is.Undelivered, pr) {
continue
}
if asked == MostDeliveryAsks {
return fmt.Errorf("issue %d names more pull requests than are asked at once; the rest at the next offer", body.Number)
}
asked++
repo, number, _ := strings.Cut(pr, "#")
state, err := t.Deliveries(repo, number)
if err != nil {
// Not known now: the delivery's own event marks it when it comes.
t.Logf("[mesh-issues] the delivery of %s (issue %d) could not be asked: %v", pr, body.Number, err)
continue
return fmt.Errorf("the delivery of %s (issue %d) could not be asked: %v", pr, body.Number, err)
}
if state == "delivered" {
if _, err := t.markDelivered(pr, body.Number); err != nil {
return err
}
continue
}
// Asked and not delivered yet: the delivery's own event marks it when it comes.
if _, err := t.change(body.Number, func(i *Issue) error {
if !contains(i.Undelivered, pr) {
i.Undelivered = append(i.Undelivered, pr)
}
return nil
}); err != nil {
return err
}
}
}
@@ -620,6 +620,8 @@ type Row struct {
Severity string `json:"severity,omitempty"`
Needs string `json:"needs,omitempty"`
Ready bool `json:"ready"`
// SuggestedOwner is the triage's or a person's suggestion, for an issue located nowhere yet.
SuggestedOwner string `json:"suggested-owner,omitempty"`
}
// Listed is the answer to a list.
@@ -697,12 +699,8 @@ func (t *Tracker) List(a ListAsk) (*Listed, error) {
}
out.Matched++
if len(out.Rows) < limit {
owner := strings.Join(is.LocatedIn, "; ")
if owner == "" && is.SuggestedOwner != "" {
owner = is.SuggestedOwner + " (suggested)"
}
out.Rows = append(out.Rows, Row{Number: n, Status: is.Status, Owner: owner, Title: is.Title,
Severity: is.Severity, Needs: is.Needs, Ready: ready})
out.Rows = append(out.Rows, Row{Number: n, Status: is.Status, Owner: strings.Join(is.LocatedIn, "; "), Title: is.Title,
Severity: is.Severity, Needs: is.Needs, Ready: ready, SuggestedOwner: is.SuggestedOwner})
}
}
switch {
@@ -116,7 +116,7 @@ func TestAClearDuplicateIsClosedAndItsReportKeptOnTheOlder(t *testing.T) {
t.Fatalf("links %+v", n.Links)
}
notes := triageNotes(n)
if len(notes) != 1 || !strings.HasPrefix(notes[0], "Triage: duplicate of 365.") ||
if len(notes) != 1 || !strings.HasPrefix(notes[0], "Triage of this issue: duplicate of 365.") ||
!strings.Contains(notes[0], "the agent on control (sonnet)") || !strings.Contains(notes[0], "Severity: minor — a contained fix") {
t.Fatalf("newer's notes %q", notes)
}
@@ -160,7 +160,7 @@ func TestRelatedOrNotCertainIsLinkedAndNotedAndNothingMoves(t *testing.T) {
if len(n.Links) != 1 || n.Links[0].Kind != LinkRelatesTo || n.Links[0].Target != "365" {
t.Fatalf("%v: links %+v", v, n.Links)
}
if nn := triageNotes(n); len(nn) != 1 || !strings.HasPrefix(nn[0], "Triage: not a duplicate; closest is 365.") ||
if nn := triageNotes(n); len(nn) != 1 || !strings.HasPrefix(nn[0], "Triage of this issue: not a duplicate; closest is 365.") ||
!strings.Contains(nn[0], "Relates to 365") {
t.Fatalf("%v: %q", v, nn)
}
@@ -187,7 +187,7 @@ func TestAnIssueThatRepeatsNothingGainsOneNoteAndItsValues(t *testing.T) {
n.SuggestedOwner != "mesh-catalog modules/records" {
t.Fatalf("%+v", n)
}
if nn := triageNotes(n); len(nn) != 1 || !strings.HasPrefix(nn[0], "Triage: none of 2 older issues") ||
if nn := triageNotes(n); len(nn) != 1 || !strings.HasPrefix(nn[0], "Triage of this issue: none of 2 older issues") ||
!strings.Contains(nn[0], "Needs: operator — only the operator knows") || !strings.Contains(nn[0], "Suggested owner: mesh-catalog modules/records") {
t.Fatalf("%q", nn)
}
@@ -728,7 +728,83 @@ func TestThePassRunsBesideTheTriageOfNewIssues(t *testing.T) {
if p.State != PassDone || p.Judged != 6 {
t.Fatalf("%+v", p)
}
if nn := triageNotes(get(t, h, newer)); len(nn) != 1 || !strings.HasPrefix(nn[0], "Triage: none of 6 older") {
if nn := triageNotes(get(t, h, newer)); len(nn) != 1 || !strings.HasPrefix(nn[0], "Triage of this issue: none of 6 older") {
t.Fatalf("%q", nn)
}
}
// After a judgement that failed, the next one still writes its verdict note (review of PR 184).
func TestAFailureNoteDoesNotHideTheVerdictNote(t *testing.T) {
h := newHarness(t)
h.open(t, "the counter skips a number")
n := h.open(t, "a number is skipped")
s := withJudge(h, &stubJudge{err: errors.New("down")})
if err := h.Check(n); err == nil {
t.Fatal("a failure was taken")
}
s.err = nil
if err := h.Check(n); err != nil {
t.Fatal(err)
}
nn := triageNotes(get(t, h, n))
if len(nn) != 2 || !strings.HasPrefix(nn[1], "Triage of this issue: none of 1") {
t.Fatalf("%q", nn)
}
}
// One delivery that lets two dependencies go says ready once; a wontfix duplicate lets go only with its original;
// fixed-by is read in any case and as the forge's page; a delivery that cannot be asked is not taken.
func TestReadyIsSaidOnceFollowsDuplicatesAndAskingFailsLoudly(t *testing.T) {
h := newHarness(t)
a := h.open(t, "a")
b := h.open(t, "b")
w := h.open(t, "waits on both")
_, _ = h.Link(LinkAsk{Number: w, Kind: LinkDependsOn, Target: keyOf(a)})
_, _ = h.Link(LinkAsk{Number: w, Kind: LinkDependsOn, Target: keyOf(b)})
h.Deliveries = func(string, string) (string, error) { return "merged", nil }
resolve(t, h, a, "Mesh-Catalog#9")
resolve(t, h, b, "https://forge.example/novox/mesh-catalog/pulls/9")
if got := pullRequests(get(t, h, a)); !reflect.DeepEqual(got, []string{"mesh-catalog#9"}) {
t.Fatalf("%v", got)
}
if got := pullRequests(get(t, h, b)); !reflect.DeepEqual(got, []string{"mesh-catalog#9"}) {
t.Fatalf("%v", got)
}
tr := stdio.Envelope{Body: json.RawMessage(`{"repository":"novox/mesh-catalog","number":9,"to":"delivered"}`)}
if err := h.OnTransition(tr); err != nil {
t.Fatal(err)
}
readies := 0
for _, e := range h.said() {
if e.Event == EventReady {
readies++
}
}
if readies != 1 || len(triageNotes(get(t, h, w))) != 1 {
t.Fatalf("%d ready events, notes %q", readies, triageNotes(get(t, h, w)))
}
h2 := newHarness(t)
orig := h2.open(t, "the original")
dup := h2.open(t, "its duplicate")
w2 := h2.open(t, "waits on the duplicate")
_, _ = h2.Link(LinkAsk{Number: w2, Kind: LinkDependsOn, Target: keyOf(dup)})
_, _ = h2.Link(LinkAsk{Number: dup, Kind: LinkDuplicates, Target: keyOf(orig)})
_, _ = h2.Move(MoveAsk{Number: dup, Status: StatusWontfix, Reason: "duplicate of 365"})
_ = h2.OnMoved(stdio.Envelope{Body: json.RawMessage(`{"number":366,"to":"wontfix"}`)})
if v, _ := h2.View(w2); v.Ready {
t.Fatal("ready while the original is open")
}
h2.Deliveries = func(string, string) (string, error) { return "", errors.New("mesh-delivery did not answer") }
resolve(t, h2, orig, "mesh-catalog#3")
if err := h2.OnMoved(stdio.Envelope{Body: json.RawMessage(`{"number":365,"to":"resolved"}`)}); err == nil {
t.Fatal("a delivery that could not be asked was taken")
}
h2.Deliveries = func(string, string) (string, error) { return "delivered", nil }
if err := h2.OnMoved(stdio.Envelope{Body: json.RawMessage(`{"number":365,"to":"resolved"}`)}); err != nil {
t.Fatal(err)
}
if v, _ := h2.View(w2); !v.Ready || len(triageNotes(get(t, h2, w2))) != 1 {
t.Fatalf("not let go through the duplicate: %+v", v)
}
}