mesh/merge-gate pass: builds mesh-knowledge → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
A changes link onto a drafted clause was refused, so records that really change such a rule could only cite it (ADR 0294, 0301, 0320); and return renumbered ADR 0281's clauses, pointing other records' links at other clauses. The changed drafted clause keeps its number and stays unserved; return refuses a record whose clauses another record changed or links by number. Issue 494, ADR 0321.
557 lines
21 KiB
Go
557 lines
21 KiB
Go
package main
|
|
|
|
// Drafting and confirming clauses at the import (novox/hq ADR 0290 rule 8, to-be 55 §7). For a decision whose
|
|
// Decision section is prose, one judgement drafts its clauses: the section in, numbered statements out, each one
|
|
// sentence saying nothing the section does not. They are kept `drafted`, and never served as in force. A second
|
|
// judgement, a separate call that sees only the record and the drafts, answers per clause whether the record says
|
|
// exactly that: a clause it accepts becomes `confirmed`, one it rejects stays `drafted` with the reason. Which
|
|
// clause of an older record a supersedes-in-part changes is judged and confirmed the same way. When every record
|
|
// of a topic has been through both, the operator is told once, with the counts. `return` sends a record back to
|
|
// be drafted again.
|
|
//
|
|
// The judgements are the agent's on this node (claude-code.claude_code_judge, ADR 0278): no tools, no session,
|
|
// at most 25 seconds each, one at a time. The worker takes one step at a time in the background.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// DefaultJudgeModel is the model the judgements ask unless the settings name another.
|
|
const DefaultJudgeModel = "sonnet"
|
|
|
|
// MostDraftTries is how many times a record's drafting is tried before it is left failed, said by its get.
|
|
const MostDraftTries = 3
|
|
|
|
var draftSchema = map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"clauses": map[string]any{"type": "array", "items": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"lead": map[string]any{"type": "string", "description": "a few words naming what is decided, ending with a full stop"},
|
|
"text": map[string]any{"type": "string", "description": "the one sentence of the statement, after the lead"},
|
|
},
|
|
"required": []string{"lead", "text"},
|
|
}},
|
|
},
|
|
"required": []string{"clauses"},
|
|
}
|
|
|
|
var confirmSchema = map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"verdicts": map[string]any{"type": "array", "items": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"clause": map[string]any{"type": "integer"},
|
|
"exact": map[string]any{"type": "boolean", "description": "true when the record says exactly this, nothing more"},
|
|
"reason": map[string]any{"type": "string", "description": "when not exact: what the record says instead"},
|
|
},
|
|
"required": []string{"clause", "exact"},
|
|
}},
|
|
},
|
|
"required": []string{"verdicts"},
|
|
}
|
|
|
|
var whichSchema = map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"clauses": map[string]any{"type": "array", "items": map[string]any{"type": "integer"},
|
|
"description": "the numbers of the older record's clauses the newer record replaces part of"},
|
|
"reason": map[string]any{"type": "string"},
|
|
},
|
|
"required": []string{"clauses", "reason"},
|
|
}
|
|
|
|
var whichConfirmSchema = map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"exact": map[string]any{"type": "boolean"},
|
|
"reason": map[string]any{"type": "string"},
|
|
},
|
|
"required": []string{"exact", "reason"},
|
|
}
|
|
|
|
// MostDraftSource is the most text a record is drafted from in one judgement, which takes at most 64 KiB.
|
|
const MostDraftSource = 48 << 10
|
|
|
|
// draftSource is what a record's clauses are drafted from: its Decision section, or, for a record that has none
|
|
// (hq issue 487), what it decides across its sections — all but its preamble, consequences and references.
|
|
func draftSource(r *Record) (text string, whole bool) {
|
|
if section, ok := decisionSection(r); ok && strings.TrimSpace(section) != "" {
|
|
return section, false
|
|
}
|
|
var b strings.Builder
|
|
for _, s := range r.Sections {
|
|
name := strings.ToLower(strings.TrimSpace(s.Name))
|
|
if name == "preamble" || strings.HasPrefix(name, "consequences") || strings.HasPrefix(name, "references") || strings.TrimSpace(s.Text) == "" {
|
|
continue
|
|
}
|
|
fmt.Fprintf(&b, "## %s\n\n%s\n\n", s.Name, strings.TrimSpace(s.Text))
|
|
}
|
|
return strings.TrimSpace(b.String()), true
|
|
}
|
|
|
|
// sourceName is how a question names what it hands the judgement.
|
|
func sourceName(whole bool) string {
|
|
if whole {
|
|
return "The record's sections"
|
|
}
|
|
return "The Decision section"
|
|
}
|
|
|
|
// DraftQuestion is the first judgement's question: the record's Decision section — or, where it has none, its
|
|
// sections — and nothing else of the mesh.
|
|
func DraftQuestion(r *Record, section, returned string, whole bool) string {
|
|
var b strings.Builder
|
|
if whole {
|
|
fmt.Fprintf(&b, "Below are the sections of a decision record, %s: %q. It has no Decision section: what it decides is written "+
|
|
"as prose across its sections. Write what it decides as numbered clauses: each one statement, one sentence, with a lead of a "+
|
|
"few words naming what is decided. A clause says nothing the sections do not say: no inference, no context from elsewhere, "+
|
|
"nothing added. Every decision the sections make is in one clause; a sentence that only explains, or describes what was, is "+
|
|
"in none. Keep the record's own words where you can.\n", r.Key(), r.Title)
|
|
if returned != "" {
|
|
fmt.Fprintf(&b, "\nAn earlier draft was sent back, with this reason: %s\n", returned)
|
|
}
|
|
b.WriteString("\n" + sourceName(true) + ":\n\n" + section)
|
|
return b.String()
|
|
}
|
|
fmt.Fprintf(&b, "Below is the Decision section of a decision record, %s: %q. It is written as prose. Write what it decides as numbered "+
|
|
"clauses: each one statement, one sentence, with a lead of a few words naming what is decided. A clause says nothing the section "+
|
|
"does not say: no inference, no context from elsewhere, nothing added. Every decision the section makes is in one clause; a "+
|
|
"sentence that only explains is in none. Keep the section's own words where you can.\n", r.Key(), r.Title)
|
|
if returned != "" {
|
|
fmt.Fprintf(&b, "\nAn earlier draft was sent back, with this reason: %s\n", returned)
|
|
}
|
|
b.WriteString("\nThe Decision section:\n\n" + section)
|
|
return b.String()
|
|
}
|
|
|
|
// ConfirmQuestion is the second judgement's question: the record and the drafts, nothing else.
|
|
func ConfirmQuestion(r *Record, section string, drafts []Clause, whole bool) string {
|
|
var b strings.Builder
|
|
what := "the Decision section of a decision record"
|
|
if whole {
|
|
what = "the sections of a decision record that has no Decision section"
|
|
}
|
|
fmt.Fprintf(&b, "Below are %s, %s: %q, and clauses drafted from it by someone else. For each "+
|
|
"clause, answer whether the text says exactly that: exact is true only when the clause says nothing the text does not, "+
|
|
"and leaves out nothing that changes its meaning. When it is not exact, say in reason what the text says instead.\n\n"+
|
|
"%s:\n\n%s\n\nThe drafted clauses:\n\n", what, r.Key(), r.Title, sourceName(whole), section)
|
|
for _, c := range drafts {
|
|
fmt.Fprintf(&b, "%d. %s\n", c.N, c.Text)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func (k *KB) judgeModel() string {
|
|
if m := strings.TrimSpace(k.settings().JudgeModel); m != "" {
|
|
return m
|
|
}
|
|
return DefaultJudgeModel
|
|
}
|
|
|
|
// DraftStep takes one step of the drafting and confirming: the first record waiting to be drafted or confirmed,
|
|
// then the first changes link whose clause is not yet judged. It answers what it did, or "" when nothing waits.
|
|
func (k *KB) DraftStep() (string, error) {
|
|
if k.Judge == nil || strings.EqualFold(strings.TrimSpace(k.settings().Drafting), "off") {
|
|
return "", nil
|
|
}
|
|
ctx := context.Background()
|
|
all, err := k.Store.All(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, e := range all {
|
|
d := e.Current.Drafting
|
|
if d == nil || d.State == DraftDone || (d.State == DraftFailed && d.Tries >= MostDraftTries) {
|
|
continue
|
|
}
|
|
if d.State == DraftFailed && k.now().Sub(d.At) < time.Duration(d.Tries)*10*time.Minute {
|
|
continue
|
|
}
|
|
return k.draftOne(ctx, e.Current.Key())
|
|
}
|
|
for _, e := range all {
|
|
if !e.Current.Accepted() {
|
|
continue
|
|
}
|
|
for i, l := range e.Current.Links {
|
|
if l.Kind == LinkChanges && (l.Source == LinkPending || l.Source == SourceDrafted) && !l.Unresolved {
|
|
return k.judgeChange(ctx, e.Current.Key(), i)
|
|
}
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
// draftOne drafts or confirms one record's clauses, under the write lock only to read and to write.
|
|
func (k *KB) draftOne(ctx context.Context, key Key) (string, error) {
|
|
k.mu.Lock()
|
|
e, err := k.Store.Get(ctx, key)
|
|
k.mu.Unlock()
|
|
if err != nil || e == nil {
|
|
return "", err
|
|
}
|
|
r := e.Current.Clone()
|
|
section, whole := draftSource(r)
|
|
if section == "" || len(section) > MostDraftSource {
|
|
why := fmt.Sprintf("no Decision section, and its sections hold %d bytes of what it decides, more than one judgement takes (%d): "+
|
|
"it has no clauses", len(section), MostDraftSource)
|
|
if section == "" {
|
|
why = "no Decision section, and no section saying what it decides: it has no clauses"
|
|
}
|
|
r.Drafting = &Drafting{State: DraftDone, Reason: why, At: k.now()}
|
|
return k.saveDraft(ctx, e, r, EventDrafted, why)
|
|
}
|
|
model := k.judgeModel()
|
|
fail := func(step string, err error) (string, error) {
|
|
// Tried again after a pause that grows with each try, at the step it failed at, up to MostDraftTries.
|
|
r.Drafting.State, r.Drafting.Reason, r.Drafting.At = DraftFailed, step+": "+err.Error(), k.now()
|
|
r.Drafting.Tries++
|
|
_, serr := k.saveDraft(ctx, e, r, EventDrafted, step+" failed: "+err.Error())
|
|
if serr != nil {
|
|
return "", serr
|
|
}
|
|
return "", fmt.Errorf("%s %s: %v", step, key, err)
|
|
}
|
|
if r.Drafting == nil {
|
|
r.Drafting = &Drafting{State: DraftWaiting}
|
|
}
|
|
// A failed record goes back to the step it failed at: confirming when it holds drafts, drafting otherwise.
|
|
confirming := r.Drafting.State == DraftDrafted || (r.Drafting.State == DraftFailed && hasDrafts(r))
|
|
if !confirming {
|
|
answer, by, err := k.Judge(DraftQuestion(r, section, r.Drafting.Returned, whole), draftSchema, model)
|
|
if err != nil {
|
|
return fail("drafting", err)
|
|
}
|
|
var drafted struct {
|
|
Clauses []struct {
|
|
Lead string `json:"lead"`
|
|
Text string `json:"text"`
|
|
} `json:"clauses"`
|
|
}
|
|
if err := json.Unmarshal(answer, &drafted); err != nil || len(drafted.Clauses) == 0 {
|
|
return fail("drafting", errors.New("the judgement drafted no clause"))
|
|
}
|
|
r.Clauses = nil
|
|
for i, c := range drafted.Clauses {
|
|
lead := strings.TrimSpace(strings.Trim(c.Lead, "* "))
|
|
if !strings.HasSuffix(lead, ".") {
|
|
lead += "."
|
|
}
|
|
r.Clauses = append(r.Clauses, Clause{N: i + 1, Text: "**" + lead + "** " + strings.TrimSpace(c.Text),
|
|
Status: clauseStatusOf(r.Status), Source: SourceDrafted})
|
|
}
|
|
r.Drafting = &Drafting{State: DraftDrafted, Drafter: by, Returned: r.Drafting.Returned, Tries: r.Drafting.Tries, At: k.now()}
|
|
return k.saveDraft(ctx, e, r, EventDrafted, fmt.Sprintf("%d clauses drafted by %s", len(r.Clauses), by))
|
|
}
|
|
answer, by, err := k.Judge(ConfirmQuestion(r, section, r.Clauses, whole), confirmSchema, model)
|
|
if err != nil {
|
|
return fail("confirming", err)
|
|
}
|
|
var verdicts struct {
|
|
Verdicts []struct {
|
|
Clause int `json:"clause"`
|
|
Exact bool `json:"exact"`
|
|
Reason string `json:"reason"`
|
|
} `json:"verdicts"`
|
|
}
|
|
if err := json.Unmarshal(answer, &verdicts); err != nil {
|
|
return fail("confirming", errors.New("the judgement answered no verdicts"))
|
|
}
|
|
confirmed := 0
|
|
for i := range r.Clauses {
|
|
r.Clauses[i].Unconfirmed = "the second judgement gave no verdict on it"
|
|
for _, v := range verdicts.Verdicts {
|
|
if v.Clause != r.Clauses[i].N {
|
|
continue
|
|
}
|
|
if v.Exact {
|
|
r.Clauses[i].Source, r.Clauses[i].Unconfirmed = SourceConfirmed, ""
|
|
confirmed++
|
|
} else {
|
|
r.Clauses[i].Unconfirmed = strings.TrimSpace(v.Reason)
|
|
if r.Clauses[i].Unconfirmed == "" {
|
|
r.Clauses[i].Unconfirmed = "not exact"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
r.Drafting.State, r.Drafting.Checker, r.Drafting.At, r.Drafting.Reason = DraftDone, by, k.now(), ""
|
|
said, err := k.saveDraft(ctx, e, r, EventConfirmed, fmt.Sprintf("%d of %d drafted clauses confirmed by %s", confirmed, len(r.Clauses), by))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
k.tellTopic(ctx, r.Topic)
|
|
return said, nil
|
|
}
|
|
|
|
func hasDrafts(r *Record) bool {
|
|
for _, c := range r.Clauses {
|
|
if c.Source == SourceDrafted {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (k *KB) saveDraft(ctx context.Context, e *Entry, r *Record, event, why string) (string, error) {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
by := "the knowledge base"
|
|
if r.Drafting != nil && event == EventDrafted && r.Drafting.Drafter != "" {
|
|
by = r.Drafting.Drafter
|
|
}
|
|
if r.Drafting != nil && event == EventConfirmed && r.Drafting.Checker != "" {
|
|
by = r.Drafting.Checker
|
|
}
|
|
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 could not be kept (%v)", r.Key(), err)
|
|
}
|
|
k.emit(event, r, by, why, e.Versions+1)
|
|
return r.Key().String() + ": " + why, nil
|
|
}
|
|
|
|
// tellTopic tells the operator, once, when every prose record of a topic has been drafted and confirmed.
|
|
func (k *KB) tellTopic(ctx context.Context, topic string) {
|
|
if k.Notify == nil || topic == "" {
|
|
return
|
|
}
|
|
all, err := k.Store.All(ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
records, confirmed, drafted := 0, 0, 0
|
|
for _, e := range all {
|
|
r := e.Current
|
|
if r.Topic != topic || r.Drafting == nil {
|
|
continue
|
|
}
|
|
if r.Drafting.State != DraftDone {
|
|
return
|
|
}
|
|
records++
|
|
for _, c := range r.Clauses {
|
|
if c.Source == SourceConfirmed {
|
|
confirmed++
|
|
} else if c.Source == SourceDrafted {
|
|
drafted++
|
|
}
|
|
}
|
|
}
|
|
if records == 0 {
|
|
return
|
|
}
|
|
first, err := k.Store.Told(ctx, topic)
|
|
if err != nil || !first {
|
|
return
|
|
}
|
|
summary := fmt.Sprintf("The clauses of %d prose decisions on %s are drafted and checked: %d confirmed, %d still drafted.", records, topic, confirmed, drafted)
|
|
explanation := "A second judgement confirmed each clause against its record; a clause still drafted is never served as in force. " +
|
|
"You may send any record back with the knowledge base's return, and its clauses are drafted again. Nothing needs you otherwise."
|
|
if err := k.Notify("knowledge.clauses."+strings.ReplaceAll(topic, " ", "-"), summary, "Clauses drafted for "+topic, explanation, ""); err != nil {
|
|
k.logf("[mesh-knowledge] the operator could not be told about %s's clauses: %v", topic, err)
|
|
}
|
|
}
|
|
|
|
// judgeChange judges, then confirms, which clause of an older record a supersedes-in-part of hq changes.
|
|
func (k *KB) judgeChange(ctx context.Context, from Key, i int) (string, error) {
|
|
k.mu.Lock()
|
|
e, err := k.Store.Get(ctx, from)
|
|
var t *Entry
|
|
if err == nil && e != nil && i < len(e.Current.Links) {
|
|
if to, ok := e.Current.Links[i].To.Record(); ok {
|
|
t, err = k.Store.Get(ctx, to)
|
|
}
|
|
}
|
|
k.mu.Unlock()
|
|
if err != nil || e == nil || t == nil {
|
|
return "", err
|
|
}
|
|
r := e.Current.Clone()
|
|
l := &r.Links[i]
|
|
older := t.Current
|
|
served := 0
|
|
for _, c := range older.Clauses {
|
|
if c.Source != SourceDrafted {
|
|
served++
|
|
}
|
|
}
|
|
if served == 0 {
|
|
return "", nil // the older record's clauses are not confirmed yet: judged once they are
|
|
}
|
|
model := k.judgeModel()
|
|
newer, whole := draftSource(r)
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "%s says it replaces part of an older record, %s. Below are the clauses of %s and %s of %s.\n\n", r.Key(), older.Key(), older.Key(),
|
|
strings.ToLower(sourceName(whole)), r.Key())
|
|
for _, c := range older.Clauses {
|
|
fmt.Fprintf(&b, "%d. %s\n", c.N, c.Text)
|
|
}
|
|
fmt.Fprintf(&b, "\n%s of %s:\n\n%s\n", sourceName(whole), r.Key(), newer)
|
|
if l.Source == LinkPending {
|
|
answer, by, err := k.Judge(b.String()+"\nWhich of the older record's clauses does the newer record replace part of? Name their numbers.", whichSchema, model)
|
|
if err != nil {
|
|
return "", fmt.Errorf("judging which clause %s changes: %v", r.Key(), err)
|
|
}
|
|
var which struct {
|
|
Clauses []int `json:"clauses"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
if json.Unmarshal(answer, &which) != nil || len(which.Clauses) == 0 || older.Clause(which.Clauses[0]) == nil {
|
|
return "", fmt.Errorf("judging which clause %s changes: the judgement named no clause of %s", r.Key(), older.Key())
|
|
}
|
|
base := *l
|
|
l.To.Clause, l.Source = which.Clauses[0], SourceDrafted
|
|
for _, n := range which.Clauses[1:] {
|
|
if older.Clause(n) != nil {
|
|
extra := base
|
|
extra.To.Clause, extra.Source = n, SourceDrafted
|
|
r.Links = append(r.Links, extra)
|
|
}
|
|
}
|
|
return k.saveLinks(ctx, e, r, by, fmt.Sprintf("which clause of %s it changes drafted: §%s (%s)", older.Key(), joinInts(which.Clauses), which.Reason))
|
|
}
|
|
answer, by, err := k.Judge(b.String()+fmt.Sprintf("\nA first judgement said %s replaces part of clause %d of %s. Is that exact?", r.Key(), l.To.Clause, older.Key()), whichConfirmSchema, model)
|
|
if err != nil {
|
|
return "", fmt.Errorf("confirming which clause %s changes: %v", r.Key(), err)
|
|
}
|
|
var v struct {
|
|
Exact bool `json:"exact"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
if json.Unmarshal(answer, &v) != nil {
|
|
return "", fmt.Errorf("confirming which clause %s changes: no verdict", r.Key())
|
|
}
|
|
if !v.Exact {
|
|
clause := l.To.Clause
|
|
l.To.Clause, l.Source = 0, SourceDrafted+": not confirmed"
|
|
return k.saveLinks(ctx, e, r, by, fmt.Sprintf("that it changes %s §%d was not confirmed: %s", older.Key(), clause, v.Reason))
|
|
}
|
|
l.Source = SourceConfirmed
|
|
said, err := k.saveLinks(ctx, e, r, by, fmt.Sprintf("that it changes %s §%d confirmed by %s", older.Key(), l.To.Clause, by))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// The older clause is changed now, as an acceptance would have made it.
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
t, err = k.Store.Get(ctx, older.Key())
|
|
if err != nil || t == nil {
|
|
return said, err
|
|
}
|
|
c := t.Current.Clone()
|
|
if cl := c.Clause(l.To.Clause); cl != nil && cl.Status == ClauseInForce {
|
|
cl.Status = ClauseChanged
|
|
cl.ChangedBy = append(cl.ChangedBy, r.Key().String())
|
|
what := fmt.Sprintf("§%d changed by %s (as hq said, its clause judged and confirmed)", l.To.Clause, r.Key())
|
|
if err := k.Store.Save(ctx, Version{By: by, At: k.now(), Why: what, Record: c}, true, false, t.Revision); err != nil {
|
|
return said, err
|
|
}
|
|
k.emit(EventChanged, c, by, what, t.Versions+1)
|
|
}
|
|
return said, nil
|
|
}
|
|
|
|
func (k *KB) saveLinks(ctx context.Context, e *Entry, r *Record, by, why string) (string, error) {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
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 could not be kept (%v)", r.Key(), err)
|
|
}
|
|
event := EventDrafted
|
|
if strings.Contains(why, "confirmed") {
|
|
event = EventConfirmed
|
|
}
|
|
k.emit(event, r, by, why, e.Versions+1)
|
|
return r.Key().String() + ": " + why, nil
|
|
}
|
|
|
|
func joinInts(ns []int) string {
|
|
s := make([]string, len(ns))
|
|
for i, n := range ns {
|
|
s[i] = strconv.Itoa(n)
|
|
}
|
|
return strings.Join(s, ", §")
|
|
}
|
|
|
|
// Return sends a record's drafted or confirmed clauses back to be drafted again, with the reason, which the next
|
|
// drafting reads.
|
|
func (k *KB) Return(caller string, key Key, reason string) (map[string]any, error) {
|
|
ctx := context.Background()
|
|
reason = strings.TrimSpace(reason)
|
|
if reason == "" {
|
|
return nil, errors.New("return needs reason: what is wrong with the clauses drafted")
|
|
}
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
e, err := k.Store.Get(ctx, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if e == nil {
|
|
return nil, fmt.Errorf("the knowledge base holds no %s", key)
|
|
}
|
|
r := e.Current.Clone()
|
|
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)
|
|
}
|
|
all, err := k.Store.All(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if held := clauseHolds(r, all); len(held) > 0 {
|
|
return nil, fmt.Errorf("%s is not returned: drafting it again would number its clauses anew, and these hold them to their numbers: %s. "+
|
|
"A clause keeps its number once another record links it (novox/hq issue 494). To say a drafted clause is wrong "+
|
|
"or no longer holds, propose a record that changes it (changes %s §N)", key, strings.Join(held, "; "), strings.TrimPrefix(key.String(), "ADR "))
|
|
}
|
|
r.Clauses = nil
|
|
r.Drafting = &Drafting{State: DraftWaiting, Returned: reason, At: k.now()}
|
|
why := "returned to be drafted again: " + reason
|
|
if err := k.Store.Save(ctx, Version{By: caller, At: k.now(), Why: why, Record: r}, true, false, e.Revision); err != nil {
|
|
return nil, err
|
|
}
|
|
k.emit(EventReturned, r, caller, why, e.Versions+1)
|
|
return map[string]any{"record": key.String(), "says": fmt.Sprintf("%s's clauses are taken back and will be drafted again, with your reason; until then it serves none.", key)}, nil
|
|
}
|
|
|
|
// clauseHolds is what holds a record's clauses to their numbers: each clause another record changed or superseded,
|
|
// and each link of another record, served or proposed, that names one of its clauses. A redraft numbers the clauses
|
|
// anew, so a record any of these name is never returned: its links would point at other clauses.
|
|
func clauseHolds(r *Record, all []*Entry) []string {
|
|
var out []string
|
|
for _, c := range r.Clauses {
|
|
if len(c.ChangedBy) > 0 && (c.Status == ClauseChanged || c.Status == ClauseSuperseded) {
|
|
out = append(out, fmt.Sprintf("§%d is %s by %s", c.N, c.Status, strings.Join(c.ChangedBy, ", ")))
|
|
}
|
|
}
|
|
key := r.Key()
|
|
seen := map[string]bool{}
|
|
for _, e := range all {
|
|
for _, v := range []*Record{e.Current, e.Pending} {
|
|
if v == nil || v.Key() == key || v.Status == StatusWithdrawn {
|
|
continue
|
|
}
|
|
for _, l := range v.Links {
|
|
to, ok := l.To.Record()
|
|
if !ok || to != key || l.To.Clause == 0 {
|
|
continue
|
|
}
|
|
s := fmt.Sprintf("%s (%s) %s §%d", v.Key(), v.Status, l.Kind, l.To.Clause)
|
|
if !seen[s] {
|
|
seen[s] = true
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|