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 the knowledge base's own judgement // (review.go), 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" stdio "git.novox.be/novox/mesh-sdk/go" ) // Unproven is who acted, as recorded, where the runtime proves no caller (hq issue 365): the SDK's word. The caller // is never taken from an argument. const Unproven = stdio.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). Resolved is the line its message says // once it is cleared: the channel keeps the one given when it is raised (novox/hq issue 497); "" for none. type Notifier func(key, summary, headline, explanation, resolved string) error // Clearer tells the operator's channel that what it was told under key is over, in one resolved line. type Clearer func(key, resolved string) error // KB is the knowledge base. type KB struct { Store Store Records Records Reserve Reserver Judge Judgement Asks AskBus Notify Notifier Clear Clearer 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 stuckMu sync.Mutex stuck map[Key]*stuckReview // kick wakes the background work when a proposal waits for its review; nil where nothing runs in the // background (the tests take each step themselves). kick chan struct{} 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. Every version proposed // waits for the knowledge base's own review (review.go), which the background work takes at once. A proposal is // revised as new versions by its author until it is accepted; an accepted record is changed only by a progressive // insight (Insight), which is reviewed like a proposal 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 proven(r.ReservedBy) && caller != r.ReservedBy { return nil, fmt.Errorf("%s was reserved by %s: only who reserved a number proposes under it. Nothing was proposed", key, r.ReservedBy) } 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. if !proven(caller) || caller != w.Author { return nil, fmt.Errorf("%s is proposed by %s: only its author, proven by the runtime, revises it (this call is by %s). "+ "Nothing was proposed", key, w.Author, caller) } r.Author, r.Reviews, r.ReservedBy, r.ReservedAt, r.ReservedFor = w.Author, w.Reviews, w.ReservedBy, w.ReservedAt, w.ReservedFor r.Writers = w.Writers 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 held.Pending != nil && (!proven(caller) || caller != held.Pending.Author) { return nil, fmt.Errorf("%s has a progressive insight by %s waiting for its review: only its author, proven by the "+ "runtime, revises it (this call is by %s). Nothing was proposed", key, held.Pending.Author, caller) } 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.Writers = append([]string{}, held.Current.Writers...) if held.Current.Author != "" && !contains(r.Writers, held.Current.Author) { r.Writers = append(r.Writers, held.Current.Author) } 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 && held != nil && !held.Current.ReservedAt.IsZero() && r.Date < held.Current.ReservedAt.Format("2006-01-02") { // A new record is of its own day: a date before its reservation would put it under rules older records keep. refusals = append(refusals, Refusal{Rule: "date", Says: fmt.Sprintf("a new record is dated from the day its number was reserved, %s, not %s", held.Current.ReservedAt.Format("2006-01-02"), r.Date)}) } if !contains(r.Writers, caller) { r.Writers = append(r.Writers, caller) } // Every version proposed is reviewed by the knowledge base itself, once; its earlier reviews stay with the // versions they reviewed. r.Judging = judgingOf(r) 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) k.wake() if r.Judging.State == JudgingUnproven { return &Proposed{Record: key.String(), Version: n, Status: StatusProposed, Says: fmt.Sprintf("%s is proposed as version %d, and %s. Nothing of it is in force.", key, n, r.Judging.Reason)}, nil } says := fmt.Sprintf("%s is proposed as version %d. The knowledge base reviews it now, in a judgement of its own; nothing of it "+ "is in force until that judgement 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 knowledge base reviews it now; the accepted "+ "version is served until that review approves it", key, n) } says += ". get shows the review's verdict and findings once it is made; on changes, you revise it by proposing it again" return &Proposed{Record: key.String(), Version: n, Status: StatusProposed, Says: says + "."}, nil } // ---- acceptance ------------------------------------------------------------------------------------------ // 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 } // findingPrefix keys the notice of one whole-set finding: the prefix, the rule, and the records it names. const findingPrefix = "knowledge.finding." func findingKey(f Finding) string { names := make([]string, len(f.Records)) for i, r := range f.Records { names[i] = strings.ReplaceAll(strings.ToLower(r), " ", "-") } return findingPrefix + f.Rule + "." + strings.Join(names, ".") } // TellFindings, called by the background work and never under the write lock, tells the operator, once, each finding of the rules that judge the whole record set, as a warning // naming the rule and the records, and clears the notice once the rule holds for them again. A finding is a fault // of the records, which their authors repair; it is not the module failing, so it leaves the module's health as it // is and never holds the module's own rollouts (the operator's decision of 2026-10-12 on novox/hq issue 504, as // issue 508 decided for a proposal never reviewed). A notice that cannot be sent is not kept as told, and is sent // again at the next step. func (k *KB) TellFindings() { if k.Notify == nil || k.Clear == nil { return } ctx := context.Background() findings, err := k.Store.Findings(ctx) if err != nil { k.logf("[mesh-knowledge] the whole-set findings cannot be read to tell them: %v", err) return } now := map[string]bool{} for _, f := range findings { key := findingKey(f) now[key] = true first, err := k.Store.Told(ctx, key) if err != nil || !first { continue } records := strings.Join(f.Records, ", ") summary := fmt.Sprintf("The rule %s, which judges the whole record set, fails: %s.", f.Rule, f.Says) explanation := "The knowledge base works; its records break a rule. The acceptance stands. The author of " + records + " repairs it: a decision cited by nothing names its home in a progressive insight (propose with homes and insight), " + "or a record that cites it is accepted. get and links show the record." resolved := fmt.Sprintf("The rule %s holds again for %s", f.Rule, records) if err := k.Notify(key, summary, "A rule of the record set fails: "+records, explanation, resolved); err != nil { k.logf("[mesh-knowledge] the operator could not be told that %s fails for %s: %v; told again at the next step", f.Rule, records, err) if uerr := k.Store.Untold(ctx, key); uerr != nil { k.logf("[mesh-knowledge] and that it was not told could not be kept: %v", uerr) } } } told, err := k.Store.ToldOf(ctx, findingPrefix) if err != nil { k.logf("[mesh-knowledge] the notices of whole-set findings cannot be read: %v", err) return } for _, key := range told { if now[key] { continue } if err := k.Clear(key, ""); err != nil { k.logf("[mesh-knowledge] the notice %s could not be cleared: %v; it is cleared again at the next step", key, err) continue } if err := k.Store.Untold(ctx, key); err != nil { k.logf("[mesh-knowledge] the notice %s was cleared, and that could not be kept: %v", key, err) } } } // ---- 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 }