diff --git a/modules/mesh-knowledge/README.md b/modules/mesh-knowledge/README.md index bb749dd0..d3576f92 100644 --- a/modules/mesh-knowledge/README.md +++ b/modules/mesh-knowledge/README.md @@ -96,7 +96,9 @@ as text. `get` serves each, and any other field a record of hq wrote under `fiel 3. Revise it by proposing again; every version is kept. 4. The knowledge base reviews every version proposed itself. It asks the agent module's judge (`claude-code.claude_code_judge` on this node) one question, in a separate call with no tools, that holds only - the record, the records it links and the rules, and answers `approve` or `changes` with findings. The background + the record, the records it links and the rules, and answers `approve` or `changes` with findings. The rules it sees + name every kind of link `propose` accepts, from the one list `propose` holds a link to, and the record it sees + shows its state in the knowledge base apart from a research effort's own status (hq issue 492). The background work asks it at once, before the next step of any drafting, one judgement at a time. - On `changes` the record stays proposed, the findings kept with the version they judged (`get` shows them). Its author revises it by proposing it again, and that version is reviewed again. diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/record.go b/modules/mesh-knowledge/cmd/mesh-knowledge/record.go index 53003fbe..24d91e50 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/record.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/record.go @@ -73,6 +73,18 @@ const ( // LinkKinds are the kinds a link may be. var LinkKinds = []string{LinkSupersedes, LinkChanges, LinkExtends, LinkBecame, LinkCites, LinkRestsOn, LinkTouches} +// LinkMeanings says what each kind of link means, as the knowledge base's own review is told (hq issue 492): the +// rules it sees list LinkKinds, which propose holds a link to, so the two cannot drift. +var LinkMeanings = map[string]string{ + LinkSupersedes: "a record it replaces whole", + LinkChanges: "a clause of another decision it replaces part of, named as 0276 ยง4", + LinkExtends: "a record of its kind it builds on", + LinkBecame: "from a research effort, the decision record or design it became", + LinkCites: "any record, issue or design it names", + LinkRestsOn: "from a decision, a decision it stands on, which may not be superseded under it", + LinkTouches: "from a research effort, a record, issue or design its effort bears on", +} + // What a link may point at besides a record of this knowledge base: an issue in the tracker, a design in hq. const ( TargetIssue = "issue" diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/review.go b/modules/mesh-knowledge/cmd/mesh-knowledge/review.go index c864f397..3b63fe73 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/review.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/review.go @@ -87,6 +87,10 @@ func ReviewQuestion(r *Record, linked []*Record) string { for _, rule := range PerRecordRules { fmt.Fprintf(&b, "- %s\n", rule.Holds) } + b.WriteString("\nThe kinds of link a record may have, each going out from the record or one of its clauses:\n") + for _, kind := range LinkKinds { + fmt.Fprintf(&b, "- `%s`: %s\n", kind, LinkMeanings[kind]) + } b.WriteString("\nThe record:\n\n") b.WriteString(Render(reviewed(r))) if len(linked) > 0 { @@ -112,9 +116,16 @@ func ReviewQuestion(r *Record, linked []*Record) string { } // reviewed is the record as its judgement sees it: what it says, without where its review stands. +// Its state in the knowledge base (proposed, while it is reviewed) and a research effort's own status (active, +// 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 = nil, nil, nil, nil, nil, "" + 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) + } + c.Status, c.Effort = "", "" return c } diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/review_sees_test.go b/modules/mesh-knowledge/cmd/mesh-knowledge/review_sees_test.go new file mode 100644 index 00000000..75d7c60f --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/review_sees_test.go @@ -0,0 +1,60 @@ +package main + +import ( + "strings" + "testing" +) + +// hq issue 492: the first live review (research 046) sent a valid proposal back because the rules it was shown +// named no link kind propose accepts beyond its words, and the record it was shown carried the record's state +// `proposed` where a research effort's status belongs. + +// Every link kind propose accepts is named in the rules the judgement sees: one list, so the two cannot drift. +func TestTheReviewIsShownEveryLinkKindProposeAccepts(t *testing.T) { + q := ReviewQuestion(&Record{Kind: KindDecision, Number: 300, Title: "x", Status: StatusProposed}, nil) + for _, kind := range LinkKinds { + if !strings.Contains(q, "- `"+kind+"`: ") { + t.Errorf("the review is not shown the link kind %s", kind) + } + } + for _, kind := range LinkKinds { + if strings.TrimSpace(LinkMeanings[kind]) == "" { + t.Errorf("the link kind %s says nothing of what it means", kind) + } + } + if len(LinkMeanings) != len(LinkKinds) { + t.Errorf("%d meanings for %d link kinds", len(LinkMeanings), len(LinkKinds)) + } +} + +// A research effort's own status is shown apart from its state in the knowledge base. +func TestTheReviewIsShownAResearchEffortsStatusApartFromItsState(t *testing.T) { + w := imported(t, newMemStore()) + r, err := w.k.ReserveNumber("author@g14", KindResearch, "a vendor-neutral inference layer") + if err != nil { + t.Fatal(err) + } + if _, err := w.k.Propose("author@g14", ProposeAsk{Kind: KindResearch, Number: r.Number, Title: "A vendor-neutral inference layer", + Date: "2026-10-11", Deciders: []string{"jochen"}, Status: StatusActive, + Sections: []Section{{Name: "00-overview.md", Text: "What an inference layer the mesh controls would be."}}, + Links: []LinkAsk{{Kind: LinkTouches, To: "0290"}, {Kind: LinkTouches, To: "issue 365"}}}); err != nil { + t.Fatal(err) + } + w.reviewAll(t) + if len(w.judge.reviews) != 1 { + t.Fatalf("%d reviews", len(w.judge.reviews)) + } + q := w.judge.reviews[0] + record := q[strings.Index(q, "The record:"):] + if !strings.Contains(record, "research status: active") || !strings.Contains(record, "state in the knowledge base: proposed") { + t.Fatalf("the record as the review sees it:\n%.600s", record) + } + for _, never := range []string{"\nstatus: proposed", "\neffort: "} { + if strings.Contains(record, never) { + t.Errorf("the review is shown %q", strings.TrimSpace(never)) + } + } + if !strings.Contains(q, "- `touches`: ") { + t.Error("the review is not told what touches is") + } +}