The knowledge base's review knows its round: from round 3 only a wrong record asks changes, at round 5 the operator decides (issue 784, ADR 0665)

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW
This commit is contained in:
2026-11-09 23:13:55 +01:00
parent 5e537f851f
commit 62644aa012
7 changed files with 296 additions and 9 deletions
@@ -92,6 +92,9 @@ const AskGlance = 1
// 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) oc.Ask {
if a.Round > 0 {
return changesAsk(r, a)
}
if a.Form == AskGlance {
return glanceAsk(r, a)
}
@@ -124,6 +127,44 @@ func glanceAsk(r *Record, a *OperatorAsk) oc.Ask {
return withOptions(r, a, oc.Ask{ID: a.ID, Headline: headline, Explanation: explanation, Details: details})
}
// changesAsk is the ask of a record whose round-5 review asked changes (ADR 0665 §3): the findings, read at a glance,
// and the two answers. It is composed from the record and its ask alone: the findings are the last review's.
func changesAsk(r *Record, a *OperatorAsk) oc.Ask {
name := r.Key().String()
var b strings.Builder
fmt.Fprintf(&b, "The knowledge base's review of %s asked changes at round %d, the last round: you decide. Approve accepts the version as written; "+
"Decline leaves it proposed for its author, and its next review starts at round 1.", name, a.Round)
if t := strings.TrimSuffix(strings.TrimSpace(r.Title), "."); shown(t) {
fmt.Fprintf(&b, "\n\nIts title: %s.", t)
}
if n := len(r.Reviews); n > 0 {
b.WriteString("\n\nThe findings:")
size, hidden := 0, 0
for _, f := range strings.Split(r.Reviews[n-1].Findings, "\n") {
if f = strings.TrimSpace(f); f == "" {
continue
}
if rs := []rune(f); len(rs) > 300 {
f = string(rs[:300]) + "..."
}
if !shown(f) || size+len(f) > 1500 {
hidden++
continue
}
size += len(f)
fmt.Fprintf(&b, "\n- %s", f)
}
if hidden > 0 {
fmt.Fprintf(&b, "\n(%d finding(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 := withOptions(r, a, oc.Ask{ID: a.ID, Headline: fmt.Sprintf("%s: accept it as written after round %d?", name, a.Round), Explanation: b.String()})
out.Options[0].Does = "the version is accepted as written and its clauses are in force"
out.Options[1].Does = "the record stays proposed for its author; its next review is round 1"
return out
}
// 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) oc.Ask {
@@ -180,7 +221,11 @@ func newAskID(r *Record) string {
// 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) {
//
// round is 0 for a record the review approved. For round MostReviewRounds it is that round: the review asked changes,
// and the operator is asked to accept the version as written or leave it proposed (ADR 0665 §3). An ask not made
// then takes the round's review back, so its retry is the same round.
func (k *KB) askOperator(ctx context.Context, held *Entry, w *Record, by string, tries int, round int) (string, error) {
version := held.CurrentVersion
if held.Pending != nil {
version = held.PendingVersion
@@ -189,6 +234,10 @@ func (k *KB) askOperator(ctx context.Context, held *Entry, w *Record, by string,
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())
if round > 0 {
w.Reviews = w.Reviews[:len(w.Reviews)-1]
said = fmt.Sprintf("round %d asked changes, and %s (%s", round, 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)
@@ -203,17 +252,21 @@ func (k *KB) askOperator(ctx context.Context, held *Entry, w *Record, by string,
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, Form: AskGlance}
oa := &OperatorAsk{ID: newAskID(w), At: k.now(), Version: version, Said: AskWaiting, Form: AskGlance, Round: round}
ask := composeAsk(w, oa)
if err := occ.Check(ask, 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.IsInsight()},
asked := "approved; the operator is asked"
if round > 0 {
asked = fmt.Sprintf("round %d asked changes; the operator is asked", round)
}
if err := k.Store.Save(ctx, Version{By: by, At: k.now(), Why: "reviewed: " + asked, Record: w, Insight: held.Pending != nil && held.Pending.IsInsight()},
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)
k.emit(EventReviewed, w, by, asked, held.Versions+1)
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))
@@ -351,6 +404,9 @@ func (k *KB) decide(ctx context.Context, e *Entry, w *Record, warrant oc.Warrant
case OptionDecline:
w = w.Clone()
w.Ask.Warrant = occ.Says(warrant)
if w.Ask.Round > 0 {
w.CountFrom = len(w.Reviews) // the revision's review starts a new count at round 1 (ADR 0665 §3)
}
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")