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
This commit is contained in:
2026-11-09 23:20:22 +01:00
parent 62644aa012
commit 09ea4a40bf
4 changed files with 72 additions and 7 deletions
@@ -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 {
@@ -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")
@@ -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)
@@ -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)
}
}