Compare commits
3
Commits
feat/072-moves
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65fad0ae1c | ||
|
|
5d50b952d4 | ||
|
|
f19e3ccffd |
@@ -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. A record accepted after it does not supersede or change what is retracted; a record still being drafted is retracted only once its drafting is done, and a record with a retraction (waiting or approved) is never returned to be drafted again. The caller need not be proven by the runtime, as for `withdraw`: it is recorded, and the operator's Approve is what authorises the retraction |
|
||||
| `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 {
|
||||
|
||||
@@ -500,6 +500,16 @@ func (k *KB) Return(caller string, key Key, reason string) (map[string]any, erro
|
||||
return nil, fmt.Errorf("the knowledge base holds no %s", key)
|
||||
}
|
||||
r := e.Current.Clone()
|
||||
if r.Retracting != nil || len(r.Retractions) > 0 {
|
||||
// Drafting again numbers the clauses anew and brings them back in force: a retraction waiting would mark
|
||||
// another clause than the operator was shown, and one approved would be undone.
|
||||
which := "approved"
|
||||
if r.Retracting != nil {
|
||||
which = "waiting"
|
||||
}
|
||||
return nil, fmt.Errorf("%s is not returned: it has a retraction %s, which drafting it again would undo or point at other "+
|
||||
"clauses. Withdraw a waiting retraction first; a retracted record or clause stays so", key, which)
|
||||
}
|
||||
if r.Drafting == nil {
|
||||
return nil, fmt.Errorf("%s's clauses were written by its author, not drafted: a written clause is changed by a new record", key)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -486,6 +490,12 @@ func (k *KB) accept(ctx context.Context, held *Entry, w *Record, by, why, warran
|
||||
k.logf("[mesh-knowledge] %s %s %s, which cannot be read now (%v): the whole-set rules will say so", r.Key(), l.Kind, to, err)
|
||||
continue
|
||||
}
|
||||
if t.Current.Status == StatusRetracted {
|
||||
// What the operator retracted stays retracted: a record proposed before it and accepted after does not
|
||||
// bring it back as superseded or changed.
|
||||
k.logf("[mesh-knowledge] %s %s %s, which is retracted: it stays retracted", r.Key(), l.Kind, to)
|
||||
continue
|
||||
}
|
||||
c := t.Current.Clone()
|
||||
var what string
|
||||
switch l.Kind {
|
||||
@@ -493,6 +503,9 @@ func (k *KB) accept(ctx context.Context, held *Entry, w *Record, by, why, warran
|
||||
c.Status = StatusSuperseded
|
||||
c.Extra = addExtra(c.Extra, "superseded-by", r.Key().String())
|
||||
for i := range c.Clauses {
|
||||
if c.Clauses[i].Status == ClauseRetracted {
|
||||
continue
|
||||
}
|
||||
c.Clauses[i].Status = ClauseSuperseded
|
||||
c.Clauses[i].ChangedBy = append(c.Clauses[i].ChangedBy, r.Key().String())
|
||||
}
|
||||
@@ -502,6 +515,10 @@ func (k *KB) accept(ctx context.Context, held *Entry, w *Record, by, why, warran
|
||||
if cl == nil || cl.Status == ClauseSuperseded {
|
||||
continue
|
||||
}
|
||||
if cl.Status == ClauseRetracted {
|
||||
k.logf("[mesh-knowledge] %s changes %s §%d, which is retracted: it stays retracted", r.Key(), to, l.To.Clause)
|
||||
continue
|
||||
}
|
||||
cl.Status = ClauseChanged
|
||||
from := r.Key().String()
|
||||
if l.From > 0 {
|
||||
|
||||
@@ -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,468 @@
|
||||
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 ""
|
||||
}
|
||||
@@ -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,242 @@ 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)
|
||||
// then is a record proposed before the retraction and accepted after it, changing §1 or superseding the record:
|
||||
// its acceptance never brings back what was retracted.
|
||||
then string
|
||||
}
|
||||
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", ""},
|
||||
{"one clause, then a change of it proposed before and accepted after", []int{1}, OptionApprove, LinkChanges},
|
||||
{"the record whole, then a supersession proposed before and accepted after", nil, OptionApprove, LinkSupersedes},
|
||||
} {
|
||||
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)
|
||||
later := 0
|
||||
if c.then != "" {
|
||||
later = w.reserved(t, "author@test")
|
||||
p := valid(later)
|
||||
p.Links = []LinkAsk{{Kind: LinkExtends, To: itoa4(n)}, {Kind: LinkChanges, To: itoa4(n) + " §1", From: 1}}
|
||||
if c.then == LinkSupersedes {
|
||||
p.Links = []LinkAsk{{Kind: LinkSupersedes, To: itoa4(n)}}
|
||||
}
|
||||
if _, err := w.k.Propose("author@test", p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
if c.then != "" {
|
||||
w.reviewAll(t)
|
||||
if l := mustGet(t, w.k.Store, Key{KindDecision, later}).Current; !l.Accepted() {
|
||||
t.Fatalf("the later record was not accepted: %s", l.Status)
|
||||
}
|
||||
r := mustGet(t, w.k.Store, key).Current
|
||||
h, _ := w.k.Holds("", &key, 0)
|
||||
// Whole: retracted, nothing held. One clause: §1 retracted, and §2 the only one held.
|
||||
whole := c.clauses == nil
|
||||
if (whole && (r.Status != StatusRetracted || len(h.Clauses) != 0)) ||
|
||||
(!whole && (r.Clause(1).Status != ClauseRetracted || len(h.Clauses) != 1)) {
|
||||
t.Fatalf("an acceptance brought back what was retracted: %s, %+v, holds %+v", r.Status, r.Clauses, h.Clauses)
|
||||
}
|
||||
return
|
||||
}
|
||||
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")
|
||||
}
|
||||
// A drafted record: retracted only once its drafting is done, and never returned to be drafted again while a
|
||||
// retraction waits or after one, which would number its clauses anew and bring them back in force.
|
||||
drafted := Key{KindDecision, 240}
|
||||
draftUntil(t, w, drafted, DraftDrafted)
|
||||
if _, err := w.k.Retract("lead@shanks", drafted, []int{1}, why); err == nil || !strings.Contains(err.Error(), "draft") {
|
||||
t.Fatalf("a record still being drafted: %v", err)
|
||||
}
|
||||
draftUntil(t, w, drafted, DraftDone)
|
||||
out, err := w.k.Retract("lead@shanks", drafted, []int{1}, why)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := w.k.Return("operator@shanks", drafted, "redraft"); err == nil || !strings.Contains(err.Error(), "retract") {
|
||||
t.Fatalf("returned while a retraction waits: %v", err)
|
||||
}
|
||||
w.router.answer(out.Ask, OptionApprove, w.clock)
|
||||
if _, err := w.k.Reconcile(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := w.k.Return("operator@shanks", drafted, "redraft"); err == nil || !strings.Contains(err.Error(), "retract") {
|
||||
t.Fatalf("returned after a retraction: %v", err)
|
||||
}
|
||||
if c := mustGet(t, w.k.Store, drafted).Current.Clause(1); c == nil || c.Status != ClauseRetracted {
|
||||
t.Fatalf("§1: %+v", c)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"import",
|
||||
"return",
|
||||
"withdraw",
|
||||
"retract",
|
||||
"progress"
|
||||
]
|
||||
}
|
||||
@@ -54,6 +55,7 @@
|
||||
"import",
|
||||
"return",
|
||||
"withdraw",
|
||||
"retract",
|
||||
"progress"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user