Join item 1: the catalogue's modules take the router's ask from operator-channel's contract and client
mesh-sdk holds neither asks nor asker, nor the vault's answer types, and mesh-delivery's sdk/views is mesh-sdk's go/views (a1-progress.md, the rewrite map for the join): thirteen modules switch to the operatorchannel contract and client, mesh-sdk's channel package and meshvault, with go.mod requiring each at the group pin (nats: meshbroker too). The gate holds the checks mesh-sdk dropped (an ask's id is its key's asking, at least one option), with a row that fails without them; claude-code refuses a router record whose times do not parse, as its JSON decode did, with a row. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
|
||||
)
|
||||
|
||||
// novox/hq issue 639: the operator would not read the ask for ADR 0343 — thirty plain lines, every clause one per
|
||||
@@ -44,13 +44,13 @@ func TestADecisionAskReadsAtAGlance(t *testing.T) {
|
||||
r := adr0343()
|
||||
at := time.Date(2026, 10, 13, 14, 5, 0, 0, time.UTC)
|
||||
a := composeAsk(r, &OperatorAsk{ID: "kb-decision-343-0d1e2f3a", At: at, Version: 12, Form: AskGlance})
|
||||
if err := a.Check(at); err != nil {
|
||||
if err := occ.Check(a, at); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if a.Headline != "Your agents' memory follows you to every machine" {
|
||||
t.Errorf("the headline is not what it changes for the operator: %q", a.Headline)
|
||||
}
|
||||
if err := asks.Glance(a.Explanation); err != nil {
|
||||
if err := occ.Glance(a.Explanation); err != nil {
|
||||
t.Errorf("%v:\n%s", err, a.Explanation)
|
||||
}
|
||||
if n := strings.Count(a.Explanation, "\n• "); n != 3 || !strings.HasPrefix(a.Explanation, "What an agent learns") {
|
||||
@@ -69,13 +69,13 @@ func TestADecisionAskReadsAtAGlance(t *testing.T) {
|
||||
}
|
||||
// The ask made before the brief is composed again as it was, so its warrant still holds after the change.
|
||||
old := composeAsk(r, &OperatorAsk{ID: "kb-decision-343-0d1e2f3a", At: at, Version: 12})
|
||||
if !strings.Contains(old.Explanation, "Its 13 clause(s):") || old.Headline != "Accept ADR 0343?" || old.Details != "" || old.Digest() == a.Digest() {
|
||||
if !strings.Contains(old.Explanation, "Its 13 clause(s):") || old.Headline != "Accept ADR 0343?" || old.Details != "" || occ.Digest(old) == occ.Digest(a) {
|
||||
t.Errorf("an ask made before the brief is not composed as it was asked:\n%s", old.Explanation)
|
||||
}
|
||||
// Without a brief (a record proposed before the rule), the glance is the title, and no clause.
|
||||
r.Brief = nil
|
||||
a = composeAsk(r, &OperatorAsk{ID: "kb-decision-343-0d1e2f3a", At: at, Version: 12, Form: AskGlance})
|
||||
if a.Headline != "Approve ADR 0343?" || asks.Glance(a.Explanation) != nil || strings.Contains(a.Explanation, "Only the memory folders") {
|
||||
if a.Headline != "Approve ADR 0343?" || occ.Glance(a.Explanation) != nil || strings.Contains(a.Explanation, "Only the memory folders") {
|
||||
t.Errorf("without a brief: %q\n%s", a.Headline, a.Explanation)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
)
|
||||
|
||||
// Unproven is who acted, as recorded, where the runtime proves no caller (hq issue 365): the SDK's word. The caller
|
||||
@@ -816,7 +816,7 @@ func (k *KB) WithdrawRecord(caller string, key Key, why string) (*Withdrawn, err
|
||||
k.emit(EventChanged, r, caller, full, e.Versions+1)
|
||||
if waiting && k.Asks != nil {
|
||||
id := cur.Ask.ID
|
||||
if err := k.Asks.Publish(asks.CancelSubject(Module), map[string]string{"id": id}, "cancel."+id); err != nil {
|
||||
if err := k.Asks.Publish(oc.AcceptSubjectCancel(Module), map[string]string{"id": id}, "cancel."+id); err != nil {
|
||||
k.logf("[mesh-knowledge] the ask %s for %s is taken back here, and the router could not be told (%v): it expires there, "+
|
||||
"and no answer to it is acted on", id, key, err)
|
||||
}
|
||||
|
||||
@@ -22,8 +22,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
)
|
||||
|
||||
const bundleVar = "MESH_KNOWLEDGE_TEST_BUNDLE"
|
||||
@@ -326,7 +327,7 @@ func (j *judge) judge(question string, schema map[string]any, model string) (jso
|
||||
// the cancels taken back on its subject.
|
||||
type router struct {
|
||||
mu sync.Mutex
|
||||
asked []asks.Ask
|
||||
asked []oc.Ask
|
||||
records map[string]routerRecord
|
||||
cancelled []string
|
||||
}
|
||||
@@ -337,13 +338,13 @@ func (r *router) Publish(subject string, body any, id string) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
switch subject {
|
||||
case asks.AskSubject(Module):
|
||||
a := body.(asks.Ask)
|
||||
case oc.AcceptSubjectAsk(Module):
|
||||
a := body.(oc.Ask)
|
||||
r.asked = append(r.asked, a)
|
||||
cp := a
|
||||
r.records[a.ID] = routerRecord{State: "open", Ask: &cp}
|
||||
return nil
|
||||
case asks.CancelSubject(Module):
|
||||
case oc.AcceptSubjectCancel(Module):
|
||||
m := body.(map[string]string)
|
||||
r.cancelled = append(r.cancelled, m["id"])
|
||||
return nil
|
||||
@@ -363,14 +364,14 @@ func (r *router) Record(id string) (*stdio.StateEntry, error) {
|
||||
}
|
||||
|
||||
// answer closes an ask as the router does when the operator taps an option on a channel that proves who answered.
|
||||
func (r *router) answer(id, option string, at time.Time) asks.Warrant {
|
||||
func (r *router) answer(id, option string, at time.Time) oc.Warrant {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
rec := r.records[id]
|
||||
o, _ := rec.Ask.Option(option)
|
||||
w := asks.Warrant{Ask: id, Asker: Module, About: rec.Ask.About, Outcome: asks.OutcomeChosen, Option: o.ID, Label: o.Label,
|
||||
Level: o.Level, By: &asks.Person{Who: asks.Operator, Kind: "telegram", Identity: "1", Verified: "user id verified"},
|
||||
Channel: "telegram", At: at, AskDigest: rec.Ask.Digest()}
|
||||
o, _ := occ.OptionOf(*rec.Ask, option)
|
||||
w := oc.Warrant{Ask: id, Asker: Module, About: rec.Ask.About, Outcome: occ.OutcomeChosen, Option: o.ID, Label: o.Label,
|
||||
Level: o.Level, By: &oc.Person{Who: occ.Operator, Kind: "telegram", Identity: "1", Verified: "user id verified"},
|
||||
Channel: "telegram", At: occ.FormatTime(at), AskDigest: occ.Digest(*rec.Ask)}
|
||||
rec.State, rec.Warrant = "closed", &w
|
||||
r.records[id] = rec
|
||||
return w
|
||||
|
||||
@@ -29,8 +29,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
)
|
||||
|
||||
// Seat is the role this module holds, once for the mesh.
|
||||
@@ -122,8 +122,8 @@ func callerOf(args map[string]any) string { return stdio.CallerOf(args) }
|
||||
// hear subscribes to the router's word on this module's asks, asked again until the runtime takes it.
|
||||
func hear(k *KB) {
|
||||
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
|
||||
err := stdio.Subscribe(asks.Seat+".decided."+Module, func(e stdio.Envelope) error {
|
||||
var w asks.Warrant
|
||||
err := stdio.Subscribe(oc.Seat+".decided."+Module, func(e stdio.Envelope) error {
|
||||
var w oc.Warrant
|
||||
if json.Unmarshal(e.Body, &w) == nil {
|
||||
k.HeardEnd(w)
|
||||
}
|
||||
|
||||
@@ -20,8 +20,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
|
||||
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.
|
||||
@@ -34,7 +35,7 @@ const (
|
||||
)
|
||||
|
||||
// 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
|
||||
const AskLasts = occ.ApproveLasts - 10*time.Minute
|
||||
|
||||
// The states of an operator's ask, as the record keeps them.
|
||||
const (
|
||||
@@ -58,8 +59,8 @@ func (sdkAskBus) Publish(subject string, body any, id string) error {
|
||||
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,
|
||||
func act(r *Record, a *OperatorAsk, option string) occ.Act {
|
||||
return occ.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()}
|
||||
}
|
||||
|
||||
@@ -90,7 +91,7 @@ const AskGlance = 1
|
||||
// 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 — in the form it was asked in, so an ask made before a form changed is still
|
||||
// held to the words the operator was shown.
|
||||
func composeAsk(r *Record, a *OperatorAsk) asks.Ask {
|
||||
func composeAsk(r *Record, a *OperatorAsk) oc.Ask {
|
||||
if a.Form == AskGlance {
|
||||
return glanceAsk(r, a)
|
||||
}
|
||||
@@ -101,10 +102,10 @@ func composeAsk(r *Record, a *OperatorAsk) asks.Ask {
|
||||
// points; who proposed it, its review, its fingerprint and how to read every clause are its Details, which the
|
||||
// router notes after the answers. A record without a brief (one proposed before the rule) is asked with its
|
||||
// title as its summary. The clauses are never listed.
|
||||
func glanceAsk(r *Record, a *OperatorAsk) asks.Ask {
|
||||
func glanceAsk(r *Record, a *OperatorAsk) oc.Ask {
|
||||
name := r.Key().String()
|
||||
headline, explanation := "Approve "+name+"?", name+" needs your approval before it holds."
|
||||
if t := strings.TrimSuffix(strings.TrimSpace(r.Title), "."); shown(t) && len([]rune(t)) < asks.SummaryLength {
|
||||
if t := strings.TrimSuffix(strings.TrimSpace(r.Title), "."); shown(t) && len([]rune(t)) < occ.SummaryLength {
|
||||
explanation = t + "."
|
||||
}
|
||||
if r.Brief != nil && len(briefProblems(r.Brief)) == 0 {
|
||||
@@ -120,12 +121,12 @@ func glanceAsk(r *Record, a *OperatorAsk) asks.Ask {
|
||||
}
|
||||
details := fmt.Sprintf("%s, proposed by %s; the knowledge base's own review approved it. Fingerprint %s.\n%s",
|
||||
name, author, fingerprint(r.Digest()), clauses)
|
||||
return withOptions(r, a, asks.Ask{ID: a.ID, Headline: headline, Explanation: explanation, Details: details})
|
||||
return withOptions(r, a, oc.Ask{ID: a.ID, Headline: headline, Explanation: explanation, Details: details})
|
||||
}
|
||||
|
||||
// listedAsk is the ask as it was composed before the brief: every clause's lead listed. Composed only to hold
|
||||
// the warrant of an ask made then.
|
||||
func listedAsk(r *Record, a *OperatorAsk) asks.Ask {
|
||||
func listedAsk(r *Record, a *OperatorAsk) oc.Ask {
|
||||
name := r.Key().String()
|
||||
headline := "Accept " + name + "?"
|
||||
var b strings.Builder
|
||||
@@ -150,21 +151,21 @@ func listedAsk(r *Record, a *OperatorAsk) asks.Ask {
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&b, "\n\nRead it whole with the knowledge base's get, number %d; its fingerprint is %s.", r.Number, fingerprint(r.Digest()))
|
||||
return withOptions(r, a, asks.Ask{ID: a.ID, Headline: headline, Explanation: b.String()})
|
||||
return withOptions(r, a, oc.Ask{ID: a.ID, Headline: headline, Explanation: b.String()})
|
||||
}
|
||||
|
||||
// withOptions is an ask with what every form of it carries: who answers, its expiry, what it is about, and its
|
||||
// two options, each binding its act.
|
||||
func withOptions(r *Record, a *OperatorAsk, out asks.Ask) asks.Ask {
|
||||
out.Who, out.Expires = asks.Operator, a.At.Add(AskLasts)
|
||||
func withOptions(r *Record, a *OperatorAsk, out oc.Ask) oc.Ask {
|
||||
out.Who, out.Expires = occ.Operator, occ.FormatTime(a.At.Add(AskLasts))
|
||||
out.OnExpiry = "the record stays proposed; nothing of it is in force"
|
||||
out.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})
|
||||
binds, _ := occ.ActDigest(act(r, a, o.id))
|
||||
out.Options = append(out.Options, oc.Option{ID: o.id, Label: o.label, Does: o.does, Level: occ.Approve, Binds: binds})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -204,7 +205,7 @@ func (k *KB) askOperator(ctx context.Context, held *Entry, w *Record, by string,
|
||||
}
|
||||
oa := &OperatorAsk{ID: newAskID(w), At: k.now(), Version: version, Said: AskWaiting, Form: AskGlance}
|
||||
ask := composeAsk(w, oa)
|
||||
if err := ask.Check(k.now()); err != nil {
|
||||
if err := occ.Check(ask, k.now()); err != nil {
|
||||
return unasked(fmt.Sprintf("the ask could not be made (%v)", err))
|
||||
}
|
||||
w.Ask = oa
|
||||
@@ -213,7 +214,7 @@ func (k *KB) askOperator(ctx context.Context, held *Entry, w *Record, by string,
|
||||
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 {
|
||||
if err := k.Asks.Publish(oc.AcceptSubjectAsk(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))
|
||||
}
|
||||
@@ -224,9 +225,9 @@ func (k *KB) askOperator(ctx context.Context, held *Entry, w *Record, by string,
|
||||
type routerRecord struct {
|
||||
State string `json:"state"`
|
||||
// Held is why no channel can carry the ask yet, while its state is held (ADR 0397).
|
||||
Held string `json:"held"`
|
||||
Ask *asks.Ask `json:"ask"`
|
||||
Warrant *asks.Warrant `json:"warrant"`
|
||||
Held string `json:"held"`
|
||||
Ask *oc.Ask `json:"ask"`
|
||||
Warrant *oc.Warrant `json:"warrant"`
|
||||
}
|
||||
|
||||
// Reconcile decides every record waiting for the operator whose ask the router has closed: accepted on a warrant
|
||||
@@ -319,10 +320,10 @@ func (k *KB) endAsk(ctx context.Context, e *Entry, w *Record, why string) string
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func (k *KB) decide(ctx context.Context, e *Entry, w *Record, warrant oc.Warrant) string {
|
||||
w = w.Clone()
|
||||
w.Ask.Held = "" // an ask that ended is held no longer
|
||||
if warrant.Outcome != asks.OutcomeChosen {
|
||||
if warrant.Outcome != occ.OutcomeChosen {
|
||||
why := "ended " + string(warrant.Outcome)
|
||||
if warrant.Words != "" {
|
||||
why += " (" + warrant.Words + ")"
|
||||
@@ -330,9 +331,9 @@ func (k *KB) decide(ctx context.Context, e *Entry, w *Record, warrant asks.Warra
|
||||
return k.endAsk(ctx, e, w, why)
|
||||
}
|
||||
ask := composeAsk(w, w.Ask)
|
||||
option, err := warrant.For(Module, ask)
|
||||
option, err := occ.For(warrant, Module, ask)
|
||||
if err == nil {
|
||||
err = option.Performs(act(w, w.Ask, option.ID))
|
||||
err = occ.Performs(option, 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)
|
||||
@@ -341,15 +342,15 @@ func (k *KB) decide(ctx context.Context, e *Entry, w *Record, warrant asks.Warra
|
||||
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())
|
||||
w.Ask.Said, w.Ask.Warrant = AskApproved, occ.Says(warrant)
|
||||
r, err := k.accept(ctx, e, w, "the operator", "accepted on the operator's Approve after the knowledge base's review", "warrant: "+occ.Says(warrant))
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return fmt.Sprintf("%s: %s on the operator's warrant (%s)", r.Key(), r.Status, warrant.Says())
|
||||
return fmt.Sprintf("%s: %s on the operator's warrant (%s)", r.Key(), r.Status, occ.Says(warrant))
|
||||
case OptionDecline:
|
||||
w = w.Clone()
|
||||
w.Ask.Warrant = warrant.Says()
|
||||
w.Ask.Warrant = occ.Says(warrant)
|
||||
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")
|
||||
@@ -357,8 +358,8 @@ func (k *KB) decide(ctx context.Context, e *Entry, w *Record, warrant asks.Warra
|
||||
|
||||
// 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 {
|
||||
func (k *KB) HeardEnd(w oc.Warrant) {
|
||||
if w.Asker != Module || w.Outcome == occ.OutcomeChosen {
|
||||
if _, err := k.Reconcile(); err != nil {
|
||||
k.logf("[mesh-knowledge] reading the router's answers: %v", err)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
|
||||
)
|
||||
|
||||
// RetractAsked is the status of a retraction asked and not yet answered.
|
||||
@@ -127,7 +128,7 @@ func (k *KB) Retract(caller string, key Key, clauses []int, why string) (*Retrac
|
||||
x := &Retraction{Clauses: sorted, Why: why, By: caller, At: at, Title: cur.Title, Leads: leads,
|
||||
Ask: &OperatorAsk{ID: newRetractAskID(cur), At: at, Version: e.CurrentVersion, Said: AskWaiting, Form: AskGlance}}
|
||||
ask := composeRetractAsk(cur, x)
|
||||
if err := ask.Check(at); err != nil {
|
||||
if err := occ.Check(ask, at); err != nil {
|
||||
return nil, fmt.Errorf("the ask could not be made (%v); nothing was asked", err)
|
||||
}
|
||||
r := cur.Clone()
|
||||
@@ -136,7 +137,7 @@ func (k *KB) Retract(caller string, key Key, clauses []int, why string) (*Retrac
|
||||
if err := k.Store.Save(ctx, Version{By: caller, At: at, Why: what, Record: r}, true, false, e.Revision); err != nil {
|
||||
return nil, fmt.Errorf("%s could not be written (%v); nothing was asked", key, err)
|
||||
}
|
||||
if err := k.Asks.Publish(asks.AskSubject(Module), ask, ask.ID); err != nil {
|
||||
if err := k.Asks.Publish(oc.AcceptSubjectAsk(Module), ask, ask.ID); err != nil {
|
||||
// Taken back: the record says it was not asked, and nothing waits on it.
|
||||
k.logf("[mesh-knowledge] the retraction of %s could not be asked: %v", key, err)
|
||||
if e2, gerr := k.Store.Get(ctx, key); gerr == nil && e2 != nil {
|
||||
@@ -176,7 +177,7 @@ func (k *KB) WithdrawRetraction(caller string, key Key, why string) (*Withdrawn,
|
||||
return nil, err
|
||||
}
|
||||
if k.Asks != nil {
|
||||
if err := k.Asks.Publish(asks.CancelSubject(Module), map[string]string{"id": id}, "cancel."+id); err != nil {
|
||||
if err := k.Asks.Publish(oc.AcceptSubjectCancel(Module), map[string]string{"id": id}, "cancel."+id); err != nil {
|
||||
k.logf("[mesh-knowledge] the ask %s for %s is taken back here, and the router could not be told (%v): it expires there, "+
|
||||
"and no answer to it is acted on", id, key, err)
|
||||
}
|
||||
@@ -201,18 +202,18 @@ func newRetractAskID(r *Record) string {
|
||||
}
|
||||
|
||||
// retractAct is what an answer to a retraction binds: this record, the clauses, why, who asked, and the answer.
|
||||
func retractAct(r *Record, x *Retraction, option string) asks.Act {
|
||||
return asks.Act{"verb": "knowledge-base.retract", "option": option, "ask": x.Ask.ID, "kind": r.Kind,
|
||||
func retractAct(r *Record, x *Retraction, option string) occ.Act {
|
||||
return occ.Act{"verb": "knowledge-base.retract", "option": option, "ask": x.Ask.ID, "kind": r.Kind,
|
||||
"number": strconv.Itoa(r.Number), "clauses": clauseList(x.Clauses), "why": x.Why, "by": x.By}
|
||||
}
|
||||
|
||||
// composeRetractAsk is the ask for a retraction, composed from the retraction alone, so it is the same when it is
|
||||
// composed again to hold the warrant to it.
|
||||
func composeRetractAsk(r *Record, x *Retraction) asks.Ask {
|
||||
func composeRetractAsk(r *Record, x *Retraction) oc.Ask {
|
||||
name := r.Key().String()
|
||||
headline := "Retract " + name + "?"
|
||||
if !x.Whole() {
|
||||
if h := "Retract " + name + " " + clauseList(x.Clauses) + "?"; len([]rune(h)) <= asks.HeadlineLength {
|
||||
if h := "Retract " + name + " " + clauseList(x.Clauses) + "?"; len([]rune(h)) <= occ.HeadlineLength {
|
||||
headline = h
|
||||
} else {
|
||||
headline = fmt.Sprintf("Retract %d clauses of %s?", len(x.Clauses), name)
|
||||
@@ -225,12 +226,12 @@ func composeRetractAsk(r *Record, x *Retraction) asks.Ask {
|
||||
summary := what + " would no longer hold; nothing is deleted, its versions stay in history."
|
||||
var points []string
|
||||
if shown(x.Why) {
|
||||
points = append(points, "Why: "+clip(x.Why, asks.PointLength-6))
|
||||
points = append(points, "Why: "+clip(x.Why, occ.PointLength-6))
|
||||
} else {
|
||||
points = append(points, "Why: read it with the knowledge base's get.")
|
||||
}
|
||||
if t := strings.TrimSuffix(strings.TrimSpace(x.Title), "."); shown(t) {
|
||||
points = append(points, "Record: "+clip(t, asks.PointLength-9))
|
||||
points = append(points, "Record: "+clip(t, occ.PointLength-9))
|
||||
}
|
||||
if !x.Whole() {
|
||||
var leads []string
|
||||
@@ -240,7 +241,7 @@ func composeRetractAsk(r *Record, x *Retraction) asks.Ask {
|
||||
}
|
||||
}
|
||||
if len(leads) > 0 {
|
||||
points = append(points, clip("Clauses: "+strings.Join(leads, "; "), asks.PointLength))
|
||||
points = append(points, clip("Clauses: "+strings.Join(leads, "; "), occ.PointLength))
|
||||
}
|
||||
}
|
||||
explanation := summary
|
||||
@@ -249,15 +250,15 @@ func composeRetractAsk(r *Record, x *Retraction) asks.Ask {
|
||||
}
|
||||
details := fmt.Sprintf("%s, retraction of %s asked by %s: %s\nRead it with the knowledge base's get, number %d.",
|
||||
name, x.What(), x.By, x.Why, r.Number)
|
||||
out := asks.Ask{ID: x.Ask.ID, Headline: headline, Explanation: explanation, Details: details,
|
||||
Who: asks.Operator, Expires: x.Ask.At.Add(AskLasts), OnExpiry: "nothing is retracted; the record holds as it did",
|
||||
out := oc.Ask{ID: x.Ask.ID, Headline: headline, Explanation: explanation, Details: details,
|
||||
Who: occ.Operator, Expires: occ.FormatTime(x.Ask.At.Add(AskLasts)), OnExpiry: "nothing is retracted; the record holds as it did",
|
||||
About: "knowledge." + r.Kind + "." + strconv.Itoa(r.Number)}
|
||||
for _, o := range []struct{ id, label, does string }{
|
||||
{OptionApprove, "Approve", "it is retracted and no longer holds"},
|
||||
{OptionDecline, "Decline", "nothing is retracted; it holds as it did"},
|
||||
} {
|
||||
binds, _ := asks.ActDigest(retractAct(r, x, o.id))
|
||||
out.Options = append(out.Options, asks.Option{ID: o.id, Label: o.label, Does: o.does, Level: asks.Approve, Binds: binds})
|
||||
binds, _ := occ.ActDigest(retractAct(r, x, o.id))
|
||||
out.Options = append(out.Options, oc.Option{ID: o.id, Label: o.label, Does: o.does, Level: occ.Approve, Binds: binds})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -310,30 +311,30 @@ func (k *KB) endRetractionSaid(ctx context.Context, e *Entry, by, why string) st
|
||||
}
|
||||
|
||||
// decideRetraction holds one closed ask's warrant to the retraction, and retracts on the operator's Approve.
|
||||
func (k *KB) decideRetraction(ctx context.Context, e *Entry, warrant asks.Warrant) string {
|
||||
func (k *KB) decideRetraction(ctx context.Context, e *Entry, warrant oc.Warrant) string {
|
||||
cur := e.Current
|
||||
x := cur.Retracting
|
||||
if warrant.Outcome != asks.OutcomeChosen {
|
||||
if warrant.Outcome != occ.OutcomeChosen {
|
||||
why := "retraction not made: the operator's ask ended " + string(warrant.Outcome)
|
||||
if warrant.Words != "" {
|
||||
why += " (" + warrant.Words + ")"
|
||||
}
|
||||
return k.endRetractionSaid(ctx, e, "the knowledge base", why)
|
||||
}
|
||||
option, err := warrant.For(Module, composeRetractAsk(cur, x))
|
||||
option, err := occ.For(warrant, Module, composeRetractAsk(cur, x))
|
||||
if err == nil {
|
||||
err = option.Performs(retractAct(cur, x, option.ID))
|
||||
err = occ.Performs(option, retractAct(cur, x, option.ID))
|
||||
}
|
||||
if err != nil {
|
||||
k.logf("[mesh-knowledge] REFUSED the answer to the ask to retract %s: %v", cur.Key(), err)
|
||||
return k.endRetractionSaid(ctx, e, "the knowledge base", "retraction not made: the answer does not hold here: "+err.Error())
|
||||
}
|
||||
if option.ID != OptionApprove {
|
||||
return k.endRetractionSaid(ctx, e, "the operator", "retraction of "+x.What()+" declined by the operator ("+warrant.Says()+")")
|
||||
return k.endRetractionSaid(ctx, e, "the operator", "retraction of "+x.What()+" declined by the operator ("+occ.Says(warrant)+")")
|
||||
}
|
||||
r := cur.Clone()
|
||||
done := *r.Retracting
|
||||
done.Ask.Said, done.Ask.Warrant, done.Ask.Held, done.Applied = AskApproved, warrant.Says(), "", k.now()
|
||||
done.Ask.Said, done.Ask.Warrant, done.Ask.Held, done.Applied = AskApproved, occ.Says(warrant), "", k.now()
|
||||
r.Retracting = nil
|
||||
if done.Whole() {
|
||||
done.Before, r.Status = r.Status, StatusRetracted
|
||||
@@ -345,7 +346,7 @@ func (k *KB) decideRetraction(ctx context.Context, e *Entry, warrant asks.Warran
|
||||
}
|
||||
}
|
||||
r.Retractions = append(r.Retractions, done)
|
||||
why := fmt.Sprintf("retracted %s on the operator's Approve, asked by %s: %s; warrant: %s", done.What(), done.By, done.Why, warrant.Says())
|
||||
why := fmt.Sprintf("retracted %s on the operator's Approve, asked by %s: %s; warrant: %s", done.What(), done.By, done.Why, occ.Says(warrant))
|
||||
if err := k.Store.Save(ctx, Version{By: "the operator", At: k.now(), Why: why, Record: r}, true, false, e.Revision); err != nil {
|
||||
return fmt.Sprintf("%s could not be retracted (%v); it holds as it did", r.Key(), err)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
|
||||
)
|
||||
|
||||
// to-be 55 §10: "A record is accepted only after the knowledge base's own judgement approves it, and, when it needs
|
||||
@@ -248,9 +248,9 @@ func TestARecordThatNeedsTheOperatorIsAcceptedOnlyOnTheWarrant(t *testing.T) {
|
||||
t.Fatalf("after the review: %s, asks %d", e.Current.Status, len(w.router.asked))
|
||||
}
|
||||
ask := w.router.asked[0]
|
||||
if err := ask.Check(w.clock); err != nil || ask.Options[0].Level != asks.Approve || ask.Options[1].Level != asks.Approve ||
|
||||
if err := occ.Check(ask, w.clock); err != nil || ask.Options[0].Level != occ.Approve || ask.Options[1].Level != occ.Approve ||
|
||||
!strings.Contains(ask.Details, "the knowledge base's own review") || ask.Headline != "Merges land after ninety seconds of quiet" ||
|
||||
asks.Glance(ask.Explanation) != nil || strings.Contains(ask.Explanation, "The window closes") {
|
||||
occ.Glance(ask.Explanation) != nil || strings.Contains(ask.Explanation, "The window closes") {
|
||||
t.Fatalf("the ask: %v %+v", err, ask)
|
||||
}
|
||||
// Without the warrant: nothing is accepted, however often the router's record is read or the reviews run.
|
||||
@@ -275,7 +275,7 @@ func TestARecordThatNeedsTheOperatorIsAcceptedOnlyOnTheWarrant(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e := mustGet(t, s, Key{KindDecision, n})
|
||||
if e.Current.Status != StatusAccepted || e.Current.Ask == nil || e.Current.Ask.Warrant != warrant.Says() ||
|
||||
if e.Current.Status != StatusAccepted || e.Current.Ask == nil || e.Current.Ask.Warrant != occ.Says(warrant) ||
|
||||
e.Current.Ask.Said != AskApproved || len(said) != 1 {
|
||||
t.Fatalf("on the warrant: %+v, ask %+v, said %v", e.Current.Status, e.Current.Ask, said)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
|
||||
)
|
||||
|
||||
// Rule is one rule that judges one record: its id and what it holds.
|
||||
@@ -654,7 +654,7 @@ func Holds(rule, kind, date string) bool {
|
||||
|
||||
// briefProblems are the ways a brief would not read at a glance on the operator's phone (novox/hq issue 639):
|
||||
// missing; a headline empty or longer than an ask's; a summary or a point the router could refuse, or that
|
||||
// names a path, an address or a long word (shown); more than three points, or one too long (asks.Glance).
|
||||
// names a path, an address or a long word (shown); more than three points, or one too long (occ.Glance).
|
||||
func briefProblems(b *Brief) []string {
|
||||
if b == nil {
|
||||
return []string{"a record that needs the operator carries a brief (headline, summary, points): what it changes for the operator, " +
|
||||
@@ -662,8 +662,8 @@ func briefProblems(b *Brief) []string {
|
||||
}
|
||||
var out []string
|
||||
h := strings.TrimSpace(b.Headline)
|
||||
if h == "" || strings.Contains(h, "\n") || len([]rune(h)) > asks.HeadlineLength {
|
||||
out = append(out, fmt.Sprintf("the brief's headline is empty, more than one line, or longer than %d characters", asks.HeadlineLength))
|
||||
if h == "" || strings.Contains(h, "\n") || len([]rune(h)) > occ.HeadlineLength {
|
||||
out = append(out, fmt.Sprintf("the brief's headline is empty, more than one line, or longer than %d characters", occ.HeadlineLength))
|
||||
}
|
||||
if s := strings.TrimSpace(b.Summary); s == "" || strings.Contains(s, "\n") {
|
||||
out = append(out, "the brief's summary is empty or more than one line")
|
||||
@@ -673,7 +673,7 @@ func briefProblems(b *Brief) []string {
|
||||
out = append(out, fmt.Sprintf("the brief's point %d is empty or more than one line", i+1))
|
||||
}
|
||||
}
|
||||
if err := asks.Glance(b.Explanation()); err != nil {
|
||||
if err := occ.Glance(b.Explanation()); err != nil {
|
||||
out = append(out, "the brief "+strings.TrimPrefix(err.Error(), "the explanation "))
|
||||
}
|
||||
for _, line := range append([]string{h}, strings.Split(b.Explanation(), "\n")...) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
|
||||
)
|
||||
|
||||
func TestAReservedRecordIsWithdrawnAtOnce(t *testing.T) {
|
||||
@@ -181,7 +181,7 @@ func TestARetractionTakesEffectOnlyOnTheOperatorsApprove(t *testing.T) {
|
||||
t.Fatalf("asked %d times", len(w.router.asked)-asked)
|
||||
}
|
||||
a := w.router.asked[len(w.router.asked)-1]
|
||||
if err := a.Check(w.clock); err != nil || asks.Glance(a.Explanation) != nil || len([]rune(a.Headline)) > 60 ||
|
||||
if err := occ.Check(a, w.clock); err != nil || occ.Glance(a.Explanation) != nil || len([]rune(a.Headline)) > 60 ||
|
||||
!strings.Contains(a.Headline, key.String()) || !strings.Contains(a.Explanation+a.Details, why) {
|
||||
t.Fatalf("the ask %+v: %v", a, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user