Files
mesh-catalog/modules/mesh-knowledge/cmd/mesh-knowledge/operator.go
T
jschoubben 9cfb5f9edb
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 superseded: a newer head of the same pull request
mesh-knowledge: keep when an owed review is due, say it in the history, and tell the operator once after its last try (hq issue 497)
A failed review's try count was already kept with the version, but not when it was asked again, so a 10-minute
pause read as a review lost to a restart; and after the last try only the history and the health said so.
2026-10-11 19:24:07 +02:00

291 lines
12 KiB
Go

package main
// A record that needs the operator (novox/hq ADR 0290 rule 4, to-be 55 §5): once the knowledge base's own 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 the knowledge base's own 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 record the knowledge base's review approved, and keeps the ask with
// it. The caller holds the write lock. An ask that could not be made or published is taken back, and the review is
// asked again after a pause, counted with the tries before it (tries): the record stays proposed, and is reviewed
// at most MostReviewTries times.
func (k *KB) askOperator(ctx context.Context, held *Entry, w *Record, by string, tries int) (string, error) {
version := held.CurrentVersion
if held.Pending != nil {
version = held.PendingVersion
}
unasked := func(why string) (string, error) {
w.Ask = nil
w.Judging = failedJudging(tries+1, why, k.now())
said := fmt.Sprintf("approved by the knowledge base's review, and %s (%s", why, w.Judging.owedSays())
e, err := k.Store.Get(ctx, w.Key())
if err != nil || e == nil {
return "", fmt.Errorf("%s: %s, and that could not be kept (%v)", w.Key(), said, err)
}
if err := k.Store.Save(ctx, Version{By: ReviewBy, At: k.now(), Why: said, Record: w, Insight: e.Pending != nil},
e.Pending == nil, false, e.Revision); err != nil {
return "", fmt.Errorf("%s: %s, and that could not be kept (%v)", w.Key(), said, err)
}
k.emit(EventReviewed, w, ReviewBy, said, e.Versions+1)
return w.Key().String() + ": " + said, nil
}
if k.Asks == nil {
return unasked("the operator cannot be asked from here")
}
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 unasked(fmt.Sprintf("the ask could not be made (%v)", 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 "", fmt.Errorf("%s: the review could not be kept (%v); the operator was not asked", w.Key(), 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 unasked(fmt.Sprintf("the operator could not be asked (%v)", err))
}
return 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 the knowledge base's 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))
}
}
}