Let the knowledge base answer its whole-set findings, whole or by record (issue 1069)

This commit is contained in:
jochen
2026-10-17 23:01:22 +02:00
parent 1cfbcf8e30
commit b387cddc7c
6 changed files with 113 additions and 2 deletions
@@ -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
}
@@ -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)
}
}
@@ -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. " +
@@ -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) {