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
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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user