From b387cddc7c6357c5d4417c07575618975373cb71 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 17 Oct 2026 23:01:22 +0200 Subject: [PATCH] Let the knowledge base answer its whole-set findings, whole or by record (issue 1069) --- modules/mesh-knowledge/README.md | 1 + .../cmd/mesh-knowledge/findings.go | 55 +++++++++++++++++++ .../cmd/mesh-knowledge/findings_test.go | 37 +++++++++++++ .../mesh-knowledge/cmd/mesh-knowledge/main.go | 18 +++++- .../cmd/mesh-knowledge/manifest_test.go | 2 +- modules/mesh-knowledge/module.json | 2 + 6 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 modules/mesh-knowledge/cmd/mesh-knowledge/findings.go create mode 100644 modules/mesh-knowledge/cmd/mesh-knowledge/findings_test.go diff --git a/modules/mesh-knowledge/README.md b/modules/mesh-knowledge/README.md index 177adea3..163f1c70 100644 --- a/modules/mesh-knowledge/README.md +++ b/modules/mesh-knowledge/README.md @@ -18,6 +18,7 @@ operator, it is accepted only on the operator's Approve, given on a channel that | `ask` | `question` (at most 500 characters), `bound` | the records and clauses bearing on the question, ranked; each clause says whether it holds, what changed it, or that it is only drafted; each record says what changed it since and how to read its reasoning | | `get` | `number`, `kind`, `reasoning`, `section`, `version`, `bound` | one record: clauses, links both ways, what changed it since, versions, reviews, the operator's ask; its reasoning only when asked | | `list` | `kind`, `status`, `topic`, `flag` (`research-done` or `proposed-old`), `before` (a number, the cursor), `limit` | short rows, newest first, number by number: number, kind, title, status, topic, date, versions, and whether a proposal is pending; `next`, the `before` that reads on, when more match | +| `findings` | `number` (optional) | the whole-set findings kept — each a rule's failure, the records it names, what it says, and when; with `number`, only the findings naming that record | | `holds` | `topic` or `number`, `bound` | the clauses in force for a topic of the reading order or for one record; never a drafted clause | | `links` | `number`, `kind` | the record's typed links, out and in | | `history` | `number`, `kind`, `from`, `to`, `bound` | every version; with `from` and `to`, the difference between the two, line by line | diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/findings.go b/modules/mesh-knowledge/cmd/mesh-knowledge/findings.go new file mode 100644 index 00000000..8c08b4be --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/findings.go @@ -0,0 +1,55 @@ +package main + +// findings (novox/hq issue 1069): the whole-set findings kept (judgeSet, kb.go), or only those naming one +// record. + +import ( + "context" + "fmt" + "strings" + "time" +) + +// FindingRow is one whole-set finding as findings answers it. +type FindingRow struct { + Rule string `json:"rule"` + Records []string `json:"records"` + Says string `json:"says"` + At time.Time `json:"at"` +} + +// Findings is the answer to findings. +type Findings struct { + Rows []FindingRow `json:"rows"` + Count int `json:"count"` +} + +// Findings is the whole-set findings kept, or only those naming one record, given as a record names it ("276", +// "ADR 276", "research 045") (novox/hq issue 1069). +func (k *KB) Findings(number string) (*Findings, error) { + named := "" + if strings.TrimSpace(number) != "" { + ref, err := ParseRef(number) + if err != nil { + return nil, err + } + key, ok := ref.Record() + if !ok { + return nil, fmt.Errorf("number is a record's number, such as 276 or research 045, not %q", number) + } + named = key.String() + } + all, err := k.Store.Findings(context.Background()) + if err != nil { + return nil, err + } + out := &Findings{Rows: []FindingRow{}} + for _, f := range all { + if named != "" && !contains(f.Records, named) { + continue + } + out.Rows = append(out.Rows, FindingRow{Rule: f.Rule, Records: f.Records, Says: f.Says, At: f.At}) + } + out.Count = len(out.Rows) + return out, nil +} diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/findings_test.go b/modules/mesh-knowledge/cmd/mesh-knowledge/findings_test.go new file mode 100644 index 00000000..4f96eb8f --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/findings_test.go @@ -0,0 +1,37 @@ +package main + +import ( + "context" + "testing" + "time" +) + +// findings answers the whole-set findings kept, or only those naming one record (issue 1069). +func TestFindingsAnswersKeptFindingsOrOnlyTheOneNamingARecord(t *testing.T) { + s := newMemStore() + at := time.Date(2026, 10, 17, 9, 0, 0, 0, time.UTC) + seeded := []Finding{ + {Rule: "cited", Records: []string{"ADR 0276"}, Says: "ADR 0276 is accepted and nothing cites it", At: at}, + {Rule: "supersession", Records: []string{"ADR 0100", "ADR 0101"}, Says: "ADR 0100 supersedes ADR 0101, which is accepted", At: at}, + } + if err := s.SetFindings(context.Background(), seeded); err != nil { + t.Fatal(err) + } + k := &KB{Store: s} + + all, err := k.Findings("") + if err != nil { + t.Fatal(err) + } + if all.Count != 2 || len(all.Rows) != 2 { + t.Fatalf("want both findings, got %+v", all) + } + + one, err := k.Findings("276") + if err != nil { + t.Fatal(err) + } + if one.Count != 1 || len(one.Rows) != 1 || one.Rows[0].Rule != "cited" { + t.Fatalf("want only the finding naming 0276, got %+v", one) + } +} diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/main.go b/modules/mesh-knowledge/cmd/mesh-knowledge/main.go index 9de56e74..288a894c 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/main.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/main.go @@ -37,7 +37,7 @@ import ( const Seat = "knowledge-base" // Verbs are the seat's verbs, as the manifest declares and claims them (ADR 0290 rule 6). -var Verbs = []string{"ask", "get", "list", "holds", "links", "history", "reserve", "propose", "import", "return"} +var Verbs = []string{"ask", "get", "list", "findings", "holds", "links", "history", "reserve", "propose", "import", "return"} // HealthTool is the module's own tool the node-engine asks how it is. const HealthTool = "knowledge_health" @@ -853,6 +853,22 @@ func Tools(k *KB, caller func(map[string]any) string) []stdio.Tool { } return k.List(ask) }}, + {Name: Seat + ".findings", + Description: "The whole-set findings kept: every failure of the rules that judge the whole record set, each saying which " + + "records it names and what the rule says of them — a record accepted and cited by nothing, a supersession or a " + + "change no accepted record makes. With number, only the findings that name that record.", + Input: schema(nil, map[string]any{"number": text("only the findings naming this record, such as 276 or research 045")}), + Run: func(m map[string]any) (any, error) { + a, err := readArgs("findings", m, "number") + if err != nil { + return nil, err + } + number, err := a.str("number") + if err != nil { + return nil, err + } + return k.Findings(number) + }}, {Name: Seat + ".holds", Description: "The clauses in force now, for a topic of the reading order or for one record, each with the record it comes " + "from; a clause changed in part says by what. Never a clause only drafted from prose: those are counted and not served. " + diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/manifest_test.go b/modules/mesh-knowledge/cmd/mesh-knowledge/manifest_test.go index cfab7061..9945acb0 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/manifest_test.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/manifest_test.go @@ -62,7 +62,7 @@ func TestItDeclaresAndHoldsTheKnowledgeBaseSeatOnceForTheMesh(t *testing.T) { if m.Module != Module || !reflect.DeepEqual(m.Seats, want) || !reflect.DeepEqual(m.Claims, want) { t.Fatalf("module %q, seats %+v, claims %+v", m.Module, m.Seats, m.Claims) } - if !reflect.DeepEqual(Verbs, []string{"ask", "get", "list", "holds", "links", "history", "reserve", "propose", "import", "return"}) { + if !reflect.DeepEqual(Verbs, []string{"ask", "get", "list", "findings", "holds", "links", "history", "reserve", "propose", "import", "return"}) { t.Fatalf("the verbs are not ADR 0290 rule 6's: %v", Verbs) } if !reflect.DeepEqual(m.Emits, Events) { diff --git a/modules/mesh-knowledge/module.json b/modules/mesh-knowledge/module.json index 0b30dada..33c8c71f 100644 --- a/modules/mesh-knowledge/module.json +++ b/modules/mesh-knowledge/module.json @@ -24,6 +24,7 @@ "ask", "get", "list", + "findings", "holds", "links", "history", @@ -42,6 +43,7 @@ "ask", "get", "list", + "findings", "holds", "links", "history",