knowledge-base: retract an accepted record or its clauses on the operator's Approve

The operator prefers removing a wrong decision to stacking a correcting
record on it, and withdraw took only reserved or proposed records. retract
asks the operator through the router like a record that needs the operator,
and changes nothing until the Approve; a retracted record or clause no longer
holds in ask, holds, get, links and progress, history keeps every version, and
the whole-set rules still read a retracted record's links so a retraction alone
raises no finding. withdraw with retraction true takes a waiting one back.
This commit is contained in:
2026-10-31 10:07:58 +01:00
parent a68b39f7b5
commit f19e3ccffd
17 changed files with 856 additions and 51 deletions
+2 -1
View File
@@ -26,7 +26,8 @@ operator, it is accepted only on the operator's Approve, given on a channel that
| `propose` | the record whole | proposed, or refused naming every rule it breaks; an accepted record changes only by a progressive insight (`insight`), but an active research effort takes new versions freely, each reviewed as a proposal. Each version proposed is then reviewed by the knowledge base itself (below) |
| `import` | `dry-run`, `commit` | hq's decision records and research efforts at the commit the records module stands at, each whole at its own number |
| `return` | `number`, `reason` | drafted clauses sent back to be drafted again; refused for a record whose clauses another record changed or links by number |
| `withdraw` | `number`, `kind`, `why` | a reserved or proposed record withdrawn, as a new version naming why; its number stays spent. A pending review is cleared and a waiting operator's ask is ended. Refused for a record already accepted, superseded or withdrawn |
| `withdraw` | `number`, `kind`, `why`, `retraction` | a reserved or proposed record withdrawn, as a new version naming why; its number stays spent. A pending review is cleared and a waiting operator's ask is ended. Refused for a record already accepted, superseded or withdrawn. With `retraction` true, the retraction waiting on the record is withdrawn instead, and the record holds as it did |
| `retract` | `number`, `kind`, `clauses`, `why` | an accepted or superseded record retracted whole, or the clauses listed, on the operator's Approve only (asked on the router like a record that needs the operator). Until then, and on Decline or expiry, nothing changes. A retracted record or clause no longer holds: `ask` and `holds` leave it out, `get` marks it with by whom, when, why and the warrant, `links` say so both ways, history keeps every version; what it changed or superseded stays so, marked retracted, and the whole-set rules still read its links |
| `progress` | `of` (`mesh`, `design <n>`, `milestone <design>/<id>`, `issue <n>`, `decision <n>` or `ADR <n>`, `research <n>`), `detail` (`minimal` or `detailed`), `bound`, `since` (a window such as `60m`, at most `24h`) | how far it has come (hq issue 1947): each child done ✓, moving ▶, waiting ⏸ or stuck ✗ with when it last moved, worked out from the issue tracker's rows and mesh-delivery's deliveries and groups, never written (ADR 0424 §1). `minimal` is one line per milestone, for a report every five minutes (two or three bus calls); `detailed` the report's tree with each issue's pull requests, the groups holding them and what it depends on. A design's title and state are read through the records module, cached by hq's commit; its milestones and their issues are the issue tracker's milestone records (`issue-tracker.milestones`, issue 1781), asked once per answer; a design with none is refused, naming the seed (`issue-tracker.import` with `milestones`). With `since`, only what changed in the window under `of`, headed "N changes in the last 60 min": milestones moved (from what to what, from `issue-tracker.milestones` `one`), issues opened or moved from what to what (from `issue-tracker.changes`, the tracker's log), noted or linked (from `issue-tracker.get`), pull requests opened, checked, merged, held or closed (from `mesh-delivery.show`'s transitions and check). It keeps no state; its warnings say when the tracker cannot answer `changes` (a move is then said by the status it reached, not from what) and that mesh-delivery keeps no time a merge reached its machines |
`ask`, `get`, `holds` and `history` stay within `bound` bytes (4096 unless asked for, at most 65536) and say what they cut
@@ -65,6 +65,8 @@ func clip(s string, n int) string {
// held says, for a clause, whether it holds and what changed it, in the words an agent reads.
func held(r *Record, c Clause) string {
switch {
case r.Status == StatusRetracted || c.Status == ClauseRetracted:
return retractedSays(r, c.N)
case c.Source == SourceDrafted:
s := "drafted, not confirmed: not the record's word, never in force"
if c.Unconfirmed != "" {
@@ -142,7 +144,7 @@ func (k *KB) since(ctx context.Context, key Key) []string {
continue
}
from, err := k.Store.Get(ctx, x.From)
if err != nil || from == nil || !from.Current.Accepted() {
if err != nil || from == nil || !from.Current.Accepted() || linkRetracted(from.Current, x.Link, nil) != "" {
continue
}
s := x.From.String()
@@ -286,29 +288,32 @@ type GetAsk struct {
// Got is the answer to get.
type Got struct {
Record string `json:"record"`
Title string `json:"title"`
Status string `json:"status"`
Topic string `json:"topic,omitempty"`
Date string `json:"date,omitempty"`
Deciders []string `json:"deciders,omitempty"`
Version int `json:"version"`
Versions int `json:"versions"`
Clauses []AnswerClause `json:"clauses,omitempty"`
Links []string `json:"links,omitempty"`
LinkedFrom []string `json:"linked-from,omitempty"`
Since []string `json:"changed-since,omitempty"`
Sections []string `json:"sections,omitempty"`
Reasoning []Section `json:"reasoning,omitempty"`
Insights []Insight `json:"insights,omitempty"`
Pending string `json:"pending,omitempty"`
Waiting string `json:"waiting,omitempty"`
Reviews []Review `json:"reviews,omitempty"`
Drafting *Drafting `json:"drafting,omitempty"`
Review string `json:"review,omitempty"`
Source string `json:"source,omitempty"`
Reserved string `json:"reserved,omitempty"`
NeedsOperat bool `json:"needs-operator,omitempty"`
Record string `json:"record"`
Title string `json:"title"`
Status string `json:"status"`
Topic string `json:"topic,omitempty"`
Date string `json:"date,omitempty"`
Deciders []string `json:"deciders,omitempty"`
Version int `json:"version"`
Versions int `json:"versions"`
Clauses []AnswerClause `json:"clauses,omitempty"`
Links []string `json:"links,omitempty"`
LinkedFrom []string `json:"linked-from,omitempty"`
Since []string `json:"changed-since,omitempty"`
Sections []string `json:"sections,omitempty"`
Reasoning []Section `json:"reasoning,omitempty"`
Insights []Insight `json:"insights,omitempty"`
Pending string `json:"pending,omitempty"`
Waiting string `json:"waiting,omitempty"`
// Retracted are the retractions the operator approved; Retracting, one waiting for the operator.
Retracted []RetractionRow `json:"retracted,omitempty"`
Retracting string `json:"retracting,omitempty"`
Reviews []Review `json:"reviews,omitempty"`
Drafting *Drafting `json:"drafting,omitempty"`
Review string `json:"review,omitempty"`
Source string `json:"source,omitempty"`
Reserved string `json:"reserved,omitempty"`
NeedsOperat bool `json:"needs-operator,omitempty"`
// Brief is what the operator's ask shows of it (issue 639).
Brief *Brief `json:"brief,omitempty"`
Cut string `json:"cut,omitempty"`
@@ -369,11 +374,16 @@ func (k *KB) Get(a GetAsk) (*Got, error) {
if a.Version == 0 {
g.Review = reviewSays(e.Working())
}
g.Retracted, g.Retracting = retractionRows(r), retractingSays(r)
for _, c := range r.Clauses {
g.Clauses = append(g.Clauses, AnswerClause{N: c.N, Text: c.Text, Holds: held(r, c), Source: c.Source})
}
for _, l := range r.Links {
g.Links = append(g.Links, l.String())
s := l.String()
if why := linkRetracted(r, l, k.recordOf(ctx, l.To)); why != "" {
s += " (" + why + ")"
}
g.Links = append(g.Links, s)
}
if in, err := k.Store.Inbound(ctx, key); err == nil {
for _, x := range in {
@@ -385,6 +395,9 @@ func (k *KB) Get(a GetAsk) (*Got, error) {
if x.Link.To.Clause > 0 {
s += fmt.Sprintf(" §%d", x.Link.To.Clause)
}
if why := linkRetracted(k.recordOf(ctx, Ref{Kind: x.From.Kind, Number: x.From.Number}), x.Link, r); why != "" {
s += " (" + why + ")"
}
g.LinkedFrom = append(g.LinkedFrom, s)
}
}
@@ -516,7 +529,7 @@ func (k *KB) Holds(topic string, key *Key, bound int) (*Holding, error) {
return nil, fmt.Errorf("holds takes a topic or a record's number")
}
out := &Holding{Of: of, Bound: bound, Clauses: []HeldClause{}}
drafted, prose := 0, 0
drafted, prose, retracted := 0, 0, 0
for _, r := range records {
if r.Kind == KindDecision && r.Accepted() && len(r.Clauses) == 0 {
prose++
@@ -525,6 +538,9 @@ func (k *KB) Holds(topic string, key *Key, bound int) (*Holding, error) {
if c.Source == SourceDrafted && r.Accepted() && c.Status != ClauseSuperseded {
drafted++
}
if r.Status == StatusRetracted || c.Status == ClauseRetracted {
retracted++
}
if Served(r, c) {
out.Clauses = append(out.Clauses, HeldClause{Record: r.Key().String(), N: c.N, Text: c.Text, Holds: held(r, c)})
}
@@ -537,6 +553,9 @@ func (k *KB) Holds(topic string, key *Key, bound int) (*Holding, error) {
if prose > 0 {
notes = append(notes, fmt.Sprintf("%d accepted decision(s) write their decision as prose and have no clauses yet", prose))
}
if retracted > 0 {
notes = append(notes, fmt.Sprintf("%d retracted clause(s) are not served: they no longer hold (get says by whom and why)", retracted))
}
out.NotYet = strings.Join(notes, "; ")
clauses := out.Clauses
v, n := fit(len(clauses), bound, func(n int) any {
@@ -577,6 +596,8 @@ type LinkRow struct {
To string `json:"to"`
Source string `json:"source,omitempty"`
Unresolved bool `json:"unresolved,omitempty"`
// Retracted says the link no longer holds, or points at what no longer holds, and why.
Retracted string `json:"retracted,omitempty"`
}
// Links is a record's links, both ways.
@@ -597,14 +618,16 @@ func (k *KB) Links(key Key) (*Linked, error) {
return k.String()
}
for _, l := range e.Current.Links {
out.Out = append(out.Out, LinkRow{Kind: l.Kind, From: from(key, l.From), To: l.To.String(), Source: l.Source, Unresolved: l.Unresolved})
out.Out = append(out.Out, LinkRow{Kind: l.Kind, From: from(key, l.From), To: l.To.String(), Source: l.Source, Unresolved: l.Unresolved,
Retracted: linkRetracted(e.Current, l, k.recordOf(ctx, l.To))})
}
in, err := k.Store.Inbound(ctx, key)
if err != nil {
return nil, err
}
for _, x := range in {
out.In = append(out.In, LinkRow{Kind: x.Link.Kind, From: from(x.From, x.Link.From), To: x.Link.To.String(), Source: x.Link.Source})
out.In = append(out.In, LinkRow{Kind: x.Link.Kind, From: from(x.From, x.Link.From), To: x.Link.To.String(), Source: x.Link.Source,
Retracted: linkRetracted(k.recordOf(ctx, Ref{Kind: x.From.Kind, Number: x.From.Number}), x.Link, e.Current)})
}
return out, nil
}
@@ -786,3 +809,16 @@ func reviewSays(r *Record) string {
// When it is asked again is said, so a pause is not taken for a review lost (novox/hq issue 497).
return fmt.Sprintf("not reviewed: %s (%s; no judgement that could not be had counts as an approval", j.Reason, j.owedSays())
}
// recordOf is the served version of the record a ref names, or nil when it names none or none is held.
func (k *KB) recordOf(ctx context.Context, ref Ref) *Record {
key, ok := ref.Record()
if !ok {
return nil
}
e, err := k.Store.Get(ctx, key)
if err != nil || e == nil {
return nil
}
return e.Current
}
@@ -369,6 +369,11 @@ func TestTheKnowledgeBaseOverTheBus(t *testing.T) {
if g.Status != StatusProposed || len(g.Clauses) != 2 || g.Clauses[0].Holds != "proposed: not in force" {
t.Fatalf("get: %+v", g)
}
// retract is served, its arguments read as an agent sends them: a proposed record is withdrawn, not retracted.
if _, err := b.call(Seat+".retract", map[string]any{"number": 300, "clauses": []int{1}, "why": "never verified"}); err == nil ||
!strings.Contains(err.Error(), "withdraw it instead") {
t.Fatalf("retract of a proposed record over the bus: %v", err)
}
var h Historied
_ = json.Unmarshal(must(b.call(Seat+".history", map[string]any{"number": 300, "from": 1, "to": 2})), &h)
if len(h.Versions) != 2 || len(h.Diff) == 0 {
@@ -338,6 +338,9 @@ func (k *KB) Propose(caller string, a ProposeAsk) (*Proposed, error) {
if why == "" {
why = "revised"
}
case held.Current.Accepted() && held.Current.Retracting != nil:
return nil, fmt.Errorf("%s has a retraction waiting for the operator: it takes no new version until that is answered or "+
"withdrawn. Nothing was proposed", key)
case held.Current.Accepted():
if held.Pending != nil && (!proven(caller) || caller != held.Pending.Author) {
return nil, fmt.Errorf("%s has a progressive insight by %s waiting for its review: only its author, proven by the "+
@@ -448,12 +451,13 @@ func (k *KB) accept(ctx context.Context, held *Entry, w *Record, by, why, warran
r.Insights = append(r.Insights, in)
}
r.Status = held.Current.Status
r.Retractions = held.Current.Retractions
if r.Kind == KindResearch && contains(ResearchStatuses, r.Effort) && held.Current.Status != StatusSuperseded {
r.Status = r.Effort
}
for i, c := range r.Clauses {
r.Clauses[i].Status = ClauseInForce
if old := held.Current.Clause(c.N); old != nil && (old.Status == ClauseChanged || old.Status == ClauseSuperseded) {
if old := held.Current.Clause(c.N); old != nil && (old.Status == ClauseChanged || old.Status == ClauseSuperseded || old.Status == ClauseRetracted) {
r.Clauses[i].Status, r.Clauses[i].ChangedBy = old.Status, old.ChangedBy
}
}
@@ -28,7 +28,7 @@ const MostListResult = 500
// AllStatuses are every status a record of either kind may hold.
var AllStatuses = []string{StatusReserved, StatusProposed, StatusAccepted, StatusSuperseded, StatusWithdrawn,
StatusActive, StatusGraduated, StatusAbandoned}
StatusActive, StatusGraduated, StatusAbandoned, StatusRetracted}
// ListAsk narrows a list.
type ListAsk struct {
@@ -37,7 +37,7 @@ import (
const Seat = "knowledge-base"
// Verbs are the seat's verbs, as the manifest declares and claims them (ADR 0290 rule 6).
var Verbs = []string{"ask", "get", "list", "findings", "holds", "links", "history", "reserve", "propose", "import", "return", "withdraw", "progress"}
var Verbs = []string{"ask", "get", "list", "findings", "holds", "links", "history", "reserve", "propose", "import", "return", "withdraw", "retract", "progress"}
// HealthTool is the module's own tool the node-engine asks how it is.
const HealthTool = "knowledge_health"
@@ -1105,9 +1105,10 @@ func Tools(k *KB, caller func(map[string]any) string) []stdio.Tool {
"accepted (a research effort accepted at any status), superseded or withdrawn — only a reserved or a proposed record " +
"takes it.",
Input: schema([]string{"number", "why"}, map[string]any{"kind": kindArg(), "number": integer("the record's number"),
"why": text("one line: why the record is withdrawn")}),
"why": text("one line: why the record (or, with retraction, its retraction) is withdrawn"),
"retraction": flag("true: withdraw the retraction waiting for the operator on this record, not the record; the record holds as it did")}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("withdraw", m, "kind", "number", "why")
a, err := readArgs("withdraw", m, "kind", "number", "why", "retraction")
if err != nil {
return nil, err
}
@@ -1119,8 +1120,47 @@ func Tools(k *KB, caller func(map[string]any) string) []stdio.Tool {
if err != nil {
return nil, err
}
retraction, err := a.flag("retraction")
if err != nil {
return nil, err
}
if retraction {
return k.WithdrawRetraction(caller(m), key, why)
}
return k.WithdrawRecord(caller(m), key, why)
}},
{Name: Seat + ".retract",
Description: "Retract an accepted (or superseded) record whole, or the clauses listed, with why in one line: the operator is " +
"asked on a channel that proves who answers (Approve or Decline), and it takes effect only on the operator's Approve. " +
"Until then, and on Decline or expiry, nothing of the record changes. A retracted record or clause no longer holds: ask " +
"and holds leave it out, get marks it retracted with by whom, when, why and the operator's warrant, links say so both " +
"ways; nothing is deleted, history keeps every version. What it changed or superseded stays so, marked retracted. get " +
"shows a retraction waiting; withdraw with retraction true takes it back. Refused: an empty why, a reserved or proposed " +
"record (withdraw it), one retracted already, a clause it has none of or retracted already, and a record with a " +
"retraction or a new version already waiting.",
Input: schema([]string{"number", "why"}, map[string]any{"kind": kindArg(), "number": integer("the record's number"),
"clauses": map[string]any{"type": "array", "items": map[string]any{"type": "integer", "minimum": 1},
"description": "the clauses to retract, by number; none: the record whole"},
"why": text("one line: why it no longer holds")}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("retract", m, "kind", "number", "clauses", "why")
if err != nil {
return nil, err
}
key, err := a.key()
if err != nil {
return nil, err
}
why, err := a.str("why")
if err != nil {
return nil, err
}
var clauses []int
if err := decode(m["clauses"], &clauses, "clauses is a list of clause numbers, such as [2, 3]"); err != nil {
return nil, err
}
return k.Retract(caller(m), key, clauses, why)
}},
{Name: Seat + ".progress",
Description: "How far something has come, in one call: of mesh (every design in progress), design <n> (its " +
"milestones), milestone <design>/<id> (its issues, such as milestone 47/R6), issue <n> (its pull requests " +
@@ -64,7 +64,7 @@ func TestItDeclaresAndHoldsTheKnowledgeBaseSeatOnceForTheMesh(t *testing.T) {
}
// ADR 0290 rule 6's verbs and those added after it; progress says a design's state, which ADR 0424 §1 makes the
// knowledge base's to work out (novox/hq issue 1947).
if !reflect.DeepEqual(Verbs, []string{"ask", "get", "list", "findings", "holds", "links", "history", "reserve", "propose", "import", "return", "withdraw", "progress"}) {
if !reflect.DeepEqual(Verbs, []string{"ask", "get", "list", "findings", "holds", "links", "history", "reserve", "propose", "import", "return", "withdraw", "retract", "progress"}) {
t.Fatalf("the verbs are not ADR 0290 rule 6's and those added after it: %v", Verbs)
}
if !reflect.DeepEqual(m.Emits, Events) {
@@ -245,6 +245,12 @@ func (k *KB) Reconcile() ([]string, error) {
}
var said []string
for _, e := range all {
if x := e.Current.Retracting; x != nil && x.Ask != nil && x.Ask.Said == AskWaiting {
if s := k.reconcileRetraction(ctx, e); s != "" {
said = append(said, s)
}
continue
}
w := e.Working()
if w.Status != StatusProposed || w.Ask == nil || w.Ask.Said != AskWaiting {
continue
@@ -366,6 +372,10 @@ func (k *KB) HeardEnd(w asks.Warrant) {
return
}
for _, e := range all {
if x := e.Current.Retracting; x != nil && x.Ask != nil && x.Ask.ID == w.Ask {
k.logf("[mesh-knowledge] %s", k.decideRetraction(ctx, e, w))
continue
}
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))
@@ -473,10 +473,10 @@ func (s *PgStore) Search(ctx context.Context, question string, limit int) ([]Hit
select kind, number, clause, rank from (
select c.kind, c.number, c.n as clause, ts_rank_cd(c.search, q.q) as rank
from clause c join record r on r.kind = c.kind and r.number = c.number, q
where c.search @@ q.q and r.status not in ('reserved', 'withdrawn')
where c.search @@ q.q and r.status not in ('reserved', 'withdrawn', 'retracted') and c.status <> 'retracted'
union all
select r.kind, r.number, 0, ts_rank_cd(r.search, q.q)
from record r, q where r.search @@ q.q and r.status not in ('reserved', 'withdrawn')
from record r, q where r.search @@ q.q and r.status not in ('reserved', 'withdrawn', 'retracted')
) hits order by rank desc, number desc, clause limit $2`, strings.Join(words, " "), limit)
if err != nil {
return nil, err
@@ -557,6 +557,12 @@ func (k *KB) progressRecord(p *Progress, kind string, n int) error {
p.Items = []Item{top}
var b strings.Builder
fmt.Fprintf(&b, "%s · %s — %s\n", name, g.Title, g.Status)
for _, x := range g.Retracted {
fmt.Fprintf(&b, "├── retracted %s by %s at %s: %s; not in force\n", x.What, x.By, x.At, x.Why)
}
if g.Retracting != "" {
b.WriteString("├── " + g.Retracting + "\n")
}
if len(designs) == 0 {
b.WriteString("├── no design names it\n")
} else {
@@ -187,15 +187,6 @@ func TestADesignsMilestonesAreTheTrackersRecords(t *testing.T) {
}
}
func containsInt(xs []int, n int) bool {
for _, x := range xs {
if x == n {
return true
}
}
return false
}
// 1947/AC6: the designs are read once per commit of hq, and again when it moves; the mesh level names those with
// milestones one by one and counts the rest.
func TestDesignsAreReadAgainOnlyWhenHqMoves(t *testing.T) {
@@ -37,6 +37,9 @@ const (
StatusActive = "active"
StatusGraduated = "graduated"
StatusAbandoned = "abandoned"
// StatusRetracted is a record taken back whole on the operator's Approve (retract.go): it no longer holds, and
// its versions stay.
StatusRetracted = "retracted"
)
// ResearchStatuses are the statuses an accepted research effort may have.
@@ -48,6 +51,8 @@ const (
ClauseInForce = "in force"
ClauseChanged = "changed"
ClauseSuperseded = "superseded"
// ClauseRetracted is a clause taken back on the operator's Approve (retract.go): it no longer holds.
ClauseRetracted = "retracted"
)
// A clause's source (to-be 55 §3).
@@ -329,6 +334,51 @@ type Record struct {
Judging *Judging `json:"judging,omitempty"`
Insights []Insight `json:"insights,omitempty"`
Pending *PendingState `json:"pending,omitempty"`
// Retracting is a retraction asked of the operator and not yet answered (retract.go); Retractions, every one
// the operator approved, oldest first.
Retracting *Retraction `json:"retracting,omitempty"`
Retractions []Retraction `json:"retractions,omitempty"`
}
// Retraction is an accepted record taken back, whole (no Clauses) or the clauses listed, by a caller with why in
// one line, in force only on the operator's Approve (retract.go). Title and Leads are what the operator was shown,
// kept so the ask is composed again the same way to hold the warrant to it.
type Retraction struct {
Clauses []int `json:"clauses,omitempty"`
Why string `json:"why"`
By string `json:"by"`
At time.Time `json:"at"`
Title string `json:"title,omitempty"`
Leads []string `json:"leads,omitempty"`
Ask *OperatorAsk `json:"ask,omitempty"`
// Before is the record's status before it was retracted whole; At applied, when the operator's Approve took effect.
Before string `json:"before,omitempty"`
Applied time.Time `json:"applied,omitempty"`
}
// Whole says the retraction takes back the record whole.
func (x Retraction) Whole() bool { return len(x.Clauses) == 0 }
// Covers says the retraction takes back clause n (any clause, for one of the record whole).
func (x Retraction) Covers(n int) bool { return x.Whole() || containsInt(x.Clauses, n) }
func containsInt(list []int, n int) bool {
for _, v := range list {
if v == n {
return true
}
}
return false
}
// RetractedBy is the retraction the operator approved that takes back clause n (0: the record whole), or nil.
func (r *Record) RetractedBy(n int) *Retraction {
for i := len(r.Retractions) - 1; i >= 0; i-- {
if x := r.Retractions[i]; (n == 0 && x.Whole()) || (n > 0 && x.Covers(n)) {
return &r.Retractions[i]
}
}
return nil
}
// Insight is a progressive insight: a correction of fact, dated, saying what stood before.
@@ -492,6 +542,12 @@ func Render(r *Record) string {
for _, in := range r.Insights {
fmt.Fprintf(&b, "insight %s: %s\n", in.Date, in.Before)
}
for _, x := range r.Retractions {
fmt.Fprintf(&b, "retracted %s: %s\n", x.What(), x.Says())
}
if x := r.Retracting; x != nil {
fmt.Fprintf(&b, "retraction asked %s: %s\n", x.What(), x.Says())
}
if len(r.Clauses) > 0 {
b.WriteString("\n## Clauses\n\n")
for _, c := range r.Clauses {
@@ -0,0 +1,465 @@
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 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 ""
}
@@ -475,7 +475,9 @@ func JudgeSet(records []*Record) []Finding {
l Link
}{}
for _, r := range records {
if !r.Accepted() && r.Status != StatusSuperseded {
// A retracted record's links stay read here: what it cited stays cited, what it changed or superseded stays
// so, and a retraction alone never makes a rule fail.
if !r.Accepted() && r.Status != StatusSuperseded && r.Status != StatusRetracted {
continue
}
for _, l := range r.Links {
@@ -525,14 +527,14 @@ func JudgeSet(records []*Record) []Finding {
}
switch l.Kind {
case LinkSupersedes:
if target.Status != StatusSuperseded {
if target.Status != StatusSuperseded && target.Status != StatusRetracted {
add("supersession", fmt.Sprintf("%s supersedes %s, which is %s", k, to, target.Status), k, to)
}
case LinkChanges:
if l.To.Clause == 0 {
continue
}
if c := target.Clause(l.To.Clause); c != nil && c.Status != ClauseChanged && c.Status != ClauseSuperseded {
if c := target.Clause(l.To.Clause); c != nil && c.Status != ClauseChanged && c.Status != ClauseSuperseded && c.Status != ClauseRetracted {
add("partial", fmt.Sprintf("%s changes %s §%d, which is %s", k, to, l.To.Clause, c.Status), k, to)
}
case LinkExtends, LinkRestsOn:
@@ -554,7 +556,7 @@ func JudgeSet(records []*Record) []Finding {
}
by := false
for _, in := range inbound[k] {
by = by || (in.l.Kind == LinkChanges && in.l.To.Clause == c.N && in.from.Accepted())
by = by || (in.l.Kind == LinkChanges && in.l.To.Clause == c.N && (in.from.Accepted() || in.from.Status == StatusRetracted))
}
if !by {
add("partial", fmt.Sprintf("%s §%d is changed and no accepted record changes it", k, c.N), k)
@@ -210,10 +210,13 @@ func (s *memStore) Search(_ context.Context, question string, limit int) ([]Hit,
}
for k, e := range s.entries {
r := e.versions[e.current-1].Record
if r.Status == StatusReserved || r.Status == StatusWithdrawn {
if r.Status == StatusReserved || r.Status == StatusWithdrawn || r.Status == StatusRetracted {
continue
}
for _, c := range r.Clauses {
if c.Status == ClauseRetracted {
continue
}
if n := score(c.Text); n > 0 {
hits = append(hits, Hit{Key: k, Clause: c.N, Rank: n})
}
@@ -7,6 +7,8 @@ import (
"context"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/asks"
)
func TestAReservedRecordIsWithdrawnAtOnce(t *testing.T) {
@@ -121,3 +123,185 @@ func TestWithdrawNeedsWhyAndRefusesARecordItHoldsNoneOf(t *testing.T) {
}
})
}
// Retract (the operator, 2026-10-31: "I'm more a fan of removing incorrect decisions than creating new records"): an
// accepted record, or listed clauses of it, stops holding on the operator's Approve and on nothing else; its versions
// stay, every read says it is retracted, and the whole-set rules do not fail for it.
func TestARetractionTakesEffectOnlyOnTheOperatorsApprove(t *testing.T) {
const why = "never verified by the operator"
type tc struct {
name string
clauses []int
answer string // approve, decline, or withdraw (the pending retraction withdrawn, then approved too late)
}
for _, c := range []tc{
{"the record whole, approved", nil, OptionApprove},
{"one clause, approved", []int{1}, OptionApprove},
{"the record whole, declined", nil, OptionDecline},
{"one clause, withdrawn before the answer", []int{1}, "withdraw"},
} {
t.Run(c.name, func(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
ctx := context.Background()
w := imported(t, s)
n := w.reserved(t, "author@test")
key, changed := Key{KindDecision, n}, Key{KindDecision, 276}
w.accepted(t, "author@test", valid(n))
w.k.judgeSet(ctx)
findings, _ := w.k.Store.Findings(ctx)
before := mustGet(t, w.k.Store, key)
asked := len(w.router.asked)
out, err := w.k.Retract("lead@shanks", key, c.clauses, why)
if err != nil {
t.Fatal(err)
}
if out.Status != RetractAsked || out.Record != key.String() || out.Ask == "" {
t.Fatalf("%+v", out)
}
// Asked once, at a glance, under the router's bounds; nothing changed yet.
if len(w.router.asked) != asked+1 {
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 ||
!strings.Contains(a.Headline, key.String()) || !strings.Contains(a.Explanation+a.Details, why) {
t.Fatalf("the ask %+v: %v", a, err)
}
g, err := w.k.Get(GetAsk{Kind: KindDecision, Number: n})
if err != nil || g.Status != StatusAccepted || !strings.Contains(g.Retracting, "lead@shanks") || len(g.Retracted) != 0 {
t.Fatalf("while asked: %+v %v", g, err)
}
if h, _ := w.k.Holds("", &key, 0); len(h.Clauses) != 2 {
t.Fatalf("while asked, holds %+v", h)
}
if _, err := w.k.Retract("lead@shanks", key, nil, "again"); err == nil {
t.Fatal("a second retraction was asked while one waits")
}
if _, err := w.k.Propose("author@test", ProposeAsk{Kind: KindDecision, Number: n, Title: "x", Insight: "y"}); err == nil ||
!strings.Contains(err.Error(), "retraction") {
t.Fatalf("an insight over a waiting retraction: %v", err)
}
switch c.answer {
case "withdraw":
if _, err := w.k.WithdrawRetraction("lead@shanks", key, "asked by mistake"); err != nil {
t.Fatal(err)
}
if !contains(w.router.cancelled, out.Ask) {
t.Fatalf("the router was not told: %v", w.router.cancelled)
}
w.router.answer(out.Ask, OptionApprove, w.clock)
default:
w.router.answer(out.Ask, c.answer, w.clock)
}
if _, err := w.k.Reconcile(); err != nil {
t.Fatal(err)
}
e := mustGet(t, w.k.Store, key)
vs, _ := w.k.Store.Versions(ctx, key)
if len(vs) <= before.Versions || e.Current.Retracting != nil {
t.Fatalf("versions %d (were %d), still retracting %+v", len(vs), before.Versions, e.Current.Retracting)
}
g, _ = w.k.Get(GetAsk{Kind: KindDecision, Number: n})
h, _ := w.k.Holds("", &key, 0)
l, _ := w.k.Links(key)
lc, _ := w.k.Links(changed)
target := mustGet(t, w.k.Store, changed).Current.Clause(1)
if c.answer != OptionApprove {
// Nothing changed: it holds as it did.
if e.Current.Status != StatusAccepted || len(h.Clauses) != 2 || len(g.Retracted) != 0 || g.Retracting != "" ||
strings.Contains(strings.Join(target.ChangedBy, ","), "retracted") {
t.Fatalf("a retraction not approved changed it: %+v, holds %d", g, len(h.Clauses))
}
return
}
// Approved: it no longer holds, says by whom, when, why and on what, and history keeps it.
if len(g.Retracted) != 1 || g.Retracted[0].By != "lead@shanks" || g.Retracted[0].Why != why ||
!strings.Contains(g.Retracted[0].Approval, "telegram") || g.Retracting != "" {
t.Fatalf("get: %+v", g.Retracted)
}
if !strings.Contains(vs[len(vs)-1].Why, "retracted") || vs[len(vs)-1].By != "the operator" {
t.Fatalf("the version: %+v", vs[len(vs)-1])
}
hit := false
for _, ev := range w.said.of(EventChanged) {
hit = hit || (ev["record"] == key.String() && strings.Contains(ev["what"].(string), "retracted"))
}
if !hit {
t.Fatalf("no changed event says the retraction: %v", w.said.of(EventChanged))
}
// What it changed stays changed; its mark says the change is retracted; the link says so both ways.
if target.Status != ClauseChanged || !strings.Contains(strings.Join(target.ChangedBy, ","), "retracted") {
t.Fatalf("0276 §1: %+v", target)
}
marked := false
for _, r := range lc.In {
marked = marked || (strings.HasPrefix(r.From, key.String()) && r.Retracted != "")
}
for _, r := range l.Out {
marked = marked && (r.Retracted != "" || (c.clauses != nil && r.From == key.String()))
}
if !marked {
t.Fatalf("links: %+v / %+v", l.Out, lc.In)
}
asked2, _ := w.k.Ask("merge window ninety seconds quiet", MostBound)
if c.clauses == nil {
if e.Current.Status != StatusRetracted || g.Status != StatusRetracted || len(h.Clauses) != 0 ||
!strings.HasPrefix(g.Clauses[0].Holds, "retracted") {
t.Fatalf("whole: %s, holds %+v, get %+v", e.Current.Status, h.Clauses, g.Clauses)
}
for _, r := range asked2.Records {
if r.Record == key.String() {
t.Fatalf("ask serves a retracted record: %+v", r)
}
}
} else {
if e.Current.Status != StatusAccepted || len(h.Clauses) != 1 || h.Clauses[0].N != 2 ||
!strings.HasPrefix(g.Clauses[0].Holds, "retracted") || g.Clauses[1].Holds != "in force" {
t.Fatalf("clause: %s, holds %+v, get %+v", e.Current.Status, h.Clauses, g.Clauses)
}
for _, r := range asked2.Records {
for _, ac := range r.Clauses {
if r.Record == key.String() && ac.N == 1 {
t.Fatalf("ask serves a retracted clause: %+v", r)
}
}
}
}
// The whole-set rules say no more than they did.
w.k.judgeSet(ctx)
if after, _ := w.k.Store.Findings(ctx); len(after) != len(findings) {
t.Fatalf("findings were %v, are %v", findings, after)
}
if _, err := w.k.Retract("lead@shanks", key, c.clauses, why); err == nil {
t.Fatal("a retracted record or clause was retracted again")
}
})
})
}
// Refused, with nothing asked: no why, a record not accepted, a clause it has none of.
eachStore(t, func(t *testing.T, s Store) {
w := imported(t, s)
n := w.reserved(t, "author@test")
key := Key{KindDecision, n}
if _, err := w.k.Retract("lead@shanks", key, nil, why); err == nil || !strings.Contains(err.Error(), "withdraw") {
t.Fatalf("a reserved record: %v", err)
}
w.accepted(t, "author@test", valid(n))
for _, bad := range []struct {
clauses []int
why string
}{{nil, " "}, {[]int{9}, why}, {[]int{1, 1}, why}} {
if _, err := w.k.Retract("lead@shanks", key, bad.clauses, bad.why); err == nil {
t.Fatalf("retract %v %q was asked", bad.clauses, bad.why)
}
}
if len(w.router.asked) != 0 {
t.Fatalf("asked %d", len(w.router.asked))
}
if _, err := w.k.WithdrawRetraction("lead@shanks", key, "nothing waits"); err == nil {
t.Fatal("a retraction was withdrawn where none waits")
}
})
}
+2
View File
@@ -33,6 +33,7 @@
"import",
"return",
"withdraw",
"retract",
"progress"
]
}
@@ -54,6 +55,7 @@
"import",
"return",
"withdraw",
"retract",
"progress"
]
}