CAT-K10: the module's tools read each state it keeps on the bus and do by hand what it does itself

knowledge_head, _operations, _section, _draft, _parts, _asked, _fill, _rebuild_index, _review_again, _integrity,
_move and _move_check, beside the health, highest and marks tools; the manifest lists them and a test compares them with the code.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 12:18:11 +01:00
parent 2bf608fc5f
commit e5e3d7dce6
5 changed files with 548 additions and 3 deletions
@@ -106,7 +106,7 @@ func serve(k *KB, status string) {
k.kick = make(chan struct{}, 1)
go hear(k)
go background(k)
if err := stdio.Serve("", append(Tools(k, callerOf), healthTool(status, k.now), highestTool(k), marksTool(k))); err != nil {
if err := stdio.Serve("", append(append(Tools(k, callerOf), healthTool(status, k.now), highestTool(k), marksTool(k)), moduleTools(k, callerOf)...)); err != nil {
logf("%v", err)
os.Exit(1)
}
@@ -126,7 +126,7 @@ func TestItsProcessesAreThePreparationTheActAndTheJudge(t *testing.T) {
}
h, _ := watch["health"].(map[string]any)
if watch == nil || watch["run"].([]any)[1] != "watch" || h["kind"] != "tool" || h["tool"] != HealthTool ||
!reflect.DeepEqual(m.Tools, []string{HealthTool, HighestTool, MarksTool}) {
!reflect.DeepEqual(m.Tools, ModuleTools) {
t.Fatalf("watch %v, tools %v", watch, m.Tools)
}
// The watch's verdict is whether the database serves, so its check names the provision it exercises: while the
@@ -0,0 +1,397 @@
package main
// The module's tools (novox/hq to-be 55 §1.1, the instruction tools-are-how-a-module-is-spoken-to): the one way a person or a
// session reads and steers what the module keeps. Every state has a tool that reads it, every act the module does by itself
// can be done by hand, and every recovery has a tool.
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buscalls"
)
// The module's tools beside the seat's verbs.
const (
HeadTool = "knowledge_head"
OperationsTool = "knowledge_operations"
SectionTool = "knowledge_section"
DraftTool = "knowledge_draft"
PartsTool = "knowledge_parts"
AskedTool = "knowledge_asked"
FillTool = "knowledge_fill"
IndexTool = "knowledge_rebuild_index"
AgainTool = "knowledge_review_again"
IntegrityTool = "knowledge_integrity"
MoveTool = "knowledge_move"
MoveCheckTool = "knowledge_move_check"
)
// ModuleTools are the module's own tools beside the health tool, in the order the manifest lists them.
var ModuleTools = []string{HealthTool, HighestTool, MarksTool, HeadTool, OperationsTool, SectionTool, DraftTool, PartsTool,
AskedTool, FillTool, IndexTool, AgainTool, IntegrityTool, MoveTool, MoveCheckTool}
var errNotOnLedger = errors.New("the records are still in the database: this tool reads the ledger, which serves them after the move")
// ledger is the ledger store the records are served from, or the refusal that they are not.
func (k *KB) ledger() (*ledgerStore, error) {
switch s := k.Store.(type) {
case *ledgerStore:
return s, nil
case *movableStore:
if s.OnLedger() {
return s.led, nil
}
}
return nil, errNotOnLedger
}
func (k *KB) movable() (*movableStore, error) {
if m, ok := k.Store.(*movableStore); ok {
return m, nil
}
return nil, errors.New("this knowledge base has no database to move from")
}
func keyArgs(name string, m map[string]any, more ...string) (Key, args, error) {
a, err := readArgs(name, m, append([]string{"kind", "number"}, more...)...)
if err != nil {
return Key{}, args{}, err
}
k, err := a.key()
return k, a, err
}
func keyProps(more map[string]any) map[string]any {
p := map[string]any{"kind": kindArg(), "number": integer("the record's number")}
for k, v := range more {
p[k] = v
}
return p
}
// moduleTools are the tools beside the seat's verbs that read and steer the records on the bus.
func moduleTools(k *KB, caller func(map[string]any) string) []stdio.Tool {
return []stdio.Tool{
{Name: HeadTool,
Description: "One record's head in the ledger, whole: its fields, clauses, links, its served and proposed versions, its sections as " +
"title, digest and the operation that wrote each, and the sequence of the last operation it holds. Refused while the records are in the database.",
Input: schema([]string{"number"}, keyProps(nil)),
Run: func(m map[string]any) (any, error) {
key, _, err := keyArgs(HeadTool, m)
if err != nil {
return nil, err
}
l, err := k.ledger()
if err != nil {
return nil, err
}
l.mu.Lock()
defer l.mu.Unlock()
h, err := l.fresh(key)
if err != nil || h == nil {
return nil, fmt.Errorf("%s is held of none (%v)", key, err)
}
return h, nil
}},
{Name: OperationsTool,
Description: "A record's history in the ledger, oldest first from a sequence: each operation with its sequence, its name, who it was by " +
"(the module's account and the caller the runtime proved) and when. A section's text is not shown; knowledge_section reads it.",
Input: schema([]string{"number"}, keyProps(map[string]any{"from": integer("the sequence to read from"), "limit": integer("at most this many")})),
Run: func(m map[string]any) (any, error) {
key, a, err := keyArgs(OperationsTool, m, "from", "limit")
if err != nil {
return nil, err
}
from, _ := a.num("from", false)
limit, _ := a.num("limit", false)
l, err := k.ledger()
if err != nil {
return nil, err
}
var page buscalls.HistoryPage
if err := l.pipe.Call(buscalls.LedgerHistory, buscalls.HistoryRequest{Ledger: LedgerName, Instance: InstanceOf(key), From: uint64(from), Limit: limit}, &page); err != nil {
return nil, err
}
for i := range page.Operations {
page.Operations[i].Body = nil // a section's text and a head are read by their own tools
}
return page, nil
}},
{Name: SectionTool,
Description: "One section's text, by the sequence of the write-section operation that wrote it, checked against its digest. Refused when the " +
"operation is no write-section of the record, or its text does not hash to its digest (the history is damaged).",
Input: schema([]string{"number", "sequence"}, keyProps(map[string]any{"sequence": integer("the sequence of the write-section")})),
Run: func(m map[string]any) (any, error) {
key, a, err := keyArgs(SectionTool, m, "sequence")
if err != nil {
return nil, err
}
seq, err := a.num("sequence", true)
if err != nil {
return nil, err
}
l, err := k.ledger()
if err != nil {
return nil, err
}
var page buscalls.HistoryPage
if err := l.pipe.Call(buscalls.LedgerHistory, buscalls.HistoryRequest{Ledger: LedgerName, Instance: InstanceOf(key), From: uint64(seq), Limit: 1}, &page); err != nil {
return nil, err
}
if len(page.Operations) == 0 || page.Operations[0].Sequence != uint64(seq) || page.Operations[0].Operation != OpWriteSection {
return nil, fmt.Errorf("operation %d of %s is no write-section", seq, key)
}
var b struct{ Title, Text, Digest string }
if err := json.Unmarshal(page.Operations[0].Body, &b); err != nil {
return nil, err
}
if SectionDigest(b.Text) != b.Digest {
return nil, fmt.Errorf("the text of operation %d does not hash to its digest %s: the history is damaged", seq, b.Digest)
}
return map[string]any{"title": b.Title, "digest": b.Digest, "text": b.Text, "author": page.Operations[0].Author, "at": page.Operations[0].At}, nil
}},
{Name: DraftTool,
Description: "A record's draft: its author, since when, the version it starts from, its fields and its sections as title, digest and the operation that wrote each. " +
"Answers that there is none when none is open.",
Input: schema([]string{"number"}, keyProps(nil)),
Run: func(m map[string]any) (any, error) {
key, _, err := keyArgs(DraftTool, m)
if err != nil {
return nil, err
}
d, _, err := k.readDraft(key)
if err != nil {
return nil, err
}
if d == nil {
return map[string]any{"record": key.String(), "says": "no draft is open"}, nil
}
return d, nil
}},
{Name: PartsTool,
Description: "The latest verdict of each part of a record's version being worked on or served: the head and each section, the digest it was judged with, " +
"its verdict and findings, who judged and when.",
Input: schema([]string{"number"}, keyProps(nil)),
Run: func(m map[string]any) (any, error) {
key, _, err := keyArgs(PartsTool, m)
if err != nil {
return nil, err
}
e, err := k.Store.Get(context.Background(), key)
if err != nil || e == nil {
return nil, fmt.Errorf("%s is held of none (%v)", key, err)
}
return map[string]any{"record": key.String(), "parts": e.Working().Parts}, nil
}},
{Name: AskedTool,
Description: "The parts asked of the judge and not answered yet: the record, the part and since when. With clear and a key, takes one mark away " +
"(a part whose answer will not come); the record's review asks it again.",
Input: schema(nil, map[string]any{"clear": text("a key to take away")}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs(AskedTool, m, "clear")
if err != nil {
return nil, err
}
kv := k.States.State(askedState)
if c, _ := a.str("clear"); c != "" {
return map[string]any{"cleared": c}, kv.Delete(c)
}
keys, err := kv.Keys()
if err != nil {
return nil, err
}
sort.Strings(keys)
out := []any{}
for _, key := range keys {
if e, err := kv.Get(key); err == nil && e != nil {
var v any
_ = json.Unmarshal(e.Value, &v)
out = append(out, map[string]any{"key": key, "asked": v})
}
}
return map[string]any{"asked": out}, nil
}},
{Name: FillTool,
Description: "How full the module's kept data is against its caps: the stream of the parts' verdicts (messages, bytes and its cap), and the number of keys of each state.",
Input: schema(nil, map[string]any{}),
Run: func(m map[string]any) (any, error) {
if _, err := readArgs(FillTool, m); err != nil {
return nil, err
}
out := map[string]any{}
if l, err := k.ledger(); err == nil {
var f buscalls.StreamFilled
if err := l.pipe.Call(buscalls.StreamFill, buscalls.StreamRequest{Stream: "reviews"}, &f); err != nil {
out["reviews"] = "cannot be read: " + err.Error()
} else {
out["reviews"] = f
}
}
keys := map[string]int{}
for _, s := range []string{draftsState, askedState, findingsState, sentState} {
if ks, err := k.States.State(s).Keys(); err == nil {
keys[s] = len(ks)
}
}
out["keys"] = keys
return out, nil
}},
{Name: IndexTool,
Description: "Build the search index again from the ledger now, and say how it stands: built, when, and how many documents it holds. ask and holds answer " +
"\"index not built\" meanwhile.",
Input: schema(nil, map[string]any{}),
Run: func(m map[string]any) (any, error) {
if _, err := readArgs(IndexTool, m); err != nil {
return nil, err
}
l, err := k.ledger()
if err != nil {
return nil, err
}
if err := l.BuildIndex(context.Background()); err != nil {
return nil, err
}
return l.IndexState(), nil
}},
{Name: AgainTool,
Description: "Ask a record's review again, for a version still proposed: its parts that approved keep their verdicts and the others are judged " +
"again; the review that ran out of tries starts its tries anew. Refused for a record that is not proposed.",
Input: schema([]string{"number"}, keyProps(nil)),
Run: func(m map[string]any) (any, error) {
key, _, err := keyArgs(AgainTool, m)
if err != nil {
return nil, err
}
return k.ReviewAgain(caller(m), key)
}},
{Name: IntegrityTool,
Description: "Check that every section a head or a draft names resolves to a write-section whose text hashes to its digest, for one record or for " +
"all of them, and say each that does not.",
Input: schema(nil, keyProps(nil)),
Run: func(m map[string]any) (any, error) {
a, err := readArgs(IntegrityTool, m, "kind", "number")
if err != nil {
return nil, err
}
l, err := k.ledger()
if err != nil {
return nil, err
}
var keys []Key
if _, has := m["number"]; has {
key, err := a.key()
if err != nil {
return nil, err
}
keys = []Key{key}
} else {
all, err := l.All(context.Background())
if err != nil {
return nil, err
}
for _, e := range all {
keys = append(keys, e.Current.Key())
}
}
bad := []string{}
l.mu.Lock()
defer l.mu.Unlock()
for _, key := range keys {
h, err := l.fresh(key)
if err != nil || h == nil {
bad = append(bad, fmt.Sprintf("%s: %v", key, err))
continue
}
for _, sr := range []*storedRecord{h.Entry.Current, h.Entry.Pending} {
if sr == nil {
continue
}
for _, ref := range sr.Sections {
delete(l.texts, ref.Digest) // read from the history, not from what was held
if _, err := l.text(ref, key); err != nil {
bad = append(bad, fmt.Sprintf("%s: %v", key, err))
}
}
}
}
return map[string]any{"checked": len(keys), "damaged": bad}, nil
}},
{Name: MoveTool,
Description: "Move the records from the database to the ledger: with dry-run, say what a copy would write and write nothing; with commit, refuse writes for " +
"its length, copy every version of every record, check the copy record by record, and set the records to be served from the ledger only if the check is " +
"clean. Run again after a copy it changes nothing. Refused once the records are served from the ledger.",
Input: schema(nil, map[string]any{"dry-run": flag("count and write nothing"), "commit": flag("copy, check and switch")}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs(MoveTool, m, "dry-run", "commit")
if err != nil {
return nil, err
}
var ask MoveAsk
if ask.DryRun, err = a.flag("dry-run"); err != nil {
return nil, err
}
if ask.Commit, err = a.flag("commit"); err != nil {
return nil, err
}
ms, err := k.movable()
if err != nil {
return nil, err
}
return ms.Move(context.Background(), ask, func(s string) { k.logf("[mesh-knowledge] the move: %s", s) })
}},
{Name: MoveCheckTool,
Description: "Compare the database with the ledger record by record now: the versions, the served and the proposed version whole and every section's text. " +
"Answers every difference, at most 50, and where the records are served from; a database that is behind is named.",
Input: schema(nil, map[string]any{}),
Run: func(m map[string]any) (any, error) {
if _, err := readArgs(MoveCheckTool, m); err != nil {
return nil, err
}
ms, err := k.movable()
if err != nil {
return nil, err
}
diffs, err := ms.Check(context.Background())
if err != nil {
return nil, err
}
return map[string]any{"source": ms.Source(), "differences": diffs, "clean": len(diffs) == 0}, nil
}},
}
}
// Review is the record's review asked again.
type ReviewedAgain struct {
Record string `json:"record"`
Says string `json:"says"`
}
// ReviewAgain asks the review of a proposed version again: a failed review starts its tries anew, and a review whose verdict
// was changes is made due, the parts that approved keeping their verdicts.
func (k *KB) ReviewAgain(caller string, key Key) (*ReviewedAgain, error) {
k.mu.Lock()
defer k.mu.Unlock()
ctx := k.ctxFor(caller)
e, err := k.Store.Get(ctx, key)
if err != nil || e == nil {
return nil, fmt.Errorf("%s is held of none (%v)", key, err)
}
w := e.Working().Clone()
if w.Status != StatusProposed {
return nil, fmt.Errorf("%s is %s, not proposed: there is no review to ask again", key, w.Status)
}
if !proven(caller) {
return nil, fmt.Errorf("review-again needs a caller the runtime proved; this call is by %s", caller)
}
w.Judging = &Judging{State: JudgingDue}
if err := k.Store.Save(ctx, Version{By: caller, At: k.now(), Why: "its review is asked again", Record: w, Insight: e.Pending != nil && e.Pending.IsInsight()}, e.Pending == nil, false, e.Revision); err != nil {
return nil, err
}
k.wake()
return &ReviewedAgain{Record: key.String(), Says: "The review of " + key.String() + " is due again; the parts that approved keep their verdicts."}, nil
}
@@ -0,0 +1,136 @@
package main
import (
"encoding/json"
"strings"
"testing"
)
func callTool(t *testing.T, tools []toolRun, name string, args map[string]any) (map[string]any, error) {
t.Helper()
for _, tool := range tools {
if tool.name == name {
out, err := tool.run(args)
if err != nil {
return nil, err
}
raw, _ := json.Marshal(out)
var m map[string]any
_ = json.Unmarshal(raw, &m)
return m, nil
}
}
t.Fatalf("no tool %s", name)
return nil, nil
}
type toolRun struct {
name string
run func(map[string]any) (any, error)
}
// Every state the module keeps on the bus has a tool that reads it, and every act it does by itself one that does it by
// hand (to-be 55 §1.1, AC54): the head, the history, a section, a draft, the parts' verdicts, the parts in flight, the fill,
// the index, the review asked again, the integrity of the sections, the move and its check.
func TestEveryStateTheModuleKeepsHasAToolThatReadsIt(t *testing.T) {
w, f := ledgerWorld(t)
var tools []toolRun
for _, tool := range moduleTools(w.k, callerOf) {
tools = append(tools, toolRun{tool.Name, tool.Run})
}
r, _ := w.k.ReserveNumber("author@g14", KindDecision, "a test record")
p := valid(r.Number)
p.Sections[0].Text = "The merge window of ADR 0276 §1 waits too long."
key := map[string]any{"number": float64(r.Number)}
if _, err := w.k.Open("author@g14", ProposeAsk{Kind: KindDecision, Number: r.Number, Title: p.Title, Topic: p.Topic, Date: p.Date, Deciders: p.Deciders, Clauses: p.Clauses, Links: p.Links, Homes: p.Homes}); err != nil {
t.Fatal(err)
}
if _, err := w.k.Write("author@g14", Key{KindDecision, r.Number}, "Context", p.Sections[0].Text); err != nil {
t.Fatal(err)
}
d, err := callTool(t, tools, DraftTool, key)
if err != nil || d["author"] != "author@g14" || len(d["sections"].([]any)) != 1 {
t.Fatalf("%v %v", err, d)
}
for _, s := range p.Sections[1:] {
_, _ = w.k.Write("author@g14", Key{KindDecision, r.Number}, s.Name, s.Text)
}
if _, err := w.k.SubmitDraft("author@g14", Key{KindDecision, r.Number}, ""); err != nil {
t.Fatal(err)
}
w.reviewAll(t)
h, err := callTool(t, tools, HeadTool, key)
if err != nil || h["entry"] == nil {
t.Fatalf("%v %v", err, h)
}
ops, err := callTool(t, tools, OperationsTool, key)
if err != nil || len(ops["operations"].([]any)) < 6 {
t.Fatalf("%v %v", err, ops)
}
var first map[string]any
for _, o := range ops["operations"].([]any) {
op := o.(map[string]any)
if op["body"] != nil {
t.Fatalf("the history shows a body: %v", op)
}
if first == nil && op["operation"] == OpWriteSection {
first = op
}
}
sec, err := callTool(t, tools, SectionTool, map[string]any{"number": key["number"], "sequence": first["sequence"]})
if err != nil || sec["text"] != p.Sections[0].Text || sec["title"] != "Context" {
t.Fatalf("%v %v", err, sec)
}
parts, err := callTool(t, tools, PartsTool, key)
if err != nil || len(parts["parts"].([]any)) != 5 {
t.Fatalf("%v %v", err, parts)
}
if _, err := callTool(t, tools, AskedTool, map[string]any{}); err != nil {
t.Fatal(err)
}
if fill, err := callTool(t, tools, FillTool, map[string]any{}); err != nil || fill["keys"] == nil {
t.Fatalf("%v %v", err, fill)
}
if st, err := callTool(t, tools, IndexTool, map[string]any{}); err != nil || st["built"] != true {
t.Fatalf("%v %v", err, st)
}
// The integrity of the sections: all well, then one damaged text in the history is named.
if res, err := callTool(t, tools, IntegrityTool, map[string]any{}); err != nil || len(res["damaged"].([]any)) != 0 {
t.Fatalf("%v %v", err, res)
}
hist := f.hist[InstanceOf(Key{KindDecision, r.Number})]
for i := range hist {
if hist[i].Operation == OpWriteSection && strings.Contains(string(hist[i].Body), "Considered Options") {
hist[i].Body = mustJSON(t, map[string]any{"title": "Considered Options", "text": "damaged", "digest": SectionDigest("1. **Keep it.** Rejected.")})
}
}
if res, _ := callTool(t, tools, IntegrityTool, key); len(res["damaged"].([]any)) == 0 {
t.Fatalf("a damaged section was not named: %v", res)
}
// An accepted record has no review to ask again; a proposed one does.
if _, err := callTool(t, tools, AgainTool, key); err == nil {
t.Fatal("a review of an accepted record was asked again")
}
q, _ := w.k.ReserveNumber("author@g14", KindDecision, "another")
if _, err := w.k.Propose("author@g14", valid(q.Number)); err != nil {
t.Fatal(err)
}
w.judge.review = `{"verdict":"changes","findings":["x"]}`
w.reviewAll(t)
w.judge.review = ""
if _, err := w.k.ReviewAgain("author@g14", Key{KindDecision, q.Number}); err != nil {
t.Fatal(err)
}
w.reviewAll(t)
if e := mustGet(t, w.k.Store, Key{KindDecision, q.Number}); e.Current.Status != StatusAccepted {
t.Fatalf("a review asked again: %s", e.Current.Status)
}
// The move's tools need the database beside the ledger.
if _, err := callTool(t, tools, MoveCheckTool, map[string]any{}); err == nil {
t.Fatal("a move check without a database")
}
if len(ModuleTools) != 15 {
t.Fatalf("%v", ModuleTools)
}
}
+13 -1
View File
@@ -151,7 +151,19 @@
"tools": [
"knowledge_health",
"knowledge_highest",
"knowledge_marks"
"knowledge_marks",
"knowledge_head",
"knowledge_operations",
"knowledge_section",
"knowledge_draft",
"knowledge_parts",
"knowledge_asked",
"knowledge_fill",
"knowledge_rebuild_index",
"knowledge_review_again",
"knowledge_integrity",
"knowledge_move",
"knowledge_move_check"
],
"emits": [
"reserved",