Let the knowledge base answer its whole-set findings, whole or by record (issue 1069)
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"ask",
|
||||
"get",
|
||||
"list",
|
||||
"findings",
|
||||
"holds",
|
||||
"links",
|
||||
"history",
|
||||
@@ -42,6 +43,7 @@
|
||||
"ask",
|
||||
"get",
|
||||
"list",
|
||||
"findings",
|
||||
"holds",
|
||||
"links",
|
||||
"history",
|
||||
|
||||
Reference in New Issue
Block a user