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.
274 lines
11 KiB
Go
274 lines
11 KiB
Go
package main
|
|
|
|
// A record that needs the operator (novox/hq ADR 0290 rule 4, to-be 55 §5): once an independent review approves
|
|
// it, the operator is asked on the router with Approve and Decline, both at the level approve, so only a channel
|
|
// that proves who answered can carry either (ADR 0259, as claude-code's proposals ask it, ADR 0274). Each option
|
|
// binds the act it authorises: this record, this version's digest. The knowledge base accepts the record only on
|
|
// the router's warrant for Approve, read back from the router's own record of the ask (which only the router
|
|
// writes) and held to the ask composed again from the record: the same digest, an option it offered, at its
|
|
// level, in time, and the act the option bound. A record changed after it was asked finds the digests differ, and
|
|
// nothing is accepted. The warrant is kept with the record.
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
)
|
|
|
|
// Module is this module's name: the one the bus grants its asks under.
|
|
const Module = "mesh-knowledge"
|
|
|
|
// The two answers, both at the level approve.
|
|
const (
|
|
OptionApprove = "approve"
|
|
OptionDecline = "decline"
|
|
)
|
|
|
|
// AskLasts is how long the operator has to answer: a day less a margin, under the router's bound for an approval.
|
|
const AskLasts = asks.ApproveLasts - 10*time.Minute
|
|
|
|
// The states of an operator's ask, as the record keeps them.
|
|
const (
|
|
AskWaiting = "waiting for the operator's answer"
|
|
AskApproved = "approved by the operator"
|
|
AskDeclined = "declined by the operator"
|
|
)
|
|
|
|
// AskBus is the operator channel as this module reaches it: publish an ask under its own name, and read the
|
|
// router's record of one of its asks.
|
|
type AskBus interface {
|
|
Publish(subject string, body any, id string) error
|
|
Record(id string) (*stdio.StateEntry, error)
|
|
}
|
|
|
|
type sdkAskBus struct{}
|
|
|
|
func (sdkAskBus) Publish(subject string, body any, id string) error {
|
|
return stdio.SeatPublish(subject, body, id)
|
|
}
|
|
func (sdkAskBus) Record(id string) (*stdio.StateEntry, error) { return stdio.SeatRecord("", id) }
|
|
|
|
// act is what an answer binds: this record's version, by its digest, and the answer.
|
|
func act(r *Record, a *OperatorAsk, option string) asks.Act {
|
|
return asks.Act{"verb": "knowledge-base.accept", "option": option, "ask": a.ID, "kind": r.Kind,
|
|
"number": strconv.Itoa(r.Number), "version": strconv.Itoa(a.Version), "digest": r.Digest()}
|
|
}
|
|
|
|
var (
|
|
unshownPath = regexp.MustCompile(`[/\\@~$]|\d+\.\d+\.\d+|[A-Za-z0-9+/=_]{24,}|(?i)\b[a-z0-9-]+\.[a-z]{2,12}\b`)
|
|
)
|
|
|
|
// shown is a line the operator's phone may be shown: no path, address, host-like name or long unbroken word —
|
|
// narrower than the router's own rule, so the ask is not refused for its words.
|
|
func shown(line string) bool { return !unshownPath.MatchString(line) }
|
|
|
|
// fingerprint is a digest as the operator is shown it: its first 24 hexadecimal digits in groups of four.
|
|
func fingerprint(digest string) string {
|
|
h := strings.TrimPrefix(digest, "sha256:")
|
|
if len(h) < 24 {
|
|
return h
|
|
}
|
|
var g []string
|
|
for i := 0; i < 24; i += 4 {
|
|
g = append(g, h[i:i+4])
|
|
}
|
|
return strings.Join(g, " ")
|
|
}
|
|
|
|
// composeAsk is the ask for a record, composed from the record and its ask alone: asked once, and composed
|
|
// again to hold the warrant to it.
|
|
func composeAsk(r *Record, a *OperatorAsk) asks.Ask {
|
|
name := r.Key().String()
|
|
headline := "Accept " + name + "?"
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "%s was proposed by %s and approved by an independent review. Its author says it needs your approval before it holds.", name, r.Author)
|
|
if shown(r.Title) {
|
|
fmt.Fprintf(&b, "\n\nIts title: %s.", strings.TrimSuffix(r.Title, "."))
|
|
}
|
|
if len(r.Clauses) > 0 {
|
|
fmt.Fprintf(&b, "\n\nIts %d clause(s):", len(r.Clauses))
|
|
size, hidden := 0, 0
|
|
for _, c := range r.Clauses {
|
|
lead := c.Lead()
|
|
if !shown(lead) || size+len(lead) > 1200 {
|
|
hidden++
|
|
continue
|
|
}
|
|
size += len(lead)
|
|
fmt.Fprintf(&b, "\n%d. %s", c.N, lead)
|
|
}
|
|
if hidden > 0 {
|
|
fmt.Fprintf(&b, "\n(%d lead(s) not shown here)", hidden)
|
|
}
|
|
}
|
|
fmt.Fprintf(&b, "\n\nRead it whole with the knowledge base's get, number %d; its fingerprint is %s.", r.Number, fingerprint(r.Digest()))
|
|
out := asks.Ask{ID: a.ID, Headline: headline, Explanation: b.String(), Who: asks.Operator,
|
|
Expires: a.At.Add(AskLasts), OnExpiry: "the record stays proposed; nothing of it is in force",
|
|
About: "knowledge." + r.Kind + "." + strconv.Itoa(r.Number)}
|
|
for _, o := range []struct{ id, label, does string }{
|
|
{OptionApprove, "Approve", "the record is accepted and its clauses are in force"},
|
|
{OptionDecline, "Decline", "the record stays proposed; nothing of it is in force"},
|
|
} {
|
|
binds, _ := asks.ActDigest(act(r, a, o.id))
|
|
out.Options = append(out.Options, asks.Option{ID: o.id, Label: o.label, Does: o.does, Level: asks.Approve, Binds: binds})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func newAskID(r *Record) string {
|
|
var b [4]byte
|
|
_, _ = rand.Read(b[:])
|
|
return fmt.Sprintf("kb-%s-%d-%s", r.Kind, r.Number, hex.EncodeToString(b[:]))
|
|
}
|
|
|
|
// askOperator asks the operator to approve a reviewed record, and keeps the ask with it.
|
|
func (k *KB) askOperator(ctx context.Context, held *Entry, w *Record, by string) (*Reviewed, error) {
|
|
if k.Asks == nil {
|
|
return nil, fmt.Errorf("%s needs the operator, and the operator cannot be asked from here; it stays proposed", w.Key())
|
|
}
|
|
version := held.CurrentVersion
|
|
if held.Pending != nil {
|
|
version = held.PendingVersion
|
|
}
|
|
oa := &OperatorAsk{ID: newAskID(w), At: k.now(), Version: version, Said: AskWaiting}
|
|
ask := composeAsk(w, oa)
|
|
if err := ask.Check(k.now()); err != nil {
|
|
return nil, fmt.Errorf("the ask for %s could not be made (%v); it stays proposed", w.Key(), err)
|
|
}
|
|
w.Ask = oa
|
|
if err := k.Store.Save(ctx, Version{By: by, At: k.now(), Why: "reviewed: approved; the operator is 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); the operator was not asked", err)
|
|
}
|
|
k.emit(EventReviewed, w, by, "approved; the operator is asked", held.Versions+1)
|
|
if err := k.Asks.Publish(asks.AskSubject(Module), ask, ask.ID); err != nil {
|
|
k.logf("[mesh-knowledge] the ask for %s could not be published: %v", w.Key(), err)
|
|
return nil, fmt.Errorf("the review is kept, and the operator could not be asked (%v): %s stays proposed; review it again to ask again", err, w.Key())
|
|
}
|
|
return &Reviewed{Record: w.Key().String(), Verdict: VerdictApprove, Status: StatusProposed,
|
|
Says: fmt.Sprintf("%s needs the operator: asked on a channel that proves who answers (%q). It is accepted only on the operator's Approve; until then nothing of it is in force.", w.Key(), ask.Headline)}, nil
|
|
}
|
|
|
|
// routerRecord is the router's record of one ask, as far as this module reads it.
|
|
type routerRecord struct {
|
|
State string `json:"state"`
|
|
Ask *asks.Ask `json:"ask"`
|
|
Warrant *asks.Warrant `json:"warrant"`
|
|
}
|
|
|
|
// Reconcile decides every record waiting for the operator whose ask the router has closed: accepted on a warrant
|
|
// for Approve that holds, left proposed on Decline, an expiry or a warrant that does not hold. It answers a line
|
|
// for each decision.
|
|
func (k *KB) Reconcile() ([]string, error) {
|
|
if k.Asks == nil {
|
|
return nil, nil
|
|
}
|
|
ctx := context.Background()
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
all, err := k.Store.All(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var said []string
|
|
for _, e := range all {
|
|
w := e.Working()
|
|
if w.Status != StatusProposed || w.Ask == nil || w.Ask.Said != AskWaiting {
|
|
continue
|
|
}
|
|
entry, err := k.Asks.Record(w.Ask.ID)
|
|
if err != nil || entry == nil {
|
|
if err == nil && k.now().After(w.Ask.At.Add(AskLasts+time.Hour)) {
|
|
said = append(said, k.endAsk(ctx, e, w, "ended: the router keeps no record of its ask (refused before it was taken, or forgotten)"))
|
|
}
|
|
continue
|
|
}
|
|
var rec routerRecord
|
|
if json.Unmarshal(entry.Value, &rec) != nil || rec.State == "open" || rec.Warrant == nil {
|
|
continue
|
|
}
|
|
said = append(said, k.decide(ctx, e, w, *rec.Warrant))
|
|
}
|
|
return said, nil
|
|
}
|
|
|
|
// endAsk records an ask ended without the operator's choice: the record stays proposed.
|
|
func (k *KB) endAsk(ctx context.Context, e *Entry, w *Record, why string) string {
|
|
w = w.Clone()
|
|
w.Ask.Said = why
|
|
if err := k.Store.Save(ctx, Version{By: "the knowledge base", At: k.now(), Why: "the operator's ask " + why, Record: w, Insight: e.Pending != nil},
|
|
e.Pending == nil, false, e.Revision); err != nil {
|
|
return fmt.Sprintf("%s: the end of its ask could not be kept (%v)", w.Key(), err)
|
|
}
|
|
k.emit(EventReviewed, w, "the knowledge base", "the operator's ask "+why, e.Versions+1)
|
|
return fmt.Sprintf("%s: the operator's ask %s; it stays proposed", w.Key(), why)
|
|
}
|
|
|
|
// decide holds one closed ask's warrant to the record, and accepts the record when it is the operator's Approve.
|
|
func (k *KB) decide(ctx context.Context, e *Entry, w *Record, warrant asks.Warrant) string {
|
|
if warrant.Outcome != asks.OutcomeChosen {
|
|
why := "ended " + string(warrant.Outcome)
|
|
if warrant.Words != "" {
|
|
why += " (" + warrant.Words + ")"
|
|
}
|
|
return k.endAsk(ctx, e, w, why)
|
|
}
|
|
ask := composeAsk(w, w.Ask)
|
|
option, err := warrant.For(Module, ask)
|
|
if err == nil {
|
|
err = option.Performs(act(w, w.Ask, option.ID))
|
|
}
|
|
if err != nil {
|
|
k.logf("[mesh-knowledge] REFUSED the answer to the ask for %s: %v", w.Key(), err)
|
|
return k.endAsk(ctx, e, w, "was answered, and the answer does not hold here: "+err.Error())
|
|
}
|
|
switch option.ID {
|
|
case OptionApprove:
|
|
w = w.Clone()
|
|
w.Ask.Said, w.Ask.Warrant = AskApproved, warrant.Says()
|
|
r, err := k.accept(ctx, e, w, "the operator", "accepted on the operator's Approve after an independent review", "warrant: "+warrant.Says())
|
|
if err != nil {
|
|
return err.Error()
|
|
}
|
|
return fmt.Sprintf("%s: %s on the operator's warrant (%s)", r.Key(), r.Status, warrant.Says())
|
|
case OptionDecline:
|
|
w = w.Clone()
|
|
w.Ask.Warrant = warrant.Says()
|
|
return k.endAsk(ctx, e, w, "declined by the operator")
|
|
}
|
|
return k.endAsk(ctx, e, w, "was answered with "+option.ID+", which it does not offer")
|
|
}
|
|
|
|
// HeardEnd takes the router's word that one of this module's asks ended without a choice: nothing is accepted on
|
|
// such a word, so it needs no proof beyond its subject. A choice is only ever taken from the router's record.
|
|
func (k *KB) HeardEnd(w asks.Warrant) {
|
|
if w.Asker != Module || w.Outcome == asks.OutcomeChosen {
|
|
if _, err := k.Reconcile(); err != nil {
|
|
k.logf("[mesh-knowledge] reading the router's answers: %v", err)
|
|
}
|
|
return
|
|
}
|
|
ctx := context.Background()
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
all, err := k.Store.All(ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, e := range all {
|
|
r := e.Working()
|
|
if r.Ask != nil && r.Ask.ID == w.Ask && r.Ask.Said == AskWaiting {
|
|
k.logf("[mesh-knowledge] %s", k.decide(ctx, e, r, w))
|
|
}
|
|
}
|
|
}
|