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…
546 lines
20 KiB
Go
546 lines
20 KiB
Go
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).
|
|
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
|
|
|
|
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
|
|
}
|
|
|
|
// ---- 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
|
|
}
|