Let a proposed record be withdrawn (issue 1081)

A dropped proposal or a stale reservation stayed proposed forever: there was
no way to say the operator decided against it. knowledge-base.withdraw takes
a reserved or proposed record to withdrawn as a new version naming why, ends
a waiting operator's ask and clears a pending review in the same version,
and refuses an already accepted, superseded or withdrawn record. The number
stays spent either way.

The old Withdraw (the background job for reservations never proposed) is
renamed WithdrawStale to make room for the new, caller-driven WithdrawRecord.
This commit is contained in:
jochen
2026-10-18 00:02:48 +02:00
parent ad80048c22
commit ddf7a5ab09
8 changed files with 204 additions and 10 deletions
@@ -606,9 +606,9 @@ func (k *KB) TellFindings() {
// ---- reservations that were never proposed -------------------------------------------------------------------
// Withdraw withdraws every number reserved longer ago than the settings allow and never proposed: it stays spent,
// with who reserved it and when.
func (k *KB) Withdraw() (int, error) {
// WithdrawStale withdraws every number reserved longer ago than the settings allow and never proposed: it stays
// spent, with who reserved it and when.
func (k *KB) WithdrawStale() (int, error) {
ctx := context.Background()
k.mu.Lock()
defer k.mu.Unlock()
@@ -638,3 +638,51 @@ func (k *KB) Withdraw() (int, error) {
}
return n, nil
}
// ---- withdraw, on the operator's decision ------------------------------------------------------------------
// Withdrawn is the answer to withdraw.
type Withdrawn struct {
Record string `json:"record"`
Status string `json:"status"`
Says string `json:"says"`
}
// WithdrawRecord withdraws a reserved or proposed record on the caller's decision, with why in one line: the
// number stays spent (novox/hq ADR 0290 rule 5), as a new version by the caller. A pending review is cleared, and
// a waiting operator's ask is ended, both as part of that one version. Refused: a record this holds none of, an
// empty why, and a record already accepted (a research effort accepted at any status), superseded or withdrawn —
// only a reserved or a proposed record takes it.
func (k *KB) WithdrawRecord(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 record 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 {
return nil, fmt.Errorf("the knowledge base holds no %s; nothing was withdrawn", key)
}
cur := e.Current
if cur.Status != StatusReserved && cur.Status != StatusProposed {
return nil, fmt.Errorf("%s is %s and takes no withdraw; only a reserved or a proposed record does. Nothing was withdrawn", key, cur.Status)
}
r := cur.Clone()
r.Status = StatusWithdrawn
r.Judging = nil
if r.Ask != nil && r.Ask.Said == AskWaiting {
r.Ask.Said = "ended: the record was withdrawn"
}
full := fmt.Sprintf("withdrawn by %s: %s", caller, why)
if err := k.Store.Save(ctx, Version{By: caller, At: k.now(), Why: full, Record: r}, true, true, e.Revision); err != nil {
return nil, fmt.Errorf("%s could not be written (%v); nothing was withdrawn", key, err)
}
k.emit(EventChanged, r, caller, full, e.Versions+1)
return &Withdrawn{Record: key.String(), Status: StatusWithdrawn,
Says: fmt.Sprintf("%s is withdrawn: %s. Its number stays spent, and is never handed out again.", key, why)}, nil
}