mesh/delivery merged: source.main moved to novox/mesh-catalog@65fad0ae1c31
mesh/check being carried to 5d50b952d4cb on 65fad0ae1c31 | from a68b39f7b512
A record proposed before a retraction and accepted after it marked the retracted clause changed or the retracted record superseded, serving it again; and returning a drafted record numbered its clauses anew, bringing retracted ones back in force or making a waiting Approve mark another clause. accept now leaves what is retracted alone, Return refuses a record with a retraction, and retract waits until a record's drafting is done.
469 lines
19 KiB
Go
469 lines
19 KiB
Go
package main
|
|
|
|
// Retract (the operator, 2026-10-31: "I'm more a fan of removing incorrect decisions than creating new records,
|
|
// especially if I didn't create and verify/approve the decisions myself"): an accepted record, whole or the clauses
|
|
// listed, is taken back on the operator's Approve. The ask goes the way a record that needs the operator goes
|
|
// (operator.go): on the router, Approve and Decline at the level approve, each binding the act it authorises, and
|
|
// the retraction takes effect only on the router's warrant for Approve, read back from the router's own record and
|
|
// held to the ask composed again. Until then, and on Decline or expiry, nothing of the record changes but the
|
|
// version that says it was asked and how that ended. A retracted record or clause no longer holds; nothing is
|
|
// deleted: every version stays in history, and the version that retracts says by whom, why and on what warrant.
|
|
// What a retracted record changed or superseded stays so; only its mark says the change is retracted.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
)
|
|
|
|
// RetractAsked is the status of a retraction asked and not yet answered.
|
|
const RetractAsked = "asked"
|
|
|
|
// RetractedMark is what a clause another record changed or superseded carries, after the record that changed it
|
|
// is named, once that change is retracted.
|
|
const RetractedMark = " (retracted)"
|
|
|
|
// Retracting is the answer to retract.
|
|
type Retracting struct {
|
|
Record string `json:"record"`
|
|
Clauses []int `json:"clauses,omitempty"`
|
|
Status string `json:"status"`
|
|
Ask string `json:"ask"`
|
|
Says string `json:"says"`
|
|
}
|
|
|
|
// What is what a retraction takes back, in words: "the record whole" or "§1, §3".
|
|
func (x Retraction) What() string {
|
|
if x.Whole() {
|
|
return "the record whole"
|
|
}
|
|
return clauseList(x.Clauses)
|
|
}
|
|
|
|
func clauseList(cs []int) string {
|
|
parts := make([]string, len(cs))
|
|
for i, n := range cs {
|
|
parts[i] = "§" + strconv.Itoa(n)
|
|
}
|
|
return strings.Join(parts, ", ")
|
|
}
|
|
|
|
// Says is a retraction in one line: who asked it, when, why, and what the operator said.
|
|
func (x Retraction) Says() string {
|
|
s := fmt.Sprintf("by %s at %s: %s", x.By, x.At.UTC().Format("2006-01-02 15:04 UTC"), x.Why)
|
|
if x.Ask != nil {
|
|
s += "; " + x.Ask.Said
|
|
if x.Ask.Warrant != "" {
|
|
s += " (" + x.Ask.Warrant + ")"
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Retract asks the operator to take back an accepted record whole (no clauses) or the clauses listed, with why in
|
|
// one line, and keeps the ask on the record as a new version. Nothing of the record changes until the operator's
|
|
// Approve (Reconcile). Refused: an empty or multi-line why; a record not held, or neither accepted nor superseded
|
|
// (a reserved or proposed one is withdrawn instead); one retracted already; a clause it has none of, listed twice,
|
|
// or retracted already; a retraction or a new version already waiting on it; and an operator who cannot be asked.
|
|
func (k *KB) Retract(caller string, key Key, clauses []int, why string) (*Retracting, error) {
|
|
why = strings.TrimSpace(why)
|
|
if why == "" || strings.Contains(why, "\n") || len([]rune(why)) > 200 {
|
|
return nil, errors.New("retract needs why: one line of at most 200 characters saying why it no longer holds; nothing was asked")
|
|
}
|
|
ctx := context.Background()
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
e, err := k.Store.Get(ctx, key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the knowledge base cannot be read (%v); nothing was asked", err)
|
|
}
|
|
if e == nil {
|
|
return nil, fmt.Errorf("the knowledge base holds no %s; nothing was asked", key)
|
|
}
|
|
cur := e.Current
|
|
switch {
|
|
case cur.Status == StatusRetracted:
|
|
return nil, fmt.Errorf("%s is retracted already; nothing was asked", key)
|
|
case cur.Status == StatusReserved || cur.Status == StatusProposed:
|
|
return nil, fmt.Errorf("%s is %s: withdraw it instead; only an accepted or superseded record is retracted. Nothing was asked", key, cur.Status)
|
|
case !cur.Accepted() && cur.Status != StatusSuperseded:
|
|
return nil, fmt.Errorf("%s is %s and takes no retraction; nothing was asked", key, cur.Status)
|
|
case cur.Retracting != nil:
|
|
return nil, fmt.Errorf("%s has a retraction waiting for the operator (%s): withdraw it first. Nothing was asked", key, cur.Retracting.Says())
|
|
case cur.Drafting != nil && cur.Drafting.State != DraftDone:
|
|
return nil, fmt.Errorf("%s's clauses are being drafted (%s): retract it once its drafting is done, so the clauses keep the "+
|
|
"numbers the operator is shown. Nothing was asked", key, cur.Drafting.State)
|
|
case e.Pending != nil:
|
|
return nil, fmt.Errorf("%s has version %d waiting for its review: let it be decided first. Nothing was asked", key, e.PendingVersion)
|
|
}
|
|
seen := map[int]bool{}
|
|
var leads []string
|
|
for _, n := range clauses {
|
|
c := cur.Clause(n)
|
|
switch {
|
|
case c == nil:
|
|
return nil, fmt.Errorf("%s has no clause %d; nothing was asked", key, n)
|
|
case seen[n]:
|
|
return nil, fmt.Errorf("clause %d is listed twice; nothing was asked", n)
|
|
case c.Status == ClauseRetracted:
|
|
return nil, fmt.Errorf("%s §%d is retracted already; nothing was asked", key, n)
|
|
}
|
|
seen[n] = true
|
|
leads = append(leads, c.Lead())
|
|
}
|
|
sorted := append([]int(nil), clauses...)
|
|
sort.Ints(sorted)
|
|
if k.Asks == nil {
|
|
return nil, errors.New("the operator cannot be asked from here; nothing was asked")
|
|
}
|
|
at := k.now()
|
|
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 {
|
|
return nil, fmt.Errorf("the ask could not be made (%v); nothing was asked", err)
|
|
}
|
|
r := cur.Clone()
|
|
r.Retracting = x
|
|
what := fmt.Sprintf("retraction of %s asked by %s: %s; the operator is asked", x.What(), caller, why)
|
|
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 {
|
|
// 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 {
|
|
back := e2.Current.Clone()
|
|
back.Retracting = nil
|
|
_ = k.Store.Save(ctx, Version{By: caller, At: k.now(), Why: "retraction not asked: the operator could not be asked (" + err.Error() + ")",
|
|
Record: back}, true, false, e2.Revision)
|
|
}
|
|
return nil, fmt.Errorf("the operator could not be asked (%v); nothing changed", err)
|
|
}
|
|
k.emit(EventChanged, r, caller, what, e.Versions+1)
|
|
return &Retracting{Record: key.String(), Clauses: sorted, Status: RetractAsked, Ask: ask.ID,
|
|
Says: fmt.Sprintf("The operator is asked to retract %s of %s (%q). Nothing changes until the operator's Approve; on Decline or "+
|
|
"expiry nothing changes. get shows it waiting; withdraw with retraction true takes it back.", x.What(), key, ask.Headline)}, nil
|
|
}
|
|
|
|
// WithdrawRetraction takes back a retraction waiting for the operator, with why in one line: the ask is ended here
|
|
// and taken back on the router, and an answer to it is never acted on.
|
|
func (k *KB) WithdrawRetraction(caller string, key Key, why string) (*Withdrawn, error) {
|
|
why = strings.TrimSpace(why)
|
|
if why == "" {
|
|
return nil, errors.New("withdraw needs why: one line saying why the retraction is withdrawn; nothing was withdrawn")
|
|
}
|
|
ctx := context.Background()
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
e, err := k.Store.Get(ctx, key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the knowledge base cannot be read (%v); nothing was withdrawn", err)
|
|
}
|
|
if e == nil || e.Current.Retracting == nil {
|
|
return nil, fmt.Errorf("no retraction of %s waits for the operator; nothing was withdrawn", key)
|
|
}
|
|
id := e.Current.Retracting.Ask.ID
|
|
full := fmt.Sprintf("retraction of %s withdrawn by %s: %s", e.Current.Retracting.What(), caller, why)
|
|
if err := k.endRetraction(ctx, e, caller, full); err != nil {
|
|
return nil, err
|
|
}
|
|
if k.Asks != nil {
|
|
if err := k.Asks.Publish(asks.CancelSubject(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)
|
|
}
|
|
}
|
|
return &Withdrawn{Record: key.String(), Status: e.Current.Status,
|
|
Says: fmt.Sprintf("The retraction of %s is withdrawn: %s. The record holds as it did.", key, why)}, nil
|
|
}
|
|
|
|
// endRetraction drops the waiting retraction as a new version saying why; nothing else of the record changes.
|
|
func (k *KB) endRetraction(ctx context.Context, e *Entry, by, why string) error {
|
|
r := e.Current.Clone()
|
|
r.Retracting = nil
|
|
if err := k.Store.Save(ctx, Version{By: by, At: k.now(), Why: why, Record: r}, true, false, e.Revision); err != nil {
|
|
return fmt.Errorf("%s: %s, and that could not be kept (%v)", r.Key(), why, err)
|
|
}
|
|
k.emit(EventChanged, r, by, why, e.Versions+1)
|
|
return nil
|
|
}
|
|
|
|
func newRetractAskID(r *Record) string {
|
|
return "kb-retract-" + strings.TrimPrefix(newAskID(r), "kb-")
|
|
}
|
|
|
|
// 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,
|
|
"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 {
|
|
name := r.Key().String()
|
|
headline := "Retract " + name + "?"
|
|
if !x.Whole() {
|
|
if h := "Retract " + name + " " + clauseList(x.Clauses) + "?"; len([]rune(h)) <= asks.HeadlineLength {
|
|
headline = h
|
|
} else {
|
|
headline = fmt.Sprintf("Retract %d clauses of %s?", len(x.Clauses), name)
|
|
}
|
|
}
|
|
what := "It"
|
|
if !x.Whole() {
|
|
what = "Those clauses"
|
|
}
|
|
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))
|
|
} 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))
|
|
}
|
|
if !x.Whole() {
|
|
var leads []string
|
|
for i, l := range x.Leads {
|
|
if shown(l) && l != "" {
|
|
leads = append(leads, fmt.Sprintf("§%d %s", x.Clauses[i], l))
|
|
}
|
|
}
|
|
if len(leads) > 0 {
|
|
points = append(points, clip("Clauses: "+strings.Join(leads, "; "), asks.PointLength))
|
|
}
|
|
}
|
|
explanation := summary
|
|
for _, p := range points {
|
|
explanation += "\n• " + p
|
|
}
|
|
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",
|
|
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})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// reconcileRetraction decides a retraction waiting for the operator whose ask the router has closed, or keeps
|
|
// that the router holds it. The caller holds the write lock.
|
|
func (k *KB) reconcileRetraction(ctx context.Context, e *Entry) string {
|
|
x := e.Current.Retracting
|
|
entry, err := k.Asks.Record(x.Ask.ID)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
if entry == nil {
|
|
if k.now().After(x.Ask.At.Add(AskLasts + time.Hour)) {
|
|
return k.endRetractionSaid(ctx, e, "the knowledge base",
|
|
"retraction not made: the router keeps no record of its ask (refused before it was taken, or forgotten)")
|
|
}
|
|
return ""
|
|
}
|
|
var rec routerRecord
|
|
if json.Unmarshal(entry.Value, &rec) != nil {
|
|
return ""
|
|
}
|
|
if rec.State == "open" || rec.State == "held" {
|
|
if held := heldSays(rec); held != x.Ask.Held {
|
|
r := e.Current.Clone()
|
|
r.Retracting.Ask.Held = held
|
|
why := "the operator's ask to retract is offered by the router"
|
|
if held != "" {
|
|
why = "the operator's ask to retract is held by the router: " + held
|
|
}
|
|
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 fmt.Sprintf("%s: what became of its ask to retract could not be kept (%v)", r.Key(), err)
|
|
}
|
|
return fmt.Sprintf("%s: %s", r.Key(), why)
|
|
}
|
|
return ""
|
|
}
|
|
if rec.Warrant == nil {
|
|
return ""
|
|
}
|
|
return k.decideRetraction(ctx, e, *rec.Warrant)
|
|
}
|
|
|
|
func (k *KB) endRetractionSaid(ctx context.Context, e *Entry, by, why string) string {
|
|
if err := k.endRetraction(ctx, e, by, why); err != nil {
|
|
return err.Error()
|
|
}
|
|
return fmt.Sprintf("%s: %s; it holds as it did", e.Current.Key(), why)
|
|
}
|
|
|
|
// 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 {
|
|
cur := e.Current
|
|
x := cur.Retracting
|
|
if warrant.Outcome != asks.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))
|
|
if err == nil {
|
|
err = option.Performs(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()+")")
|
|
}
|
|
r := cur.Clone()
|
|
done := *r.Retracting
|
|
done.Ask.Said, done.Ask.Warrant, done.Ask.Held, done.Applied = AskApproved, warrant.Says(), "", k.now()
|
|
r.Retracting = nil
|
|
if done.Whole() {
|
|
done.Before, r.Status = r.Status, StatusRetracted
|
|
} else {
|
|
for _, n := range done.Clauses {
|
|
if c := r.Clause(n); c != nil {
|
|
c.Status = ClauseRetracted
|
|
}
|
|
}
|
|
}
|
|
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())
|
|
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)
|
|
}
|
|
k.emit(EventChanged, r, "the operator", why, e.Versions+1)
|
|
k.markRetracted(ctx, r, done)
|
|
k.judgeSet(ctx)
|
|
return fmt.Sprintf("%s: %s", r.Key(), why)
|
|
}
|
|
|
|
// markRetracted marks, on every clause a retracted record or clause changed or superseded, that the change is
|
|
// retracted. The clause stays changed or superseded: a retraction takes back what the record said, not what it did
|
|
// to another; that record's author corrects it if it should hold again.
|
|
func (k *KB) markRetracted(ctx context.Context, r *Record, x Retraction) {
|
|
for _, l := range r.Links {
|
|
to, ok := l.To.Record()
|
|
if !ok || (l.Kind != LinkChanges && l.Kind != LinkSupersedes) || !(x.Whole() || (l.From > 0 && x.Covers(l.From))) {
|
|
continue
|
|
}
|
|
t, err := k.Store.Get(ctx, to)
|
|
if err != nil || t == nil {
|
|
continue
|
|
}
|
|
from := r.Key().String()
|
|
if l.From > 0 {
|
|
from += " §" + strconv.Itoa(l.From)
|
|
}
|
|
c := t.Current.Clone()
|
|
marked := false
|
|
for i := range c.Clauses {
|
|
cl := &c.Clauses[i]
|
|
if l.Kind == LinkChanges && cl.N != l.To.Clause {
|
|
continue
|
|
}
|
|
for j, by := range cl.ChangedBy {
|
|
if by == from || (l.Kind == LinkSupersedes && by == r.Key().String()) {
|
|
cl.ChangedBy[j], marked = by+RetractedMark, true
|
|
}
|
|
}
|
|
}
|
|
if !marked {
|
|
continue
|
|
}
|
|
what := fmt.Sprintf("marked: what %s changed here is retracted; the clause stays as it was changed", from)
|
|
if err := k.Store.Save(ctx, Version{By: "the knowledge base", At: k.now(), Why: what, Record: c}, true, false, t.Revision); err != nil {
|
|
k.logf("[mesh-knowledge] %s could not be marked (%v)", to, err)
|
|
continue
|
|
}
|
|
k.emit(EventChanged, c, "the knowledge base", what, t.Versions+1)
|
|
}
|
|
}
|
|
|
|
// retractedSays is how a read says a retracted record or clause no longer holds.
|
|
func retractedSays(r *Record, n int) string {
|
|
x := r.RetractedBy(n)
|
|
if x == nil && n > 0 {
|
|
x = r.RetractedBy(0)
|
|
}
|
|
if x == nil {
|
|
return "retracted: not in force"
|
|
}
|
|
return "retracted: not in force (" + x.Says() + ")"
|
|
}
|
|
|
|
// RetractionRow is one approved retraction as get shows it.
|
|
type RetractionRow struct {
|
|
What string `json:"what"`
|
|
By string `json:"by"`
|
|
At string `json:"at"`
|
|
Why string `json:"why"`
|
|
Approval string `json:"approval"`
|
|
Applied string `json:"applied,omitempty"`
|
|
}
|
|
|
|
func retractionRows(r *Record) []RetractionRow {
|
|
var out []RetractionRow
|
|
for _, x := range r.Retractions {
|
|
row := RetractionRow{What: x.What(), By: x.By, At: x.At.UTC().Format("2006-01-02 15:04 UTC"), Why: x.Why}
|
|
if x.Ask != nil {
|
|
row.Approval = x.Ask.Said
|
|
if x.Ask.Warrant != "" {
|
|
row.Approval += ": " + x.Ask.Warrant
|
|
}
|
|
}
|
|
if !x.Applied.IsZero() {
|
|
row.Applied = x.Applied.UTC().Format("2006-01-02 15:04 UTC")
|
|
}
|
|
out = append(out, row)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// retractingSays is a retraction waiting for the operator, in words, or "".
|
|
func retractingSays(r *Record) string {
|
|
x := r.Retracting
|
|
if x == nil {
|
|
return ""
|
|
}
|
|
s := fmt.Sprintf("a retraction of %s asked %s; waiting for the operator's Approve (ask %s): until then it holds as it did",
|
|
x.What(), x.Says(), x.Ask.ID)
|
|
if x.Ask.Held != "" {
|
|
s += "; the router holds the ask, not yet shown to the operator: " + x.Ask.Held
|
|
}
|
|
return s
|
|
}
|
|
|
|
// linkRetracted says, for a link read from either end, whether it no longer holds because its record or clause, or
|
|
// what it points at, is retracted; "" when neither.
|
|
func linkRetracted(from *Record, l Link, to *Record) string {
|
|
switch {
|
|
case from != nil && from.Status == StatusRetracted:
|
|
return from.Key().String() + " is retracted: the link no longer holds"
|
|
case from != nil && l.From > 0 && from.Clause(l.From) != nil && from.Clause(l.From).Status == ClauseRetracted:
|
|
return fmt.Sprintf("%s §%d is retracted: the link no longer holds", from.Key(), l.From)
|
|
case to != nil && to.Status == StatusRetracted:
|
|
return "it points at " + to.Key().String() + ", which is retracted"
|
|
case to != nil && l.To.Clause > 0 && to.Clause(l.To.Clause) != nil && to.Clause(l.To.Clause).Status == ClauseRetracted:
|
|
return fmt.Sprintf("it points at %s §%d, which is retracted", to.Key(), l.To.Clause)
|
|
}
|
|
return ""
|
|
}
|