Files
mesh-catalog/modules/mesh-knowledge/cmd/mesh-knowledge/main.go
T
jschoubben 6fb827605a
mesh/merge-gate pass: builds mesh-knowledge → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery covered: a later merge that contains it was delivered: novox/mesh-catalog@1b7f79f76177 (merged as 73367192 into main, walk plan-17917161183…
mesh-knowledge: bound every review failure per record, make one out of tries loud, and let only its author revise a pending insight (review of #211)
2026-10-11 12:41:24 +02:00

958 lines
34 KiB
Go

// mesh-knowledge: the knowledge base, holder of the knowledge-base seat (novox/hq ADR 0290, to-be 55). A Go
// bundle the control-node's runtime launches. It holds every decision record and research effort whole — its
// fields, its clauses each with a status and a source, its typed links read from both ends, its reasoning — with
// every version kept, in a database in the store; answers what holds in one bounded call; takes a record proposed,
// checked when it is written, reviewed by its own judgement and accepted, on the operator's proven word when it
// needs it; and imports hq's records at a commit. stdout is the MCP channel; what this module says,
// it says on stderr.
//
// mesh-knowledge the bundle: the seat's verbs and the health tool
// mesh-knowledge prepare the preparation step: the database's schema brought to this version's
// mesh-knowledge keep acts: judges the whole record set every few minutes and keeps the findings
// mesh-knowledge watch judges: reads whether the database serves and what the findings say, every
// minute, and leaves its verdict in the status file the health tool reads
// mesh-knowledge dry-run <hq checkout> the import's dry run over a checkout on disk, against no database
//
// The two long-running processes are the act and the judge, as the mounts module's are: keep is judged by its
// unit, watch by the health tool, so the module never judges itself by its own tool alone (ADR 0240 rule 2).
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
"os"
"sort"
"strings"
"sync"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// Seat is the role this module holds, once for the mesh.
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", "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"
func logf(format string, a ...any) { fmt.Fprintf(os.Stderr, format+"\n", a...) }
func readSettings(path string) (Settings, error) {
var s Settings
if path == "" {
return s, errors.New("the module was started without a settings file")
}
raw, err := os.ReadFile(path)
if err != nil {
return s, err
}
if err := json.Unmarshal(raw, &s); err != nil {
return s, fmt.Errorf("the settings file is not the module's settings: %v", err)
}
return s, nil
}
func main() {
if len(os.Args) > 1 {
switch os.Args[1] {
case "prepare":
os.Exit(prepare())
case "keep":
os.Exit(keep())
case "watch":
os.Exit(watch(os.Getenv("MESH_KNOWLEDGE_STATUS")))
case "dry-run":
os.Exit(dryRunAtTheTerminal(os.Args[2:]))
}
}
st := &lazyStore{open: func(ctx context.Context) (*PgStore, error) { return PgStoreFromEnv(ctx) }}
k := newKB(st)
serve(k, os.Getenv("MESH_KNOWLEDGE_STATUS"))
}
// newKB is the knowledge base as the runtime launches it: everything it reaches, over the bus.
func newKB(store Store) *KB {
file := os.Getenv("MESH_KNOWLEDGE_SETTINGS")
return &KB{
Store: store,
Records: busRecords{},
Reserve: busReserver,
Judge: busJudgement(os.Getenv("MESH_NODE")),
Asks: sdkAskBus{},
Notify: busNotifier,
Emit: func(event string, body any) error { return stdio.Emit(event, body) },
Settings: func() (Settings, error) { return readSettings(file) },
Logf: logf,
}
}
// serve runs the bundle: the verbs, the operator's answers heard, and the background work.
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))); err != nil {
logf("%v", err)
os.Exit(1)
}
}
// callerOf is who called, as the runtime proves it: the one place the caller is read. The tool runner takes the
// caller's bus user from the subject the call arrived on and hands it through the SDK (hq issue 365); Unproven where
// it proves none. It is never read from the call's arguments, so no caller can name another.
func callerOf(args map[string]any) string { return stdio.CallerOf(args) }
// hear subscribes to the router's word on this module's asks, asked again until the runtime takes it.
func hear(k *KB) {
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
time.Sleep(wait)
err := stdio.Subscribe(asks.Seat+".decided."+Module, func(e stdio.Envelope) error {
var w asks.Warrant
if json.Unmarshal(e.Body, &w) == nil {
k.HeardEnd(w)
}
return nil
})
if err == nil {
logf("[mesh-knowledge] hears the operator's answers")
return
}
logf("[mesh-knowledge] does not hear the operator's answers yet (%v); asking again in %s", err, wait)
}
}
// background reads the router's record of every waiting ask once a minute, withdraws reservations never
// proposed, reviews every proposal waiting for its review — at once when one is proposed — and drafts and
// confirms clauses one step at a time, a review always before the next drafting step: the agent judges one
// question at a time, and a proposal does not wait behind the import's drafting.
func background(k *KB) {
tick := time.NewTicker(time.Minute)
defer tick.Stop()
time.Sleep(5 * time.Second)
review := func() {
for {
said, err := k.ReviewStep()
if err != nil {
logf("[mesh-knowledge] %v", err)
return
}
if said == "" {
return
}
logf("[mesh-knowledge] %s", said)
}
}
for {
if said, err := k.Reconcile(); err != nil {
logf("[mesh-knowledge] reading the operator's answers: %v", err)
} else {
for _, s := range said {
logf("[mesh-knowledge] %s", s)
}
}
if n, err := k.Withdraw(); err != nil {
logf("[mesh-knowledge] withdrawing reservations: %v", err)
} else if n > 0 {
logf("[mesh-knowledge] withdrew %d reservation(s) never proposed", n)
}
review()
for {
said, err := k.DraftStep()
if err != nil {
logf("[mesh-knowledge] %v", err)
break
}
if said == "" {
break
}
logf("[mesh-knowledge] %s", said)
review()
}
select {
case <-tick.C:
case <-k.kick:
}
}
}
// ---- the bus -------------------------------------------------------------------------------------------------
// busReserver asks the issue tracker for a number (issue-tracker.reserve, hq issue 424).
func busReserver(kind, title string) (int, error) {
raw, err := stdio.Ask("seat:issue-tracker.reserve", map[string]any{"kind": kind, "for": title})
if err != nil {
return 0, err
}
var out struct {
Kind string `json:"kind"`
Number int `json:"number"`
}
if err := json.Unmarshal(raw, &out); err != nil || out.Number < 1 || out.Kind != kind {
return 0, fmt.Errorf("the issue tracker's answer is not a %s number: %.200s", kind, raw)
}
return out.Number, nil
}
// busJudgement asks the agent module on this node for one judgement (ADR 0278).
func busJudgement(node string) Judgement {
return func(question string, schema map[string]any, model string) (json.RawMessage, string, error) {
key := "claude-code.claude_code_judge"
if node != "" {
key += "@" + node
}
raw, err := stdio.Ask(key, map[string]any{"question": question, "schema": schema, "model": model})
if err != nil {
return nil, "", err
}
var out struct {
Answer json.RawMessage `json:"answer"`
Model string `json:"model"`
Node string `json:"node"`
}
if err := json.Unmarshal(raw, &out); err != nil || len(out.Answer) == 0 {
return nil, "", fmt.Errorf("the agent module's answer is not a judgement: %.200s", raw)
}
return out.Answer, fmt.Sprintf("the agent on %s (%s)", out.Node, out.Model), nil
}
}
// busNotifier tells the operator's channel, as a warning: it is the one severity below urgent.
func busNotifier(key, summary, headline, explanation string) error {
_, err := stdio.Ask("seat:operator-channel.notify", map[string]any{"key": key, "subject": "the knowledge base",
"severity": "warning", "summary": summary, "headline": headline, "explanation": explanation})
return err
}
// ---- the store, opened when it is first needed ------------------------------------------------------------
// lazyStore opens the database on the first call, and again after a failure, so a database that arrives late is
// not fatal; and refuses to serve a schema behind this version.
type lazyStore struct {
open func(context.Context) (*PgStore, error)
mu sync.Mutex
pg *PgStore
}
func (l *lazyStore) store(ctx context.Context) (*PgStore, error) {
l.mu.Lock()
defer l.mu.Unlock()
if l.pg == nil {
pg, err := l.open(ctx)
if err != nil {
return nil, fmt.Errorf("the knowledge base's database cannot be opened: %w", err)
}
l.pg = pg
}
if err := l.pg.Ready(ctx); err != nil {
return nil, err
}
return l.pg, nil
}
// Ready says why the store cannot serve, or nil.
func (l *lazyStore) Ready(ctx context.Context) error { _, err := l.store(ctx); return err }
func (l *lazyStore) Get(ctx context.Context, k Key) (*Entry, error) {
s, err := l.store(ctx)
if err != nil {
return nil, err
}
return s.Get(ctx, k)
}
func (l *lazyStore) All(ctx context.Context) ([]*Entry, error) {
s, err := l.store(ctx)
if err != nil {
return nil, err
}
return s.All(ctx)
}
func (l *lazyStore) Create(ctx context.Context, v Version) error {
s, err := l.store(ctx)
if err != nil {
return err
}
return s.Create(ctx, v)
}
func (l *lazyStore) Save(ctx context.Context, v Version, current, clearPending bool, revision int64) error {
s, err := l.store(ctx)
if err != nil {
return err
}
return s.Save(ctx, v, current, clearPending, revision)
}
func (l *lazyStore) Versions(ctx context.Context, k Key) ([]Version, error) {
s, err := l.store(ctx)
if err != nil {
return nil, err
}
return s.Versions(ctx, k)
}
func (l *lazyStore) Search(ctx context.Context, q string, limit int) ([]Hit, error) {
s, err := l.store(ctx)
if err != nil {
return nil, err
}
return s.Search(ctx, q, limit)
}
func (l *lazyStore) Inbound(ctx context.Context, k Key) ([]Inbound, error) {
s, err := l.store(ctx)
if err != nil {
return nil, err
}
return s.Inbound(ctx, k)
}
func (l *lazyStore) SetFindings(ctx context.Context, f []Finding) error {
s, err := l.store(ctx)
if err != nil {
return err
}
return s.SetFindings(ctx, f)
}
func (l *lazyStore) Findings(ctx context.Context) ([]Finding, error) {
s, err := l.store(ctx)
if err != nil {
return nil, err
}
return s.Findings(ctx)
}
func (l *lazyStore) Told(ctx context.Context, topic string) (bool, error) {
s, err := l.store(ctx)
if err != nil {
return false, err
}
return s.Told(ctx, topic)
}
// ---- prepare, check, dry-run --------------------------------------------------------------------------------
func prepare() int {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
s, err := PgStoreFromEnv(ctx)
if err != nil {
logf("[mesh-knowledge] preparing the database: %v", err)
return 1
}
defer s.Close()
said, err := s.Prepare(ctx)
for _, line := range said {
logf("[mesh-knowledge] %s", line)
}
if err != nil {
logf("[mesh-knowledge] preparing the database failed: %v", err)
return 1
}
return 0
}
// KeepEvery is how often keep judges the whole record set; WatchEvery how often watch looks.
const (
KeepEvery = 5 * time.Minute
WatchEvery = time.Minute
)
// keep judges the whole record set every few minutes and keeps the findings in the database: a change that
// reached the database by any way is judged within minutes, not only one an acceptance made.
func keep() int {
for {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
s, err := PgStoreFromEnv(ctx)
if err == nil {
if err = s.Ready(ctx); err == nil {
k := &KB{Store: s, Logf: logf}
f := k.judgeSet(ctx)
logf("[mesh-knowledge] the whole record set judged: %d finding(s)", len(f))
}
s.Close()
}
if err != nil {
logf("[mesh-knowledge] the whole record set cannot be judged now: %v", err)
}
cancel()
time.Sleep(KeepEvery)
}
}
// watch reads, every minute, whether the database serves this version and what the whole-set findings say,
// and leaves its verdict in the status file.
func watch(status string) int {
if status == "" {
logf("[mesh-knowledge] watch was started without MESH_KNOWLEDGE_STATUS")
return 2
}
for {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
var answer HealthAnswer
s, err := PgStoreFromEnv(ctx)
if err != nil {
answer = HealthAnswer{State: "unhealthy", Why: "the knowledge base's database cannot be opened: " + err.Error()}
} else {
answer = (&KB{Store: s}).Health(s)
s.Close()
}
cancel()
answer.At = time.Now().UTC()
if err := writeStatus(status, answer); err != nil {
logf("[mesh-knowledge] the status could not be written: %v", err)
}
time.Sleep(WatchEvery)
}
}
func writeStatus(path string, a HealthAnswer) error {
raw, err := json.Marshal(a)
if err != nil {
return err
}
tmp := path + ".next"
if err := os.WriteFile(tmp, raw, 0o600); err != nil {
return err
}
return os.Rename(tmp, path)
}
// StatusStale is how old the watch's verdict may be before the health tool says the watch stopped looking.
const StatusStale = 3 * WatchEvery
// ReadStatus is the watch's last verdict, as the health tool answers it: unhealthy when there is none, or when it
// is older than StatusStale.
func ReadStatus(path string, now time.Time) HealthAnswer {
raw, err := os.ReadFile(path)
if err != nil {
return HealthAnswer{State: "unhealthy", Why: "the watch has left no verdict yet: " + err.Error()}
}
var a HealthAnswer
if err := json.Unmarshal(raw, &a); err != nil || (a.State != "healthy" && a.State != "unhealthy") {
return HealthAnswer{State: "unhealthy", Why: "the watch's verdict cannot be read"}
}
if age := now.Sub(a.At); age > StatusStale {
return HealthAnswer{State: "unhealthy", Why: fmt.Sprintf("the watch has not looked for %s (its last verdict: %s)", age.Round(time.Second), a.Why)}
}
a.Healthy = a.State == "healthy"
return a
}
// dryRunAtTheTerminal is the import's dry run over a checkout on disk, against an empty knowledge base: what the
// import would create, before the module is rolled out. Only the verb writes.
func dryRunAtTheTerminal(args []string) int {
if len(args) != 1 {
logf("usage: mesh-knowledge dry-run <hq checkout>")
return 2
}
k := &KB{Store: newMemStore(), Logf: logf}
out, err := k.Import(Unproven, ImportAsk{DryRun: true}, dirRecords{dir: args[0]})
if err != nil {
logf("%v", err)
return 1
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
_ = enc.Encode(out)
return 0
}
// ---- health ---------------------------------------------------------------------------------------------------
// Healther is a store that can say why it cannot serve.
type Healther interface {
Ready(ctx context.Context) error
}
// HealthAnswer is the health tool's answer (novox/hq ADR 0240, ADR 0283): healthy or unhealthy, and why; At is
// when the watch looked.
type HealthAnswer struct {
State string `json:"state"`
Healthy bool `json:"healthy"`
Why string `json:"why"`
At time.Time `json:"at,omitempty"`
}
// Health is how the knowledge base is: unhealthy when its database cannot serve, when a rule that judges the
// whole record set fails, or when a proposed record ran out of review tries — the mesh raises a condition naming
// the rule, the records, or the records not reviewed.
func (k *KB) Health(ready Healther) HealthAnswer {
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
defer cancel()
if ready != nil {
if err := ready.Ready(ctx); err != nil {
return HealthAnswer{State: "unhealthy", Why: err.Error()}
}
}
findings, err := k.Store.Findings(ctx)
if err != nil {
return HealthAnswer{State: "unhealthy", Why: "the whole-set findings cannot be read: " + err.Error()}
}
var says []string
for _, f := range findings {
says = append(says, "the rule "+f.Rule+": "+f.Says)
}
sort.Strings(says)
all, err := k.Store.All(ctx)
if err != nil {
return HealthAnswer{State: "unhealthy", Why: "the records cannot be read: " + err.Error()}
}
unreviewed := Unreviewed(all)
if len(findings)+len(unreviewed) > 0 {
var parts []string
if len(unreviewed) > 0 {
if len(unreviewed) > 8 {
unreviewed = append(unreviewed[:8], fmt.Sprintf("and %d more", len(unreviewed)-8))
}
parts = append(parts, fmt.Sprintf("%d proposed record(s) could not be reviewed in %d tries and wait for their authors to propose them again: %s",
len(Unreviewed(all)), MostReviewTries, strings.Join(unreviewed, "; ")))
}
if len(says) > 0 {
if len(says) > 8 {
says = append(says[:8], fmt.Sprintf("and %d more", len(says)-8))
}
parts = append(parts, fmt.Sprintf("%d rule(s) that judge the whole record set fail: %s", len(findings), strings.Join(says, "; ")))
}
return HealthAnswer{State: "unhealthy", Why: strings.Join(parts, ". ")}
}
return HealthAnswer{State: "healthy", Healthy: true, Why: "the database serves, every rule that judges the whole record set holds, and no proposal ran out of review tries"}
}
func healthTool(status string, now func() time.Time) stdio.Tool {
return stdio.Tool{Name: HealthTool,
Description: "How the knowledge base is, as its watch last saw it: healthy when its database serves this version and every " +
"rule that judges the whole record set holds; unhealthy otherwise, naming the rule and the records — a decision accepted " +
"that nothing cites, a supersession or a change said on one end only, a record extending a superseded one — or when a " +
"proposed record ran out of review tries, naming it.",
Run: func(map[string]any) (any, error) { return ReadStatus(status, now()), nil }}
}
// ---- the verbs --------------------------------------------------------------------------------------------------
// args reads one call's arguments strictly: an argument the verb does not take is refused, never ignored — and no
// argument can say who the caller is.
type args struct {
verb string
m map[string]any
}
func readArgs(verb string, m map[string]any, takes ...string) (args, error) {
allowed := map[string]bool{}
for _, k := range takes {
allowed[k] = true
}
var unknown []string
for k := range m {
if !allowed[k] {
unknown = append(unknown, k)
}
}
if len(unknown) > 0 {
sort.Strings(unknown)
return args{}, fmt.Errorf("%s takes %s; it does not take %s", verb, strings.Join(takes, ", "), strings.Join(unknown, ", "))
}
return args{verb: verb, m: m}, nil
}
func (a args) str(k string) (string, error) {
v, ok := a.m[k]
if !ok || v == nil {
return "", nil
}
s, ok := v.(string)
if !ok {
return "", fmt.Errorf("%s is text", k)
}
return s, nil
}
func (a args) strs(k string) ([]string, error) {
v, ok := a.m[k]
if !ok || v == nil {
return nil, nil
}
switch t := v.(type) {
case string:
return []string{t}, nil
case []any:
out := make([]string, 0, len(t))
for _, e := range t {
s, ok := e.(string)
if !ok {
return nil, fmt.Errorf("every entry of %s is text", k)
}
out = append(out, s)
}
return out, nil
}
return nil, fmt.Errorf("%s is a list of text", k)
}
func (a args) num(k string, required bool) (int, error) {
v, ok := a.m[k]
if !ok || v == nil {
if required {
return 0, fmt.Errorf("%s needs %s", a.verb, k)
}
return 0, nil
}
switch t := v.(type) {
case float64:
if t >= 0 && t <= 1e6 && t == math.Trunc(t) {
return int(t), nil
}
case string:
if kindNumber.MatchString(strings.TrimSpace(t)) {
return atoi(strings.TrimSpace(t)), nil
}
}
return 0, fmt.Errorf("%s is a whole number, such as 276", k)
}
func (a args) flag(k string) (bool, error) {
v, ok := a.m[k]
if !ok || v == nil {
return false, nil
}
b, ok := v.(bool)
if !ok {
return false, fmt.Errorf("%s is true or false", k)
}
return b, nil
}
// key reads kind (decision when absent) and number.
func (a args) key() (Key, error) {
kind, err := a.str("kind")
if err != nil {
return Key{}, err
}
if kind == "" {
kind = KindDecision
}
if !contains(Kinds, kind) {
return Key{}, fmt.Errorf("kind is decision or research, not %q", kind)
}
n, err := a.num("number", true)
if err != nil {
return Key{}, err
}
if n < 1 {
return Key{}, errors.New("number is a record's number, from 1")
}
return Key{kind, n}, nil
}
func text(d string) map[string]any { return map[string]any{"type": "string", "description": d} }
func texts(d string) map[string]any {
return map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": d}
}
func integer(d string) map[string]any {
return map[string]any{"type": "integer", "minimum": 1, "description": d}
}
func flag(d string) map[string]any { return map[string]any{"type": "boolean", "description": d} }
func kindArg() map[string]any {
return map[string]any{"type": "string", "enum": Kinds, "description": "decision (the default) or research"}
}
func boundArg() map[string]any {
return map[string]any{"type": "integer", "minimum": LeastBound, "maximum": MostBound,
"description": "the most bytes the answer may hold (default 4096)"}
}
func schema(required []string, props map[string]any) map[string]any {
s := map[string]any{"type": "object", "properties": props, "additionalProperties": false}
if len(required) > 0 {
s["required"] = required
}
return s
}
// Tools are the seat's verbs, each said in plain words: what it does and what it refuses. caller is who called,
// as the runtime proves it.
func Tools(k *KB, caller func(map[string]any) string) []stdio.Tool {
return []stdio.Tool{
{Name: Seat + ".ask",
Description: "Ask what holds about something, in plain words — \"what holds about the merge window, and why\" — and get the " +
"decision records and research efforts that bear on it, ranked, with their matching clauses: each clause says whether " +
"it is in force, what changed or superseded it, or that it is only drafted and not the record's word; each record says " +
"what changed it since and how to read its reasoning. One call, within a bound it states (4 KiB unless asked for more), " +
"saying what it cut.",
Input: schema([]string{"question"}, map[string]any{"question": map[string]any{"type": "string", "maxLength": MostQuestion,
"description": "the question, in plain words, at most 500 characters"}, "bound": boundArg()}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("ask", m, "question", "bound")
if err != nil {
return nil, err
}
q, err := a.str("question")
if err != nil {
return nil, err
}
b, err := a.num("bound", false)
if err != nil {
return nil, err
}
return k.Ask(q, b)
}},
{Name: Seat + ".get",
Description: "One record by its number: its title, status, topic, date and deciders; its clauses, each with whether it holds; " +
"its links both ways; what changed it since; its versions; its reviews, and the operator's ask when it waits for one. " +
"Its reasoning (context, options, consequences, how it is checked; a research effort's documents) only with reasoning, " +
"or one section by name; an earlier version with version. Within a bound it states, 4 KiB unless asked for more, " +
"saying what it cut.",
Input: schema([]string{"number"}, map[string]any{"kind": kindArg(), "number": integer("the record's number, such as 276"),
"reasoning": flag("include its reasoning whole"), "section": text("only this section of its reasoning, by name"),
"version": integer("an earlier version"), "bound": boundArg()}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("get", m, "kind", "number", "reasoning", "section", "version", "bound")
if err != nil {
return nil, err
}
key, err := a.key()
if err != nil {
return nil, err
}
g := GetAsk{Kind: key.Kind, Number: key.Number}
if g.Reasoning, err = a.flag("reasoning"); err != nil {
return nil, err
}
if g.Section, err = a.str("section"); err != nil {
return nil, err
}
if g.Version, err = a.num("version", false); err != nil {
return nil, err
}
if g.Bound, err = a.num("bound", false); err != nil {
return nil, err
}
return k.Get(g)
}},
{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. " +
"Within a bound it states, 4 KiB unless asked for more, saying what it cut.",
Input: schema(nil, map[string]any{"topic": text("a topic, such as the mesh"), "kind": kindArg(),
"number": integer("one record's number"), "bound": boundArg()}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("holds", m, "topic", "kind", "number", "bound")
if err != nil {
return nil, err
}
topic, err := a.str("topic")
if err != nil {
return nil, err
}
b, err := a.num("bound", false)
if err != nil {
return nil, err
}
var key *Key
if _, given := m["number"]; given {
kk, err := a.key()
if err != nil {
return nil, err
}
key = &kk
}
if key != nil && topic != "" {
return nil, errors.New("holds takes a topic or a record's number, not both")
}
return k.Holds(topic, key, b)
}},
{Name: Seat + ".links",
Description: "A record's typed links, both ways: what it supersedes, changes (clause by clause), extends, became or cites, " +
"and every record that points at it, each with how it was found.",
Input: schema([]string{"number"}, map[string]any{"kind": kindArg(), "number": integer("the record's number")}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("links", m, "kind", "number")
if err != nil {
return nil, err
}
key, err := a.key()
if err != nil {
return nil, err
}
return k.Links(key)
}},
{Name: Seat + ".history",
Description: "A record's versions, every one kept: who made it, when, why, and whether it was a progressive insight; with " +
"from and to, the difference between those two versions, line by line. Within a bound it states, 4 KiB unless asked for " +
"more, saying what it cut.",
Input: schema([]string{"number"}, map[string]any{"kind": kindArg(), "number": integer("the record's number"),
"from": integer("the earlier version to compare"), "to": integer("the later version to compare"), "bound": boundArg()}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("history", m, "kind", "number", "from", "to", "bound")
if err != nil {
return nil, err
}
key, err := a.key()
if err != nil {
return nil, err
}
from, err := a.num("from", false)
if err != nil {
return nil, err
}
to, err := a.num("to", false)
if err != nil {
return nil, err
}
b, err := a.num("bound", false)
if err != nil {
return nil, err
}
return k.History(key, from, to, b)
}},
{Name: Seat + ".reserve",
Description: "Reserve the next decision or research number for a record you are about to propose, from the issue " +
"tracker's counter: never one another caller got. The number is spent whether or not its record is ever accepted; " +
"one never proposed is withdrawn after a week. Refused: a kind other than decision or research, an empty title.",
Input: schema([]string{"kind", "for"}, map[string]any{"kind": map[string]any{"type": "string", "enum": Kinds},
"for": text("the title of the record the number is for")}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("reserve", m, "kind", "for")
if err != nil {
return nil, err
}
kind, err := a.str("kind")
if err != nil {
return nil, err
}
title, err := a.str("for")
if err != nil {
return nil, err
}
return k.ReserveNumber(caller(m), kind, title)
}},
{Name: Seat + ".propose",
Description: "Propose a record whole under a reserved number: its title, topic, date and deciders; for a decision its " +
"clauses, each opening with a bold lead (\"**What is decided.** …\"), and its reasoning in sections (Context, " +
"Considered Options, Consequences, How it is checked, References); for a research effort its status and documents; its " +
"links (supersedes, changes \"0276 §4\", extends, became, cites), and whether it needs the operator. Refused, naming " +
"every reason at once and writing nothing, when it breaks one of the 29 rules that judge one record or its prose — a " +
"link to nothing, a clause without a bold lead, a superseded record cited alone, an unknown topic, a retired word. A " +
"proposal is revised by proposing it again; an accepted record changes only by a progressive insight (insight: what " +
"stood before). The knowledge base then reviews each version itself, in a judgement that sees only the record, the " +
"records it links and the rules: on approve it is accepted (a record that needs the operator waits for the operator's " +
"Approve), on changes it stays proposed with the findings for its author to revise, and when no judgement can be had " +
"it stays proposed and says so. Nothing proposed is in force.",
Input: schema([]string{"number", "title"}, map[string]any{
"kind": kindArg(), "number": integer("the number reserve gave"), "title": text("one line"),
"topic": text("for a decision: a topic of the reading order, such as the mesh"),
"date": text("the day, YYYY-MM-DD (today when absent)"),
"deciders": texts("who decided"),
"clauses": texts("a decision's clauses in order, each \"**Lead.** the statement\""),
"sections": map[string]any{"type": "array", "description": "the reasoning, in order: each a name and its markdown",
"items": map[string]any{"type": "object", "properties": map[string]any{"name": text("such as Context"), "text": text("markdown")},
"required": []string{"name", "text"}}},
"links": map[string]any{"type": "array", "description": "its links: kind, to (\"0276\", \"0276 §4\", \"research 045\", \"issue 365\", a design's path), from (the clause it goes out from)",
"items": map[string]any{"type": "object", "properties": map[string]any{"kind": map[string]any{"type": "string", "enum": LinkKinds},
"to": text("the target"), "from": integer("the clause of this record it goes out from")}, "required": []string{"kind", "to"}}},
"homes": texts("hq documents that cite it, such as a design's path"),
"needs-operator": flag("it holds only once the operator approves it"),
"status": map[string]any{"type": "string", "enum": ResearchStatuses, "description": "a research effort's status"},
"insight": text("for an accepted record: a progressive insight, saying what stood before"),
"why": text("one line: why this version"),
}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("propose", m, "kind", "number", "title", "topic", "date", "deciders", "clauses", "sections",
"links", "homes", "needs-operator", "status", "insight", "why")
if err != nil {
return nil, err
}
key, err := a.key()
if err != nil {
return nil, err
}
p := ProposeAsk{Kind: key.Kind, Number: key.Number}
for _, f := range []struct {
k string
p *string
}{{"title", &p.Title}, {"topic", &p.Topic}, {"date", &p.Date}, {"status", &p.Status}, {"insight", &p.Insight}, {"why", &p.Why}} {
if *f.p, err = a.str(f.k); err != nil {
return nil, err
}
}
for _, f := range []struct {
k string
p *[]string
}{{"deciders", &p.Deciders}, {"clauses", &p.Clauses}, {"homes", &p.Homes}} {
if *f.p, err = a.strs(f.k); err != nil {
return nil, err
}
}
if p.NeedsOperator, err = a.flag("needs-operator"); err != nil {
return nil, err
}
if err := decode(m["sections"], &p.Sections, "sections is a list of {name, text}"); err != nil {
return nil, err
}
if err := decode(m["links"], &p.Links, "links is a list of {kind, to, from}"); err != nil {
return nil, err
}
return k.Propose(caller(m), p)
}},
{Name: Seat + ".import",
Description: "Import hq's decision records and research efforts, read through the records module at the commit its checkout " +
"stands at, each whole at its own number as its first version naming that commit. It only creates a record that is not " +
"held, never overwrites one, never moves a number counter, and run again changes nothing and says so. A decision written " +
"as prose waits for its clauses to be drafted and confirmed. With dry-run it writes nothing and says what it would do; " +
"with commit it refuses a checkout standing elsewhere.",
Input: schema(nil, map[string]any{"dry-run": flag("read everything, write nothing, say what would be done"),
"commit": text("the commit of hq the record must stand at (a prefix is enough)")}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("import", m, "dry-run", "commit")
if err != nil {
return nil, err
}
var ask ImportAsk
if ask.DryRun, err = a.flag("dry-run"); err != nil {
return nil, err
}
if ask.Commit, err = a.str("commit"); err != nil {
return nil, err
}
return k.Import(caller(m), ask, k.Records)
}},
{Name: Seat + ".return",
Description: "Send the clauses drafted from a prose decision back to be drafted again, with your reason, which the next " +
"drafting reads. Until they are drafted and confirmed again the record serves none. Refused: a record whose clauses its " +
"author wrote, an empty reason.",
Input: schema([]string{"number", "reason"}, map[string]any{"kind": kindArg(), "number": integer("the record's number"),
"reason": text("what is wrong with the drafted clauses")}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("return", m, "kind", "number", "reason")
if err != nil {
return nil, err
}
key, err := a.key()
if err != nil {
return nil, err
}
reason, err := a.str("reason")
if err != nil {
return nil, err
}
return k.Return(caller(m), key, reason)
}},
}
}
// decode reads a structured argument strictly.
func decode(v any, into any, says string) error {
if v == nil {
return nil
}
raw, err := json.Marshal(v)
if err != nil {
return errors.New(says)
}
dec := json.NewDecoder(strings.NewReader(string(raw)))
dec.DisallowUnknownFields()
if err := dec.Decode(into); err != nil {
return fmt.Errorf("%s (%v)", says, err)
}
return nil
}