From 09ea4a40bfa96f3efbd9d469519adb7d2d33b47a Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 9 Nov 2026 23:20:22 +0100 Subject: [PATCH] A closing keeps the review round, get says the round, a close waits for the operator's round-5 answer (issue 784, ADR 0665) Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW --- .../cmd/mesh-knowledge/answers.go | 20 +++++++-- .../cmd/mesh-knowledge/close.go | 13 +++++- .../cmd/mesh-knowledge/review.go | 2 +- .../cmd/mesh-knowledge/review_round_test.go | 44 +++++++++++++++++++ 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/answers.go b/modules/mesh-knowledge/cmd/mesh-knowledge/answers.go index 4a9941fb3..84737abf4 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/answers.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/answers.go @@ -311,6 +311,8 @@ type Got struct { // Idea is said of a research effort kept as an idea: it is not what holds (ADR 0629 §2). Idea string `json:"idea,omitempty"` Waiting string `json:"waiting,omitempty"` + // Round is the round of the next review of the working version (ADR 0665). + Round int `json:"round,omitempty"` // Retracted are the retractions the operator approved; Retracting, one waiting for the operator. Retracted []RetractionRow `json:"retracted,omitempty"` Retracting string `json:"retracting,omitempty"` @@ -386,11 +388,21 @@ func (k *KB) Get(a GetAsk) (*Got, error) { e.PendingVersion, k, last.Version) } } - if r.Ask != nil { - g.Waiting = "the operator's ask: " + r.Ask.Said - if r.Ask.Said == AskWaiting && r.Ask.Held != "" { + if a.Version == 0 { + g.Round = e.Working().Round() + } + ask := r.Ask + if a.Version == 0 && e.Pending != nil && e.Pending.Ask != nil { + ask = e.Pending.Ask // the ask of the version being worked + } + if ask != nil { + g.Waiting = "the operator's ask: " + ask.Said + if ask.Round > 0 { + g.Waiting = fmt.Sprintf("the operator's ask after round %d asked changes: %s", ask.Round, ask.Said) + } + if ask.Said == AskWaiting && ask.Held != "" { // Held by the router, not shown to the operator yet: said so, with why (ADR 0397, issue 926). - g.Waiting = "the operator's ask is held by the router, not yet shown to the operator: " + r.Ask.Held + g.Waiting = "the operator's ask is held by the router, not yet shown to the operator: " + ask.Held } } if a.Version == 0 { diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/close.go b/modules/mesh-knowledge/cmd/mesh-knowledge/close.go index 5d2c95085..5970de867 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/close.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/close.go @@ -85,6 +85,10 @@ func (k *KB) Close(caller string, a CloseAsk) (*Closed, error) { if cur.Retracting != nil { return nil, fmt.Errorf("%s has a retraction waiting for the operator; nothing was closed", key) } + if w := e.Working(); w.Ask != nil && w.Ask.Said == AskWaiting && w.Ask.Round > 0 { + return nil, fmt.Errorf("%s is at round %d of its review, which asked changes: the operator decides it, and no review runs before the "+ + "answer. Nothing was closed; close it after the operator's answer", key, w.Ask.Round) + } want := closeLinkKind[a.To] var added []Link var refusals []Refusal @@ -111,7 +115,11 @@ func (k *KB) Close(caller string, a CloseAsk) (*Closed, error) { name = fmt.Sprintf("%s (%d)", closeSection[a.To], n) } r.Sections = append(r.Sections, Section{Name: name, Text: note}) - r.Reviews, r.Ask, r.Retracting = nil, nil, nil + r.Reviews, r.CountFrom, r.Ask, r.Retracting = nil, 0, nil, nil + if e.Pending != nil { + // The closing keeps the round count of the version it replaces (ADR 0665 §1). + r.Reviews, r.CountFrom = e.Pending.Reviews, e.Pending.CountFrom + } r.NeedsOperator, r.Brief = false, nil r.Author = caller r.Writers = append([]string{}, cur.Writers...) @@ -243,7 +251,8 @@ func ClosingQuestion(r *Record, from string, linked []*Record) string { b.WriteString("the note says why it is parked as an idea. A closing to an idea takes no links: never ask for one.") } b.WriteString(" Answer changes otherwise, with findings, each naming the note or the link and saying what to change. " + - "Do not ask for changes of taste or wording that leave the meaning as it is.\n\n") + "Do not ask for changes of taste or wording that leave the meaning as it is.\n") + b.WriteString(roundText(r) + "\n") fmt.Fprintf(&b, "The note, section %q:\n\n%s\n\n", c.Section, c.Note) if c.To == StatusGraduated { b.WriteString("The became links the effort carries, those it carried before and those the closing adds:\n") diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/review.go b/modules/mesh-knowledge/cmd/mesh-knowledge/review.go index bd61bc25b..12aecb349 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/review.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/review.go @@ -160,7 +160,7 @@ func ReviewQuestion(r *Record, linked []*Record) string { // graduated or abandoned, which it takes once accepted) are shown apart, each named for what it is (hq issue 492). func reviewed(r *Record) *Record { c := r.Clone() - c.Reviews, c.Ask, c.Judging, c.Drafting, c.Writers, c.Author, c.Parts = nil, nil, nil, nil, nil, "", nil + c.Reviews, c.Ask, c.Judging, c.Drafting, c.Writers, c.Author, c.Parts, c.CountFrom = nil, nil, nil, nil, nil, "", nil, 0 c.Extra = addExtra(c.Extra, "state in the knowledge base", c.Status+" (every record under review is; it is not the record's own status)") if c.Kind == KindResearch && c.Effort != "" { c.Extra = addExtra(c.Extra, "research status", c.Effort) diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/review_round_test.go b/modules/mesh-knowledge/cmd/mesh-knowledge/review_round_test.go index b98db7a92..c747fdb51 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/review_round_test.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/review_round_test.go @@ -142,3 +142,47 @@ func TestTheRoundCountGoesOnAcrossRevisionsOfAPendingInsight(t *testing.T) { t.Fatalf("the third revision of the insight: %.300s", q) } } + +// A pending version of an active effort: revisions and a closing keep the count (ADR 0665 §1), `get` says the round and +// the ask after round 5, and a close is refused while that ask waits (§3). +func TestARoundCountSurvivesAClosingAndAClosingWaitsForTheOperatorsAnswer(t *testing.T) { + w := imported(t, newMemStore()) + n := w.effort(t, StatusActive) + revise := func(i int, answer string) string { + next := effortAsk(n, StatusActive, Section{Name: "00-overview.md", Text: fmt.Sprintf("What a roadmap entry would be, revision %d.", i)}) + if _, err := w.k.Propose("author@g14", next); err != nil { + t.Fatal(err) + } + w.judge.review, w.judge.reviews = answer, nil + w.reviewAll(t) + return strings.Join(w.judge.reviews, "\n") + } + for i := 1; i <= 2; i++ { + revise(i, changes(i)) + } + if g, err := w.k.Get(GetAsk{Kind: KindResearch, Number: n}); err != nil || g.Round != 3 { + t.Fatalf("get after two rounds: %+v, %v", g, err) + } + if _, err := w.k.Close("lead@shanks", CloseAsk{Number: n, To: StatusIdea, Note: "Parked: nothing carries it this month."}); err != nil { + t.Fatal(err) + } + w.judge.review, w.judge.reviews = "", nil + w.reviewAll(t) + if q := strings.Join(w.judge.reviews, "\n"); !strings.Contains(q, "This is round 3 of its review") || !strings.Contains(q, "finding of review 2") { + t.Fatalf("the closing's question: %.500s", q) + } + + m := w.effort(t, StatusActive) + n = m + for i := 1; i <= 5; i++ { + revise(i, changes(i)) + } + g, err := w.k.Get(GetAsk{Kind: KindResearch, Number: n}) + if err != nil || !strings.Contains(g.Waiting, "after round 5 asked changes") { + t.Fatalf("get after round 5: %+v, %v", g.Waiting, err) + } + if _, err := w.k.Close("lead@shanks", CloseAsk{Number: n, To: StatusIdea, Note: "Parked: nothing carries it this month."}); err == nil || + !strings.Contains(err.Error(), "the operator decides") { + t.Fatalf("a close before the answer: %v", err) + } +}