A graduated closing is judged on every became link the effort carries, carried ones too, each with its record's title and status (issue 784, ADR 0630 §2)
mesh/check being carried to 46505d42d8fb on 8acad77d33f9 | from d2f326dff40e
mesh/delivery merged: source.main moved to novox/mesh-catalog@8acad77d33f9

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
2026-11-04 16:00:13 +01:00
parent a198a959fe
commit 46505d42d8
3 changed files with 86 additions and 39 deletions
@@ -225,8 +225,8 @@ func (r *Record) hasSectionNamed(name string) bool {
}
// ClosingQuestion is the review's question for a closing (ADR 0630 §2): the record's number and title, the status it
// is served at and the move, the closing's section and the links it adds with the linked records' titles and
// statuses, and never the effort's old sections. linked are the records the added links name.
// is served at and the move, the closing's section and the links it judges (ClosingLinks) with the linked records' titles and
// statuses, and never the effort's old sections. linked are the records ClosingLinks name.
func ClosingQuestion(r *Record, from string, linked []*Record) string {
c := r.Pending.Closing
var b strings.Builder
@@ -244,11 +244,17 @@ func ClosingQuestion(r *Record, from string, linked []*Record) string {
}
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")
fmt.Fprintf(&b, "The note, section %q:\n\n%s\n\nThe links the closing adds:\n", c.Section, c.Note)
if len(c.Added) == 0 {
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")
} else {
b.WriteString("The links the closing adds:\n")
}
shown := ClosingLinks(r)
if len(shown) == 0 {
b.WriteString("- none\n")
}
for _, l := range c.Added {
for _, l := range shown {
fmt.Fprintf(&b, "- %s\n", l)
}
if len(linked) > 0 {
@@ -259,3 +265,20 @@ func ClosingQuestion(r *Record, from string, linked []*Record) string {
}
return b.String()
}
// ClosingLinks are the links a closing's review judges (ADR 0630 §2): for a closing to graduated every became link
// the record carries, carried before or added, since each must name an accepted record; otherwise the links the
// closing adds.
func ClosingLinks(r *Record) []Link {
c := r.Pending.Closing
if c.To != StatusGraduated {
return c.Added
}
var out []Link
for _, l := range r.Links {
if l.Kind == LinkBecame {
out = append(out, l)
}
}
return out
}
@@ -2,6 +2,7 @@ package main
import (
"context"
"fmt"
"strings"
"testing"
"time"
@@ -171,14 +172,10 @@ func TestAGraduatingCloseReadsTheBecameLinksTheEffortCarries(t *testing.T) {
}
// ADR 0630 §2: the review of a closing sees the closing alone, so a record over the judgement's limit is reviewable;
// an idea's closing takes no links and the question says so (§1).
// an idea's closing takes no links and the question says so (§1); a graduated closing is judged on every became link
// the effort carries, the carried ones too, each with its record's title and status.
func TestAClosingOfARecordOverTheJudgementLimitIsReviewed(t *testing.T) {
w := imported(t, newMemStore())
n := w.effort(t, StatusActive)
key := Key{KindResearch, n}
e := mustGet(t, w.k.Store, key)
big := e.Current.Clone()
big.Sections = append(big.Sections, Section{Name: "01-findings.md", Text: "OLD-FINDING " + strings.Repeat("The effort measured one more thing. ", 2200)})
// An old link to a decision with long clauses: the old question carried every clause of every linked record.
var long []Clause
for i := 1; i <= 20; i++ {
@@ -186,27 +183,49 @@ func TestAClosingOfARecordOverTheJudgementLimitIsReviewed(t *testing.T) {
}
seedRecord(t, w.k.Store, &Record{Kind: KindDecision, Number: 9000, Title: "a long decision", Status: StatusAccepted, Topic: "the mesh",
Clauses: long}, time.Now())
big.Links = append(big.Links, Link{Kind: LinkTouches, To: Ref{Kind: KindDecision, Number: 9000}, Source: SourceWritten})
if len(ReviewQuestion(big, nil)) <= MostReviewQuestion {
t.Fatal("the seeded record is not over the judgement's limit")
}
if err := w.k.Store.Save(context.Background(), Version{By: "import@test", At: time.Now(), Why: "seeded", Record: big}, true, false, e.Revision); err != nil {
t.Fatal(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.reviews = nil
said := w.reviewAll(t)
if len(w.judge.reviews) != 1 {
t.Fatalf("not judged: %v", said)
}
q := w.judge.reviews[0]
if strings.Contains(q, "OLD-FINDING") || strings.Contains(q, "A long clause") || len(q) > MostReviewQuestion || !strings.Contains(q, "takes no links") ||
!strings.Contains(q, "served as active") {
t.Errorf("the question is not the closing alone (%d bytes):\n%.700s", len(q), q)
}
if got := mustGet(t, w.k.Store, key).Current.Status; got != StatusIdea {
t.Errorf("after the review: %s", got)
adr := mustGet(t, w.k.Store, Key{KindDecision, 290}).Current
becameADR := Link{Kind: LinkBecame, To: Ref{Kind: KindDecision, Number: 290}, Source: SourceWritten}
for _, c := range []struct {
name, to string
carried []Link
want []string
}{
{"to an idea", StatusIdea, nil, []string{"takes no links", "The links the closing adds:\n- none"}},
{"to graduated, its became carried", StatusGraduated, []Link{becameADR, becameADR},
[]string{"- became ADR 0290\n", fmt.Sprintf("- ADR 0290. %s (%s)\n", clip(adr.Title, 200), StatusAccepted)}},
} {
n := w.effort(t, StatusActive)
key := Key{KindResearch, n}
e := mustGet(t, w.k.Store, key)
big := e.Current.Clone()
big.Sections = append(big.Sections, Section{Name: "01-findings.md", Text: "OLD-FINDING " + strings.Repeat("The effort measured one more thing. ", 2200)})
big.Links = append(big.Links, Link{Kind: LinkTouches, To: Ref{Kind: KindDecision, Number: 9000}, Source: SourceWritten})
big.Links = append(big.Links, c.carried...)
if len(ReviewQuestion(big, nil)) <= MostReviewQuestion {
t.Fatalf("%s: the seeded record is not over the judgement's limit", c.name)
}
if err := w.k.Store.Save(context.Background(), Version{By: "import@test", At: time.Now(), Why: "seeded", Record: big}, true, false, e.Revision); err != nil {
t.Fatal(err)
}
if _, err := w.k.Close("lead@shanks", CloseAsk{Number: n, To: c.to, Note: closeNote("0290")}); err != nil {
t.Fatalf("%s: %v", c.name, err)
}
w.judge.reviews = nil
said := w.reviewAll(t)
if len(w.judge.reviews) != 1 {
t.Fatalf("%s: not judged: %v", c.name, said)
}
q := w.judge.reviews[0]
bad := strings.Contains(q, "OLD-FINDING") || strings.Contains(q, "A long clause") || strings.Contains(q, "a long decision") ||
len(q) > MostReviewQuestion || !strings.Contains(q, "served as active") || strings.Count(q, "ADR 0290. ") > 1
for _, s := range c.want {
bad = bad || !strings.Contains(q, s)
}
if bad {
t.Errorf("%s: the question is not the closing alone (%d bytes):\n%.1500s", c.name, len(q), q)
}
if got := mustGet(t, w.k.Store, key).Current.Status; got != c.to {
t.Errorf("%s: after the review: %s", c.name, got)
}
}
}
@@ -432,16 +432,21 @@ func (k *KB) reviewOne(key Key) (string, error) {
var question string
if w.Pending != nil && w.Pending.Closing != nil {
// A closing is judged on its closing alone (ADR 0630 §2): never the old sections, and of the records it
// links only the ones the closing adds.
var added []*Record
for _, l := range w.Pending.Closing.Added {
// links only the ones its judged links name (ClosingLinks), each once.
var named []*Record
took := map[Key]bool{}
for _, l := range ClosingLinks(w) {
to, ok := l.To.Record()
if !ok || took[to] {
continue
}
for _, t := range linked {
if to, ok := l.To.Record(); ok && t.Key() == to {
added = append(added, t)
if t.Key() == to {
named, took[to] = append(named, t), true
}
}
}
question = ClosingQuestion(w, e.Current.Status, added)
question = ClosingQuestion(w, e.Current.Status, named)
} else {
question = ReviewQuestion(w, linked)
}