Add mesh-knowledge, the knowledge base's first slice (hq issue 425, ADR 0290)
mesh/merge-gate pass: builds new: modules/mesh-knowledge, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compo…
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 superseded: a newer head of the same pull request

An agent learns what a decision says only by reading whole records. This module
holds every decision record and research effort whole in a database in the store,
with clauses that carry their own status and source, typed links read from both
ends, and every version. It answers ask, get and holds within a stated bound, and
takes a record through reserve, propose (refused on any of the 29 per-record
rules), review by someone other than the author, and acceptance, on the
operator's proven warrant when the record needs it. The 4 whole-set rules make
the module unhealthy when they fail. The import reads hq at a commit through the
records module, never overwrites, and changes nothing when run again. Prose
decisions get drafted clauses that a second judgement must confirm before they
are served.
This commit is contained in:
2026-10-11 04:58:35 +02:00
parent 27cb95e98f
commit ba7b455719
50 changed files with 11883 additions and 0 deletions
@@ -0,0 +1,576 @@
package main
// The knowledge base's writes (novox/hq ADR 0290 rules 4 and 5, to-be 55 §4, §5 and §6): a number reserved from
// the issue tracker, a record proposed and judged by the 29 rules, reviewed by someone who did not write it, and
// accepted — at once, or on the operator's proven warrant when it needs the operator — with every clause it
// changes or supersedes moved accordingly and the whole record set judged after.
import (
"context"
"encoding/json"
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"sync"
"time"
)
// Unproven is who acted, as recorded, while the runtime does not tell a module who called it (hq issue 365). The
// caller is never taken from an argument.
const Unproven = "unproven"
// proven says whether a caller is one the runtime proved.
func proven(caller string) bool { return caller != "" && caller != Unproven }
// The events this module says (ADR 0290 rule 7): every change, with what changed and who changed it.
const (
EventReserved = "reserved"
EventProposed = "proposed"
EventReviewed = "reviewed"
EventAccepted = "accepted"
EventChanged = "changed"
EventImported = "imported"
EventDrafted = "drafted"
EventConfirmed = "confirmed"
EventReturned = "returned"
)
// Events are the events, in the order the manifest declares them.
var Events = []string{EventReserved, EventProposed, EventReviewed, EventAccepted, EventChanged, EventImported,
EventDrafted, EventConfirmed, EventReturned}
// Settings are the operator's values for this module, merged by the mesh into one JSON file.
type Settings struct {
// JudgeModel is the model the drafting and confirming judgements ask; sonnet when empty.
JudgeModel string `json:"judge-model,omitempty"`
// Drafting is "off" to stop drafting and confirming clauses; anything else is on.
Drafting string `json:"drafting,omitempty"`
// ReservedFor is how long a reserved number waits for its proposal before it is withdrawn, as a duration
// such as 168h; DefaultReservedFor when empty.
ReservedFor string `json:"reserved-for,omitempty"`
}
// DefaultReservedFor is how long a reservation waits for its proposal unless the settings say otherwise.
const DefaultReservedFor = 7 * 24 * time.Hour
// Reserver asks the issue tracker for the next number of a kind (issue-tracker.reserve).
type Reserver func(kind, title string) (int, error)
// Judgement asks the agent on the node one question with no tools (claude-code.claude_code_judge), and answers
// the structured answer and who judged.
type Judgement func(question string, schema map[string]any, model string) (json.RawMessage, string, error)
// Notifier tells the operator's channel something (operator-channel.notify).
type Notifier func(key, summary, headline, explanation string) error
// KB is the knowledge base.
type KB struct {
Store Store
Records Records
Reserve Reserver
Judge Judgement
Asks AskBus
Notify Notifier
Emit func(event string, body any) error
Settings func() (Settings, error)
Now func() time.Time
Logf func(format string, a ...any)
// mu serialises the writes: one instance holds the seat, and a write reads, judges and writes as one.
mu sync.Mutex
refMu sync.Mutex
ref *Reference
refAt time.Time
}
func (k *KB) now() time.Time {
if k.Now != nil {
return k.Now().UTC()
}
return time.Now().UTC()
}
func (k *KB) logf(format string, a ...any) {
if k.Logf != nil {
k.Logf(format, a...)
}
}
func (k *KB) settings() Settings {
if k.Settings == nil {
return Settings{}
}
s, err := k.Settings()
if err != nil {
k.logf("[mesh-knowledge] the settings cannot be read (%v): the defaults hold", err)
}
return s
}
// emit says a change. A change that could not be said is logged and kept: the store holds it whole, and its
// versions are the record of who changed what.
func (k *KB) emit(event string, r *Record, by, what string, version int) {
if k.Emit == nil {
return
}
body := map[string]any{"kind": r.Kind, "number": r.Number, "record": r.Key().String(), "by": by, "at": k.now(),
"what": what, "version": version, "status": r.Status}
if err := k.Emit(event, body); err != nil {
k.logf("[mesh-knowledge] the event %s for %s could not be said: %v", event, r.Key(), err)
}
}
// reference is the reading order and the glossary, read again after ten minutes.
func (k *KB) reference() (*Reference, error) {
k.refMu.Lock()
defer k.refMu.Unlock()
if k.ref != nil && k.now().Sub(k.refAt) < 10*time.Minute {
return k.ref, nil
}
if k.Records == nil {
return nil, errors.New("hq cannot be read: the module has no way to the records module")
}
ref, err := LoadReference(k.Records)
if err != nil {
if k.ref != nil {
return k.ref, nil
}
return nil, err
}
k.ref, k.refAt = ref, k.now()
return ref, nil
}
func (k *KB) lookup(ctx context.Context) Lookup {
return func(key Key) *Record {
e, err := k.Store.Get(ctx, key)
if err != nil || e == nil {
return nil
}
return e.Current
}
}
var kindNumber = regexp.MustCompile(`^\d{1,4}$`)
// ---- reserve -----------------------------------------------------------------------------------------------
// Reserved is the answer to reserve.
type Reserved struct {
Kind string `json:"kind"`
Number int `json:"number"`
Record string `json:"record"`
Says string `json:"says"`
}
// ReserveNumber asks the issue tracker for the next number of a kind and holds it as reserved, with who reserved
// it and when (to-be 55 §6). The knowledge base keeps no counter.
func (k *KB) ReserveNumber(caller, kind, title string) (*Reserved, error) {
if !contains(Kinds, kind) {
return nil, fmt.Errorf("kind is decision or research, not %q; nothing was reserved", kind)
}
title = strings.TrimSpace(title)
if title == "" || strings.Contains(title, "\n") || len([]rune(title)) > 200 {
return nil, errors.New("for is the record's title, one line of at most 200 characters; nothing was reserved")
}
if k.Reserve == nil {
return nil, errors.New("the issue tracker cannot be asked for a number here; nothing was reserved")
}
n, err := k.Reserve(kind, title)
if err != nil {
return nil, fmt.Errorf("the issue tracker did not hand out a %s number (%v); nothing was reserved", kind, err)
}
k.mu.Lock()
defer k.mu.Unlock()
ctx := context.Background()
at := k.now()
r := &Record{Kind: kind, Number: n, Title: title, Status: StatusReserved, ReservedBy: caller, ReservedAt: at, ReservedFor: title}
err = k.Store.Create(ctx, Version{By: caller, At: at, Why: "number reserved from the issue tracker", Record: r})
if errors.Is(err, ErrExists) {
return nil, fmt.Errorf("the issue tracker handed out %s, which the knowledge base already holds: the number is spent, "+
"and its counter stands below what the knowledge base holds. Reserve again, and tell the operator", r.Key())
}
if err != nil {
return nil, fmt.Errorf("%s was handed out and could not be kept (%v): the number is spent; reserve again", r.Key(), err)
}
k.emit(EventReserved, r, caller, "reserved for "+title, 1)
return &Reserved{Kind: kind, Number: n, Record: r.Key().String(),
Says: fmt.Sprintf("%s is reserved for %q. Propose the record under this number; a number never proposed is withdrawn, and never handed out again.", r.Key(), title)}, nil
}
// ---- propose -----------------------------------------------------------------------------------------------
// LinkAsk is a link as a proposal gives it: its kind, its target as written ("0276 §4"), the clause it goes out
// from.
type LinkAsk struct {
Kind string `json:"kind"`
To string `json:"to"`
From int `json:"from,omitempty"`
}
// ProposeAsk is a record proposed whole.
type ProposeAsk struct {
Kind string
Number int
Title string
Topic string
Date string
Deciders []string
Clauses []string
Sections []Section
Links []LinkAsk
Homes []string
NeedsOperator bool
// Status is a research effort's: active, graduated or abandoned.
Status string
// Insight, for an accepted record, is what stood before: the proposal is a progressive insight.
Insight string
Why string
}
// Proposed is the answer to propose.
type Proposed struct {
Record string `json:"record"`
Version int `json:"version"`
Status string `json:"status"`
Says string `json:"says"`
}
var numberedClause = regexp.MustCompile(`^(\d+)\.\s+`)
// MostRecordBytes is the most one record may hold.
const MostRecordBytes = 512 << 10
// Propose writes a record whole as proposed, or refuses it naming every rule it breaks. A proposal is revised
// as new versions until it is reviewed; an accepted record is changed only by a progressive insight (Insight),
// which waits for its own review while the accepted version stays the one served.
func (k *KB) Propose(caller string, a ProposeAsk) (*Proposed, error) {
ctx := context.Background()
k.mu.Lock()
defer k.mu.Unlock()
if !contains(Kinds, a.Kind) {
return nil, fmt.Errorf("kind is decision or research, not %q; nothing was proposed", a.Kind)
}
key := Key{a.Kind, a.Number}
held, err := k.Store.Get(ctx, key)
if err != nil {
return nil, fmt.Errorf("the knowledge base cannot be read (%v); nothing was proposed", err)
}
ref, err := k.reference()
if err != nil {
return nil, fmt.Errorf("the rules cannot be judged: %v. Nothing was proposed", err)
}
r := &Record{Kind: a.Kind, Number: a.Number, Title: strings.TrimSpace(a.Title), Topic: strings.TrimSpace(a.Topic),
Status: StatusProposed, Date: strings.TrimSpace(a.Date), Deciders: a.Deciders, Effort: a.Status,
NeedsOperator: a.NeedsOperator, Sections: a.Sections, Homes: a.Homes, Author: caller}
if r.Date == "" {
r.Date = k.now().Format("2006-01-02")
}
var refusals []Refusal
for i, text := range a.Clauses {
n := i + 1
text = strings.TrimSpace(text)
if m := numberedClause.FindStringSubmatch(text); m != nil {
n, _ = strconv.Atoi(m[1])
text = strings.TrimSpace(text[len(m[0]):])
}
r.Clauses = append(r.Clauses, Clause{N: n, Text: text, Status: ClauseProposed, Source: SourceWritten})
}
for _, l := range a.Links {
to, err := ParseRef(l.To)
if err != nil {
refusals = append(refusals, Refusal{Rule: "link-target", Says: err.Error()})
continue
}
r.Links = append(r.Links, Link{Kind: l.Kind, From: l.From, To: to, Source: SourceWritten})
}
current := true
why := strings.TrimSpace(a.Why)
if held != nil {
switch w := held.Working(); {
case held.Current.Status == StatusReserved:
r.ReservedBy, r.ReservedAt, r.ReservedFor = held.Current.ReservedBy, held.Current.ReservedAt, held.Current.ReservedFor
if why == "" {
why = "proposed"
}
case w.Status == StatusProposed && held.Pending == nil:
// A revision of a proposal: a new version, its earlier reviews kept with the versions they reviewed.
r.Author, r.Reviews, r.ReservedBy, r.ReservedAt, r.ReservedFor = w.Author, w.Reviews, w.ReservedBy, w.ReservedAt, w.ReservedFor
if a.Insight != "" {
refusals = append(refusals, Refusal{Rule: "reserved", Says: "a proposal not yet accepted is revised, not corrected by an insight"})
}
if why == "" {
why = "revised"
}
case held.Current.Accepted():
if strings.TrimSpace(a.Insight) == "" {
return nil, fmt.Errorf("%s is %s: it changes only by a new record, or by a progressive insight, a correction of fact "+
"that leaves the decision standing (give insight: what stood before). Nothing was proposed", key, held.Current.Status)
}
current = false
r.Author = caller
r.AcceptedAt = held.Current.AcceptedAt
r.Pending = &PendingState{Insight: Insight{Date: k.now().Format("2006-01-02"), Before: strings.TrimSpace(a.Insight), By: caller}}
r.Date = held.Current.Date
r.Effort = a.Status
if why == "" {
why = "a progressive insight proposed"
}
default:
return nil, fmt.Errorf("%s is %s and takes no proposal; nothing was proposed", key, held.Working().Status)
}
}
if !current && a.Kind == KindResearch && r.Effort == "" {
r.Effort = held.Current.Status
}
refusals = append(refusals, JudgeRecord(r, held, k.lookup(ctx), ref)...)
if raw, _ := json.Marshal(r); len(raw) > MostRecordBytes {
refusals = append(refusals, Refusal{Rule: "size", Says: fmt.Sprintf("the record is %d bytes; a record holds at most %d", len(raw), MostRecordBytes)})
}
if len(refusals) > 0 {
lines := make([]string, len(refusals))
for i, x := range refusals {
lines[i] = x.String()
}
return nil, fmt.Errorf("%s is refused by %d rule(s), and nothing was written:\n- %s", key, len(refusals), strings.Join(lines, "\n- "))
}
v := Version{By: caller, At: k.now(), Why: why, Record: r, Insight: !current}
if err := k.Store.Save(ctx, v, current, false, held.Revision); err != nil {
return nil, fmt.Errorf("%s could not be written (%v); nothing was proposed", key, err)
}
n := held.Versions + 1
k.emit(EventProposed, r, caller, why, n)
says := fmt.Sprintf("%s is proposed as version %d. Nothing of it is in force until a review by someone who did not write it approves it", key, n)
if r.NeedsOperator {
says += ", and then the operator's Approve"
}
if !current {
says = fmt.Sprintf("A progressive insight to %s is proposed as version %d; the accepted version is served until a review approves it", key, n)
}
return &Proposed{Record: key.String(), Version: n, Status: StatusProposed, Says: says + "."}, nil
}
// ---- review and acceptance ---------------------------------------------------------------------------------
// ReviewAsk is one review.
type ReviewAsk struct {
Kind string
Number int
Verdict string
Findings string
}
// Reviewed is the answer to review.
type Reviewed struct {
Record string `json:"record"`
Verdict string `json:"verdict"`
Status string `json:"status"`
Says string `json:"says"`
}
// Review takes a review of the version proposed, by someone who did not write it. On approve the record is
// accepted, or, when it needs the operator, asked of the operator; on changes it stays proposed, the findings
// kept with it.
func (k *KB) Review(caller string, a ReviewAsk) (*Reviewed, error) {
ctx := context.Background()
k.mu.Lock()
defer k.mu.Unlock()
key := Key{a.Kind, a.Number}
if a.Verdict != VerdictApprove && a.Verdict != VerdictChanges {
return nil, fmt.Errorf("verdict is approve or changes, not %q; nothing was reviewed", a.Verdict)
}
if a.Verdict == VerdictChanges && strings.TrimSpace(a.Findings) == "" {
return nil, errors.New("a review that asks for changes says them in findings; nothing was reviewed")
}
held, err := k.Store.Get(ctx, key)
if err != nil {
return nil, fmt.Errorf("the knowledge base cannot be read (%v); nothing was reviewed", err)
}
if held == nil || held.Working().Status != StatusProposed {
status := "not held"
if held != nil {
status = held.Working().Status
}
return nil, fmt.Errorf("%s is %s: only a proposed record is reviewed", key, status)
}
if !proven(caller) {
return nil, fmt.Errorf("the runtime does not prove who calls (hq issue 365), so a review cannot be told from one by %s's "+
"author, and none is taken until it does. Nothing was reviewed", key)
}
w := held.Working().Clone()
if caller == w.Author {
return nil, fmt.Errorf("%s was written by %s, who called this review: a record is reviewed by someone who did not write it. "+
"Nothing was reviewed", key, caller)
}
reviewed := held.CurrentVersion
if held.Pending != nil {
reviewed = held.PendingVersion
}
w.Reviews = append(w.Reviews, Review{By: caller, At: k.now(), Verdict: a.Verdict, Findings: strings.TrimSpace(a.Findings), Version: reviewed})
if a.Verdict == VerdictChanges {
if err := k.Store.Save(ctx, Version{By: caller, At: k.now(), Why: "reviewed: changes asked", Record: w, Insight: held.Pending != nil},
held.Pending == nil, false, held.Revision); err != nil {
return nil, fmt.Errorf("the review could not be kept (%v)", err)
}
k.emit(EventReviewed, w, caller, "changes asked", held.Versions+1)
return &Reviewed{Record: key.String(), Verdict: a.Verdict, Status: StatusProposed,
Says: fmt.Sprintf("%s stays proposed; the findings are kept with version %d for its author to answer with a revision.", key, reviewed)}, nil
}
if w.NeedsOperator {
return k.askOperator(ctx, held, w, caller)
}
k.emit(EventReviewed, w, caller, "approved", held.Versions+1)
r, err := k.accept(ctx, held, w, caller, "accepted after a review by "+caller, "")
if err != nil {
return nil, err
}
return &Reviewed{Record: key.String(), Verdict: a.Verdict, Status: r.Status,
Says: fmt.Sprintf("%s is %s and its clauses are in force.", key, r.Status)}, nil
}
// accept makes a reviewed version the one served: accepted, its clauses in force, every clause and record it
// changes or supersedes moved; then the whole record set is judged. by is who acted; warrant, the operator's word
// it rests on.
func (k *KB) accept(ctx context.Context, held *Entry, w *Record, by, why, warrant string) (*Record, error) {
r := w.Clone()
insight := r.Pending != nil
at := k.now()
if insight {
in := r.Pending.Insight
r.Pending = nil
r.Insights = append(r.Insights, in)
r.Status = held.Current.Status
if r.Kind == KindResearch && contains(ResearchStatuses, r.Effort) && held.Current.Status != StatusSuperseded {
r.Status = r.Effort
}
for i, c := range r.Clauses {
r.Clauses[i].Status = ClauseInForce
if old := held.Current.Clause(c.N); old != nil && (old.Status == ClauseChanged || old.Status == ClauseSuperseded) {
r.Clauses[i].Status, r.Clauses[i].ChangedBy = old.Status, old.ChangedBy
}
}
} else {
r.Status = StatusAccepted
if r.Kind == KindResearch {
r.Status = r.Effort
}
r.AcceptedAt = at
for i := range r.Clauses {
r.Clauses[i].Status = ClauseInForce
}
}
r.Effort = ""
if warrant != "" {
why += "; " + warrant
}
if err := k.Store.Save(ctx, Version{By: by, At: at, Why: why, Record: r, Insight: insight}, true, true, held.Revision); err != nil {
return nil, fmt.Errorf("%s could not be accepted (%v); it stays proposed", r.Key(), err)
}
k.emit(EventAccepted, r, by, why, held.Versions+1)
// What it changes and supersedes, each a new version of that record.
for _, l := range r.Links {
to, ok := l.To.Record()
if !ok || (l.Kind != LinkSupersedes && l.Kind != LinkChanges) || (insight && hasLink(held.Current, l)) {
continue
}
t, err := k.Store.Get(ctx, to)
if err != nil || t == nil {
k.logf("[mesh-knowledge] %s %s %s, which cannot be read now (%v): the whole-set rules will say so", r.Key(), l.Kind, to, err)
continue
}
c := t.Current.Clone()
var what string
switch l.Kind {
case LinkSupersedes:
c.Status = StatusSuperseded
c.Extra = addExtra(c.Extra, "superseded-by", r.Key().String())
for i := range c.Clauses {
c.Clauses[i].Status = ClauseSuperseded
c.Clauses[i].ChangedBy = append(c.Clauses[i].ChangedBy, r.Key().String())
}
what = "superseded by " + r.Key().String()
case LinkChanges:
cl := c.Clause(l.To.Clause)
if cl == nil || cl.Status == ClauseSuperseded {
continue
}
cl.Status = ClauseChanged
from := r.Key().String()
if l.From > 0 {
from += " §" + strconv.Itoa(l.From)
}
cl.ChangedBy = append(cl.ChangedBy, from)
what = fmt.Sprintf("§%d changed by %s", l.To.Clause, from)
}
if err := k.Store.Save(ctx, Version{By: by, At: at, Why: what, Record: c}, true, false, t.Revision); err != nil {
k.logf("[mesh-knowledge] %s could not be marked %s (%v): the whole-set rules will say so", to, what, err)
continue
}
k.emit(EventChanged, c, by, what, t.Versions+1)
}
k.judgeSet(ctx)
return r, nil
}
// judgeSet holds the whole record set to its 4 rules and keeps what fails, which the module's health answers.
func (k *KB) judgeSet(ctx context.Context) []Finding {
all, err := k.Store.All(ctx)
if err != nil {
k.logf("[mesh-knowledge] the whole record set cannot be read to judge it: %v", err)
return nil
}
records := make([]*Record, len(all))
for i, e := range all {
records[i] = e.Current
}
findings := JudgeSet(records)
at := k.now()
for i := range findings {
findings[i].At = at
}
if err := k.Store.SetFindings(ctx, findings); err != nil {
k.logf("[mesh-knowledge] the whole-set findings could not be kept: %v", err)
}
for _, f := range findings {
k.logf("[mesh-knowledge] the whole-set rule %s fails: %s", f.Rule, f.Says)
}
return findings
}
// ---- reservations that were never proposed -------------------------------------------------------------------
// Withdraw withdraws every number reserved longer ago than the settings allow and never proposed: it stays spent,
// with who reserved it and when.
func (k *KB) Withdraw() (int, error) {
ctx := context.Background()
k.mu.Lock()
defer k.mu.Unlock()
bound := DefaultReservedFor
if s := strings.TrimSpace(k.settings().ReservedFor); s != "" {
if d, err := time.ParseDuration(s); err == nil && d > 0 {
bound = d
}
}
all, err := k.Store.All(ctx)
if err != nil {
return 0, err
}
n := 0
for _, e := range all {
if e.Current.Status != StatusReserved || k.now().Sub(e.Current.ReservedAt) < bound {
continue
}
r := e.Current.Clone()
r.Status = StatusWithdrawn
why := fmt.Sprintf("withdrawn: reserved by %s at %s and never proposed", r.ReservedBy, r.ReservedAt.Format(time.RFC3339))
if err := k.Store.Save(ctx, Version{By: "the knowledge base", At: k.now(), Why: why, Record: r}, true, false, e.Revision); err != nil {
return n, err
}
k.emit(EventChanged, r, "the knowledge base", why, e.Versions+1)
n++
}
return n, nil
}