mesh-knowledge: ask changes in the review for a decision nothing would cite, so its acceptance never turns the module unhealthy (hq issue 504)

The whole-set rule cited ran only after acceptance: the review approved a
decision with no home and no record linking it, the rule then failed, and
the module's health turned red for a fault its author could only repair
after the fact. hq held the rule at the record's own merge. The review now
judges it ahead, before the judgement: such a decision stays proposed with
a changes finding naming the rule. It is not refused at propose, because
no record can link a number only reserved.
This commit is contained in:
jochen
2026-10-12 13:25:09 +02:00
parent 3e84581927
commit 2051d8b80b
6 changed files with 127 additions and 21 deletions
+5
View File
@@ -135,6 +135,11 @@ as text. `get` serves each, and any other field a record of hq wrote under `fiel
A failure is kept as a finding and makes the module unhealthy, so the mesh raises a condition naming the rule
and the records. The acceptance stands.
`cited` is also judged ahead, in the review: a decision that names no home and that no accepted (or
superseded) record links is not judged and not accepted. Its review, by the knowledge base itself, asks changes
naming the rule (hq issue 504). It is not refused at propose, because no record can link a number only
reserved: a record citing a new one is proposed after it and accepted, and the new one is proposed again.
## The import
`import` reads hq through the records module at the commit its checkout stands at. With `commit`, it refuses a
@@ -436,9 +436,13 @@ func valid(n int) ProposeAsk {
{Name: "Consequences", Text: "Merges land sooner."},
{Name: "How it is checked", Text: "The controller's merge-window tests."},
},
Links: []LinkAsk{{Kind: LinkExtends, To: "0276"}, {Kind: LinkChanges, To: "0276 §1", From: 1}}}
Links: []LinkAsk{{Kind: LinkExtends, To: "0276"}, {Kind: LinkChanges, To: "0276 §1", From: 1}},
Homes: []string{testHome}}
}
// testHome is the hq design valid names as its home: a decision is accepted only cited (novox/hq issue 504).
const testHome = "03-DESIGN/01-to-be/55-the-knowledge-base.md"
// imported is a world whose knowledge base holds the fixture's records.
func imported(t *testing.T, s Store) *world {
t.Helper()
@@ -918,7 +918,8 @@ func Tools(k *KB, caller func(map[string]any) string) []stdio.Tool {
"link to nothing, a clause without a bold lead, a superseded record cited alone, an unknown topic, a retired word. A " +
"proposal is revised by proposing it again; an accepted record changes only by a progressive insight (insight: what " +
"stood before). The knowledge base then reviews each version itself, in a judgement that sees only the record, the " +
"records it links and the rules: on approve it is accepted (a record that needs the operator waits for the operator's " +
"records it links and the rules. A decision that names no home and that no accepted record links is not judged: " +
"its review asks changes for the whole-set rule cited, which its acceptance would break. On approve it is accepted (a record that needs the operator waits for the operator's " +
"Approve), on changes it stays proposed with the findings for its author to revise, and when no judgement can be had " +
"it stays proposed and says so. Nothing proposed is in force.",
Input: schema([]string{"number", "title"}, map[string]any{
@@ -933,7 +934,7 @@ func Tools(k *KB, caller func(map[string]any) string) []stdio.Tool {
"links": map[string]any{"type": "array", "description": "its links: kind, to (\"0276\", \"0276 §4\", \"research 045\", \"issue 365\", a design's path), from (the clause it goes out from)",
"items": map[string]any{"type": "object", "properties": map[string]any{"kind": map[string]any{"type": "string", "enum": LinkKinds},
"to": text("the target"), "from": integer("the clause of this record it goes out from")}, "required": []string{"kind", "to"}}},
"homes": texts("hq documents that cite it, such as a design's path"),
"homes": texts("hq documents that cite it, such as a design's path; a decision needs one unless an accepted record links it"),
"needs-operator": flag("it holds only once the operator approves it"),
"status": map[string]any{"type": "string", "enum": ResearchStatuses, "description": "a research effort's status"},
"insight": text("for an accepted record: a progressive insight, saying what stood before"),
@@ -408,7 +408,10 @@ func (k *KB) reviewOne(key Key) (string, error) {
k.mu.Lock()
e, err := k.Store.Get(ctx, key)
var linked []*Record
var all []*Entry
var allErr error
if err == nil && e != nil {
all, allErr = k.Store.All(ctx)
seen := map[Key]bool{key: true}
for _, l := range e.Working().Links {
to, ok := l.To.Record()
@@ -430,9 +433,20 @@ func (k *KB) reviewOne(key Key) (string, error) {
var verdict string
var findings []string
by := ReviewBy
if len(question) > MostReviewQuestion {
uncited := ""
if allErr == nil {
uncited = UncitedOnAcceptance(w, all)
}
switch {
case allErr != nil:
err = fmt.Errorf("the record set cannot be read to judge whether a record cites it: %v", allErr)
case uncited != "":
// The whole-set rule cited, judged ahead: changes, asked by the knowledge base itself without a judgement,
// so its acceptance never turns the module's health red for it (novox/hq issue 504).
verdict, findings = VerdictChanges, []string{"the rule cited, which judges the whole record set: " + uncited}
case len(question) > MostReviewQuestion:
err = fmt.Errorf("the record is %d bytes as a question, and a judgement takes at most %d: shorten it", len(question), MostReviewQuestion)
} else {
default:
var answer json.RawMessage
var judge string
answer, judge, err = k.Judge(question, reviewSchema, k.judgeModel())
@@ -546,6 +546,36 @@ func JudgeSet(records []*Record) []Finding {
return out
}
// UncitedOnAcceptance is the whole-set rule `cited` judged ahead, on a decision's working version as it would
// stand once accepted, over the records as JudgeSet reads them: "" when it names a home or a record accepted (or
// superseded) links it, else what the rule would say. hq held this rule at the merge of the record's own pull
// request, which carried the design citing it; the knowledge base holds it in the review, before the judgement, so
// an acceptance never leaves a decision cited by nothing (novox/hq issue 504). It is not refused at propose: no
// record can link a number only reserved, so a record that will cite a new one is proposed after it, accepted, and
// the new one proposed again. A citing record changed by a correction after the acceptance is what the rule, run
// after every acceptance, still says. An imported record was held to hq's checks, and its correction is not judged
// here, as JudgeSet does not judge it.
func UncitedOnAcceptance(r *Record, all []*Entry) string {
if r.Kind != KindDecision || len(r.Homes) > 0 || (r.Pending != nil && r.AcceptedAt.IsZero()) {
return ""
}
k := r.Key()
for _, e := range all {
from := e.Current
if from.Key() == k || !(from.Accepted() || from.Status == StatusSuperseded) {
continue
}
for _, l := range from.Links {
if to, ok := l.To.Record(); ok && to == k {
return ""
}
}
}
return fmt.Sprintf("%s would be accepted with nothing citing it: it names no home and no accepted record links it. Name "+
"its home (homes: the hq design or playbook that cites it), or have the record that links it accepted first; then "+
"propose this one again", k)
}
// RuleFrom is the day each dated rule began to hold a record, as hq's checks hold it: a record dated earlier was
// never held to it, and a dated correction of it (a progressive insight, which keeps the record's date) is not.
// words.py held the retired words from 2026-10-07 and the daemons' lines from 2026-10-09; cycle.py the deferrals
@@ -5,32 +5,84 @@ import (
"testing"
)
// to-be 55 §10: "A whole-set rule that fails is loud — the module's test: an acceptance that leaves a decision
// cited by nothing raises the condition." The condition is the mesh's, raised when the module's health answers
// unhealthy; the test reads the health answer the node-engine reads.
func TestAnAcceptanceThatLeavesADecisionCitedByNothingMakesTheModuleUnhealthy(t *testing.T) {
// A decision nothing would cite is not accepted: its review asks changes itself, without a judgement, naming the
// rule cited, and the module stays healthy (novox/hq issue 504). Before, the review approved it, the acceptance
// broke the rule, and the module turned unhealthy for a record its author could only repair after the fact.
func TestADecisionNothingWouldCiteIsNotAccepted(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
w := imported(t, s)
if h := w.k.Health(nil); !h.Healthy {
t.Fatalf("unhealthy after the import of real records: %s", h.Why)
}
n := w.reserved(t, "author@g14")
w.accepted(t, "author@g14", valid(n))
p := valid(n)
p.Homes = nil
if _, err := w.k.Propose("author@g14", p); err != nil {
t.Fatal(err)
}
judged := len(w.judge.reviews)
w.reviewAll(t)
e := mustGet(t, s, Key{KindDecision, n})
if e.Current.Status != StatusProposed {
t.Fatalf("a decision nothing cites was %s by its review", e.Current.Status)
}
rv := e.Current.Reviews
if len(rv) == 0 || rv[len(rv)-1].Verdict != VerdictChanges || !strings.Contains(rv[len(rv)-1].Findings, "the rule cited") ||
rv[len(rv)-1].By != ReviewBy {
t.Fatalf("its review asked no changes for the rule cited: %+v", rv)
}
if len(w.judge.reviews) != judged {
t.Fatal("a judgement was spent on a record the rule cited refuses")
}
if h := w.k.Health(nil); !h.Healthy {
t.Fatalf("unhealthy for a record not accepted: %s", h.Why)
}
// Named with its home, it is accepted, and the module stays healthy.
p.Homes = []string{testHome}
w.accepted(t, "author@g14", p)
if h := w.k.Health(nil); !h.Healthy {
t.Fatalf("unhealthy after a decision with its home was accepted: %s", h.Why)
}
})
}
// A new decision is cited by a record proposed after it: no record can link a number only reserved. Once the
// citing record is accepted, the new one proposed again is accepted. A correction of the citing record that drops
// the link still leaves the decision cited by nothing, and that is loud: to-be 55 §10, "an acceptance that leaves a
// decision cited by nothing raises the condition", the condition the mesh raises when the module's health answers
// unhealthy.
func TestARecordAcceptedCitingANewDecisionLetsItBeAcceptedAndDroppingTheLinkIsLoud(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
w := imported(t, s)
n := w.reserved(t, "author@g14")
a := valid(n)
a.Homes = nil
if _, err := w.k.Propose("author@g14", a); err != nil {
t.Fatal(err)
}
w.reviewAll(t)
m := w.reserved(t, "author@g14")
b := valid(m)
b.Links = append(b.Links, LinkAsk{Kind: LinkCites, To: itoa4(n)})
w.accepted(t, "author@g14", b)
w.accepted(t, "author@g14", a) // its revision: an accepted record links it now
if h := w.k.Health(nil); !h.Healthy {
t.Fatalf("unhealthy with both accepted, one citing the other: %s", h.Why)
}
// A correction of the citing record drops its link: the rule, run after the acceptance, says so.
b.Links = b.Links[:len(b.Links)-1]
b.Insight = "it cited " + itoaKey(n)
w.accepted(t, "author@g14", b)
h := w.k.Health(nil)
if h.Healthy || h.State != "unhealthy" || !strings.Contains(h.Why, "the rule cited") || !strings.Contains(h.Why, itoaKey(n)) {
t.Fatalf("an accepted decision nothing cites left the module %+v", h)
t.Fatalf("a decision left cited by nothing left the module %+v", h)
}
// The acceptance stands: the condition says it, and undoes nothing.
if mustGet(t, s, Key{KindDecision, n}).Current.Status != StatusAccepted {
t.Fatal("the acceptance was undone")
}
// A record that cites it heals it.
m := w.reserved(t, "author@g14")
p := valid(m)
p.Links = append(p.Links, LinkAsk{Kind: LinkCites, To: itoa4(n)})
w.accepted(t, "author@g14", p)
h = w.k.Health(nil)
if strings.Contains(h.Why, itoaKey(n)+" is accepted and nothing cites it") {
k := w.reserved(t, "author@g14")
c := valid(k)
c.Links = append(c.Links, LinkAsk{Kind: LinkCites, To: itoa4(n)})
w.accepted(t, "author@g14", c)
if h := w.k.Health(nil); strings.Contains(h.Why, itoaKey(n)+" is accepted and nothing cites it") {
t.Fatalf("still said after a record cited it: %s", h.Why)
}
})