close: read the became links the effort already carries, check the record size, drop empty table cases (issue 784, ADR 0630)
mesh/check being carried to e9ba05bd9a3c on d2f326dff40e | from 79c2fc7fe1d5
mesh/delivery merged: source.main moved to novox/mesh-catalog@d2f326dff40e

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
2026-11-04 15:15:34 +01:00
parent 8f5667cca5
commit e9ba05bd9a
3 changed files with 57 additions and 12 deletions
@@ -2,6 +2,7 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
@@ -131,7 +132,12 @@ func (k *KB) Close(caller string, a CloseAsk) (*Closed, error) {
look := k.lookup(ctx)
probe := &Record{Kind: KindResearch, Number: a.Number, Title: "the closing", Date: day, Effort: a.To, Status: StatusProposed,
Links: added, Sections: []Section{{Name: "00-overview.md", Text: note}, {Name: name, Text: note}}}
closing := JudgeRecordOn(probe, e, look, ref, day)
var closing []Refusal
for _, x := range JudgeRecordOn(probe, e, look, ref, day) {
if x.Rule != "became" { // the probe holds only the added links; the whole record's became is checked below
closing = append(closing, x)
}
}
if a.To == StatusGraduated {
became := false
for _, l := range r.Links {
@@ -141,7 +147,10 @@ func (k *KB) Close(caller string, a CloseAsk) (*Closed, error) {
refusals = append(refusals, Refusal{Rule: "became", Says: "a graduated research effort names what it became, as a became link"})
}
}
for _, l := range added {
for _, l := range r.Links {
if l.Kind != LinkBecame {
continue
}
if target, ok := l.To.Record(); ok && a.To == StatusGraduated && target.Kind == KindDecision {
if t := look(target); t != nil && t.Status != StatusAccepted {
refusals = append(refusals, Refusal{Rule: "link-target", Says: fmt.Sprintf("a graduated effort's became names accepted records: %s is %s", target, t.Status)})
@@ -149,6 +158,9 @@ func (k *KB) Close(caller string, a CloseAsk) (*Closed, error) {
}
}
refusals = append(refusals, closing...)
if raw, _ := json.Marshal(r); len(raw) > MostRecordBytes {
refusals = append(refusals, Refusal{Rule: "size", Says: fmt.Sprintf("the record is %d bytes; a record holds at most %d", len(raw), MostRecordBytes)})
}
if len(refusals) > 0 {
lines := make([]string, len(refusals))
for i, x := range refusals {
@@ -1,6 +1,7 @@
package main
import (
"context"
"strings"
"testing"
"time"
@@ -59,12 +60,9 @@ func TestACloseAddsOneSectionAndJudgesOnlyTheClosing(t *testing.T) {
}
}
// ADR 0630 §4: the moves it allows, a refused move naming the allowed ones; the active limit never fires.
// ADR 0630 §4: the moves it allows, a refused move naming the allowed ones.
func TestCloseAllowsOnlyItsMoves(t *testing.T) {
w := imported(t, newMemStore())
for i := 0; i < 3; i++ {
w.effort(t, StatusActive) // the limit is reached: close must not meet it
}
becameTo := []LinkAsk{{Kind: LinkBecame, To: "0290"}}
citesTo := []LinkAsk{{Kind: LinkCites, To: "0290"}}
for _, c := range []struct {
@@ -75,20 +73,17 @@ func TestCloseAllowsOnlyItsMoves(t *testing.T) {
note string
wantErr string
}{
{"an idea to abandoned, at the limit", StatusIdea, StatusAbandoned, citesTo, closeNote("0290"), ""},
{"an idea to abandoned", StatusIdea, StatusAbandoned, citesTo, closeNote("0290"), ""},
{"an idea to graduated", StatusIdea, StatusGraduated, becameTo, closeNote("0290"), "from idea it allows abandoned"},
{"an idea to an idea", StatusIdea, StatusIdea, nil, closeNote("0290"), "from idea it allows abandoned"},
{"an active to an idea, at the limit", StatusActive, StatusIdea, nil, closeNote("0290"), ""},
{"an active to an idea", StatusActive, StatusIdea, nil, closeNote("0290"), ""},
{"graduated needs a became", StatusActive, StatusGraduated, nil, closeNote("0290"), "became"},
{"a wrong link kind", StatusActive, StatusAbandoned, becameTo, closeNote("0290"), "cites"},
{"no note", StatusActive, StatusIdea, nil, " ", "note"},
} {
n := w.effort(t, StatusIdea)
if c.from == StatusActive {
// make room under the limit for the effort under test, then take it up
w.k.Settings = func() (Settings, error) { return Settings{ResearchActiveAtMost: 9}, nil }
w.accepted(t, "author@g14", effortAsk(n, StatusActive, Section{Name: "00-overview.md", Text: "Taken up."}))
w.k.Settings = nil
}
_, err := w.k.Close("lead@shanks", CloseAsk{Number: n, To: c.to, Links: c.links, Note: c.note})
if (err == nil) != (c.wantErr == "") || (err != nil && !strings.Contains(err.Error(), c.wantErr)) {
@@ -136,3 +131,41 @@ func TestACloseEndsTheWaitingVersionAndItsAsk(t *testing.T) {
t.Errorf("after the review: %s, the ended version's finding served: %q", got.Status, got.Section("01-findings.md"))
}
}
// ADR 0630 §2: a graduated close counts the became links the effort already carries, and holds every one of them
// to naming an accepted record.
func TestAGraduatingCloseReadsTheBecameLinksTheEffortCarries(t *testing.T) {
w := imported(t, newMemStore())
dec := w.reserved(t, "author@test")
for _, c := range []struct {
name, became, wantErr string
}{
{"an old became to an accepted decision", "0290", ""},
{"an old became to a decision not accepted", Key{KindDecision, dec}.String(), "accepted"},
} {
r, _ := w.k.ReserveNumber("author@g14", KindResearch, "an effort")
a := effortAsk(r.Number, StatusActive, Section{Name: "00-overview.md", Text: "What it was."})
a.Links = append(a.Links, LinkAsk{Kind: LinkBecame, To: "0290"})
w.accepted(t, "author@g14", a)
if c.became != "0290" {
if _, err := w.k.Propose("author@test", valid(dec)); err != nil { // proposed, never reviewed: not accepted
t.Fatal(err)
}
// the old link goes to a record not accepted: written into the held record, as an import would
e := mustGet(t, w.k.Store, Key{KindResearch, r.Number})
cur := e.Current.Clone()
for i := range cur.Links {
if cur.Links[i].Kind == LinkBecame {
cur.Links[i].To = Ref{Kind: KindDecision, Number: dec}
}
}
if err := w.k.Store.Save(context.Background(), Version{By: "import@test", At: time.Now(), Why: "seeded", Record: cur}, true, false, e.Revision); err != nil {
t.Fatal(err)
}
}
_, err := w.k.Close("lead@shanks", CloseAsk{Number: r.Number, To: StatusGraduated, Note: closeNote("0290")})
if (err == nil) != (c.wantErr == "") || (err != nil && !strings.Contains(err.Error(), c.wantErr)) {
t.Errorf("%s: %v, want %q", c.name, err, c.wantErr)
}
}
}
@@ -1135,7 +1135,7 @@ func Tools(k *KB, caller func(map[string]any) string) []stdio.Tool {
"and one more section (Graduated, Abandoned or Parked as an idea) holding your note of two or three sentences. Only the " +
"closing is judged: the rules run on the added links and the note, and the knowledge base's review sees only the closing, " +
"so an old text's findings never ask changes; they come back once in the answer as notes. A version of the effort still " +
"waiting for its review, and the operator's ask for it, end with the closing, named in the answer. Allowed: an active effort " +
"waiting for its review, whoever its author is, and the operator's ask for it, end with the closing, named in the answer. Allowed: an active effort " +
"to graduated, abandoned or idea; an idea to abandoned; anything else is refused naming the move it allows. An idea is taken up " +
"by propose at active.",
Input: schema([]string{"number", "to", "note"}, map[string]any{"number": integer("the research effort's number"),