Tighten who writes and reviews a record, date the rules, bound history (review of #206)
mesh/merge-gate pass: builds new: modules/mesh-knowledge, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compo…
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 superseded: a newer head of the same pull request

A revision came from anyone and a review was refused only to the first author,
so a co-writer could approve, and a record proposed before callers are proven
would later be reviewable by its own unproven author. Now a revision is taken
only from the proven author, a reserved number only from whoever reserved it,
and a review is refused from any writer of any version or for any record with
an unproven writer. The rules held records from no date, so a correction to an
older record was refused by rules hq never held it to; each dated rule now holds
from hq's own start date, and a new record cannot be dated before its
reservation. history and the question to ask are bounded like the other answers.
This commit is contained in:
2026-10-11 05:10:02 +02:00
parent ba7b455719
commit 10ab959d0f
10 changed files with 382 additions and 33 deletions
+29 -10
View File
@@ -15,24 +15,34 @@ accepted only on the operator's Approve, given on a channel that proves who answ
| Verb | Takes | Answers |
|---|---|---|
| `ask` | `question`, `bound` | the records and clauses bearing on the question, ranked; each clause says whether it holds, what changed it, or that it is only drafted; each record says what changed it since and how to read its reasoning |
| `ask` | `question` (at most 500 characters), `bound` | the records and clauses bearing on the question, ranked; each clause says whether it holds, what changed it, or that it is only drafted; each record says what changed it since and how to read its reasoning |
| `get` | `number`, `kind`, `reasoning`, `section`, `version`, `bound` | one record: clauses, links both ways, what changed it since, versions, reviews, the operator's ask; its reasoning only when asked |
| `holds` | `topic` or `number`, `bound` | the clauses in force for a topic of the reading order or for one record; never a drafted clause |
| `links` | `number`, `kind` | the record's typed links, out and in |
| `history` | `number`, `kind`, `from`, `to` | every version; with `from` and `to`, the difference between the two, line by line |
| `history` | `number`, `kind`, `from`, `to`, `bound` | every version; with `from` and `to`, the difference between the two, line by line |
| `reserve` | `kind`, `for` | the next decision or research number, asked of the issue tracker (`issue-tracker.reserve`); spent whether or not its record is accepted |
| `propose` | the record whole | proposed, or refused naming every rule it breaks; an accepted record changes only by a progressive insight (`insight`) |
| `review` | `number`, `verdict` (approve or changes), `findings` | accepted, asked of the operator, or kept proposed with the findings |
| `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 |
`ask`, `get` and `holds` stay within `bound` bytes (4096 unless asked for, at most 65536) and say what they cut
`ask`, `get`, `holds` and `history` stay within `bound` bytes (4096 unless asked for, at most 65536) and say what they cut
and how to read the rest. The module's own tool `knowledge_health` answers its health (below).
**Who acted** is the caller as the runtime proves it, never an argument: no verb takes one. The runtime does not
hand a module its caller yet (hq issue 365), so every change is recorded as by `unproven`. Until it does,
**every review is refused**. A review by the record's author is refused, and with no proven caller a review
cannot be told from one by the author.
**Who acted** is the caller as the runtime proves it, never an argument: no verb takes one. Today the runtime
does not hand a module its caller. The SDK's tool call carries only a name and arguments, so proving the caller
needs an SDK change first (hq issue 365). Until then every change is recorded as by `unproven`. In this module
the caller is read in one place, `callerOf` in `main.go`, so the SDK change touches that one function.
The rules about callers:
- A proposal is revised only by its author, as the runtime proves them.
- A number reserved by a proven caller is proposed only by that caller.
- A review is refused from anyone who wrote any version of the record.
- A review is refused for a record that has a version whose writer was unproven: who wrote it cannot be told
from who reviews it, so its author proposes it again under a new number.
Until the runtime proves callers, **every review is refused**.
## What a record holds
@@ -61,6 +71,16 @@ record it goes out from.
superseder, a graduated effort says what it became, and the links in its text resolve;
- 4 on the prose: no word the glossary retired, no daemon's line, a deferral names its tracking issue, and a
progressive insight in the dated, marked form.
A rule holds a record from the day hq's checks began to hold it (`RuleFrom` in `rules.go`):
- retired words from 2026-10-07;
- daemons' lines and deferrals from 2026-10-09;
- the clauses from 2026-10-11;
- the decision record's sections and the live-citation rule, which no hq check held on a decision record,
from 2026-10-11, the day the knowledge base takes the write.
A progressive insight keeps its record's date, so a correction to an older record is not refused by a rule
that never held it. A new record is dated no earlier than the day its number was reserved.
3. Revise it by proposing again; every version is kept.
4. A `review` by someone else either approves it or asks for changes. On approve the record and its clauses are
accepted and in force. Every clause it changes moves to `changed`, and every record it supersedes moves to
@@ -125,9 +145,8 @@ the consolidated schema for a fresh database is `schema.sql`. A test holds the t
## What it needs at release
- assignment to the control-node, and a postgres provider for its `postgres-database` provision;
- `issue-tracker.reserve` (hq issue 424) before `reserve` works; until then reserve says the tracker did not hand
out a number, and writes nothing;
- the runtime proving the caller (hq issue 365) before any review is taken;
- the issue tracker serving `issue-tracker.reserve` (hq issue 424, merged as mesh-catalog#204) on the mesh;
- the SDK and the runtime proving the caller (hq issue 365) before any review is taken;
- the import run once, then checked against hq record by record (hq issue 426).
## Tests
@@ -13,6 +13,9 @@ import (
"strings"
)
// MostQuestion is the most characters a question to ask may hold.
const MostQuestion = 500
// The bound of an answer.
const (
DefaultBound = 4 << 10
@@ -159,6 +162,9 @@ func (k *KB) Ask(question string, bound int) (*Asked, error) {
if question == "" {
return nil, fmt.Errorf("ask needs a question, in plain words")
}
if len([]rune(question)) > MostQuestion {
return nil, fmt.Errorf("a question is at most %d characters; this one is %d: ask it in fewer words", MostQuestion, len([]rune(question)))
}
bound = boundOf(bound)
hits, err := k.Store.Search(ctx, question, 80)
if err != nil {
@@ -600,10 +606,12 @@ type Historied struct {
To int `json:"to,omitempty"`
Diff []string `json:"diff,omitempty"`
Same bool `json:"same,omitempty"`
Cut string `json:"cut,omitempty"`
Bound int `json:"bound,omitempty"`
}
// History is a record's versions, and the difference between two of them, line by line.
func (k *KB) History(key Key, from, to int) (*Historied, error) {
func (k *KB) History(key Key, from, to, bound int) (*Historied, error) {
ctx := context.Background()
e, err := k.Store.Get(ctx, key)
if err != nil {
@@ -622,22 +630,72 @@ func (k *KB) History(key Key, from, to int) (*Historied, error) {
Status: v.Record.Status, Insight: v.Insight, Served: v.N == e.CurrentVersion, Pending: v.N == e.PendingVersion})
}
if from == 0 && to == 0 {
out.Bound = boundOf(bound)
versions := out.Versions
v, m := fit(len(versions), out.Bound-300, func(m int) any { c := *out; c.Versions = versions[len(versions)-m:]; return &c })
*out = *(v.(*Historied))
if m < len(versions) {
out.Cut = fmt.Sprintf("the %d oldest versions are not listed within %d bytes: raise bound", len(versions)-m, out.Bound)
}
return out, nil
}
if from < 1 || to < 1 || from > len(vs) || to > len(vs) {
return nil, fmt.Errorf("%s has versions 1 to %d; from and to name two of them", key, len(vs))
}
out.From, out.To = from, to
out.Diff = Diff(Render(vs[from-1].Record), Render(vs[to-1].Record))
out.Same = len(out.Diff) == 0
out.From, out.To, out.Bound = from, to, boundOf(bound)
diff := Diff(Render(vs[from-1].Record), Render(vs[to-1].Record))
out.Same = len(diff) == 0
// Within the bound: the most of the difference's lines that fit, cut at a hunk's line and said; and when the
// versions alone do not fit, the oldest of them left out first.
versions := out.Versions
v, n := fit(len(diff), out.Bound-300, func(n int) any { c := *out; c.Diff = diff[:n]; return &c })
*out = *(v.(*Historied))
var cut []string
if n < len(diff) {
cut = append(cut, fmt.Sprintf("%d of the difference's %d lines are not shown within %d bytes: raise bound, or compare nearer versions",
len(diff)-n, len(diff), out.Bound))
}
if size(out) > out.Bound-300 {
v, m := fit(len(versions), out.Bound-300, func(m int) any { c := *out; c.Versions = versions[len(versions)-m:]; return &c })
*out = *(v.(*Historied))
cut = append(cut, fmt.Sprintf("the %d oldest versions are not listed", len(versions)-m))
}
out.Cut = strings.Join(cut, "; ")
return out, nil
}
// MostDiffWork bounds the work of a difference: the lines of the two texts' changed middles multiplied. Past it,
// the middle is given whole as removed and added — still exact, only less narrow.
const MostDiffWork = 4_000_000
// Diff is the difference of two texts, line by line: each hunk headed "@@ -a,n +b,m @@", its lines "-" removed
// and "+" added, by the longest common subsequence of their lines.
func Diff(a, b string) []string {
x, y := strings.Split(a, "\n"), strings.Split(b, "\n")
// The common head and tail cost nothing to find; only the middle between them is compared.
head := 0
for head < len(x) && head < len(y) && x[head] == y[head] {
head++
}
tail := 0
for tail < len(x)-head && tail < len(y)-head && x[len(x)-1-tail] == y[len(y)-1-tail] {
tail++
}
x, y = x[head:len(x)-tail], y[head:len(y)-tail]
n, m := len(x), len(y)
if n*m > MostDiffWork {
var out []string
if n+m > 0 {
out = append(out, fmt.Sprintf("@@ -%d,%d +%d,%d @@", head+1, n, head+1, m))
}
for _, l := range x {
out = append(out, "-"+l)
}
for _, l := range y {
out = append(out, "+"+l)
}
return out
}
lcs := make([][]int, n+1)
for i := range lcs {
lcs[i] = make([]int, m+1)
@@ -670,14 +728,14 @@ func Diff(a, b string) []string {
j++
case i < n && (j == m || lcs[i+1][j] >= lcs[i][j+1]):
if len(hunk) == 0 {
ha, hb, ca, cb = i+1, j+1, 0, 0
ha, hb, ca, cb = head+i+1, head+j+1, 0, 0
}
hunk = append(hunk, "-"+x[i])
ca++
i++
default:
if len(hunk) == 0 {
ha, hb, ca, cb = i+1, j+1, 0, 0
ha, hb, ca, cb = head+i+1, head+j+1, 0, 0
}
hunk = append(hunk, "+"+y[j])
cb++
@@ -0,0 +1,79 @@
package main
import (
"strings"
"testing"
)
// A rule holds a record from the day hq's checks began to hold it (the review of mesh-catalog#206): a dated
// correction to an older record is judged by the rules that held it then, never by one that came later.
// insightTo is a progressive insight to a held record: the record as it stands, resent whole, with what stood
// before.
func insightTo(t *testing.T, w *world, k Key) ProposeAsk {
t.Helper()
r := mustGet(t, w.k.Store, k).Current
p := ProposeAsk{Kind: k.Kind, Number: k.Number, Title: r.Title, Topic: r.Topic, Date: r.Date, Deciders: r.Deciders,
Sections: r.Sections, Homes: r.Homes, Insight: "the count said 12 where 13 stood"}
for _, c := range r.Clauses {
p.Clauses = append(p.Clauses, c.Text)
}
for _, l := range r.Links {
p.Links = append(p.Links, LinkAsk{Kind: l.Kind, To: l.To.String(), From: l.From})
}
return p
}
// ADR 0011 (2026-04-03) is a real older record: its decision is prose, it has no section saying how it is checked,
// and it was written before the words and clause rules held. A correction of fact to it is taken.
func TestAnInsightToAnOlderRecordIsJudgedByTheRulesThatHeldIt(t *testing.T) {
w := newWorld(t, newMemStore(), hqMain(t))
if _, err := w.k.Import("import@test", ImportAsk{}, hqMain(t)); err != nil {
t.Fatal(err)
}
p := insightTo(t, w, Key{KindDecision, 11})
p.Sections[0].Text += "\n\nThe control plane wrote them."
if _, err := w.k.Propose("corrector@ace", p); err != nil {
t.Fatalf("an insight to ADR 0011 was refused by rules that never held it: %v", err)
}
// The same words in a record of today are refused.
n := w.reserved(t, "author@g14")
q := valid(n)
q.Sections[2].Text = "The control plane wrote them."
if _, err := w.k.Propose("author@g14", q); err == nil || !strings.Contains(err.Error(), "- retired-word:") {
t.Fatalf("a retired word in a record of today: %v", err)
}
}
func TestEachDatedRuleHoldsFromItsDay(t *testing.T) {
for _, c := range []struct {
rule, before, from string
}{
{"retired-word", "2026-10-06", "2026-10-07"},
{"daemon-line", "2026-10-08", "2026-10-09"},
{"deferral", "2026-10-08", "2026-10-09"},
{"has-clause", "2026-10-10", "2026-10-11"},
{"clause-lead", "2026-10-10", "2026-10-11"},
{"checked", "2026-10-10", "2026-10-11"},
} {
if Holds(c.rule, c.before) || !Holds(c.rule, c.from) {
t.Errorf("%s holds a record of %s: %v, of %s: %v", c.rule, c.before, Holds(c.rule, c.before), c.from, Holds(c.rule, c.from))
}
}
if !Holds("title", "2026-01-01") {
t.Error("an undated rule does not hold an old record")
}
}
// A new record cannot be dated back past the rules: it is of the day its number was reserved, or later.
func TestANewRecordIsNotDatedBeforeItsReservation(t *testing.T) {
w := imported(t, newMemStore())
n := w.reserved(t, "author@g14")
p := valid(n)
p.Date = "2026-01-01"
p.Sections[2].Text = "The control plane wrote them."
_, err := w.k.Propose("author@g14", p)
if err == nil || !strings.Contains(err.Error(), "- date:") {
t.Fatalf("a new record dated before its reservation: %v", err)
}
}
@@ -22,7 +22,7 @@ func TestThreeVersionsAreKeptAndTheFirstAndThirdCompareExactly(t *testing.T) {
if _, err := w.k.Propose("author@g14", p); err != nil { // version 3
t.Fatal(err)
}
h, err := w.k.History(Key{KindDecision, n}, 0, 0)
h, err := w.k.History(Key{KindDecision, n}, 0, 0, 0)
if err != nil {
t.Fatal(err)
}
@@ -30,7 +30,7 @@ func TestThreeVersionsAreKeptAndTheFirstAndThirdCompareExactly(t *testing.T) {
h.Versions[2].Why != "the bound named" || !h.Versions[2].Served || h.Versions[0].By != "author@g14" {
t.Fatalf("history: %+v", h.Versions)
}
d, err := w.k.History(Key{KindDecision, n}, 1, 3)
d, err := w.k.History(Key{KindDecision, n}, 1, 3, 0)
if err != nil {
t.Fatal(err)
}
@@ -50,7 +50,7 @@ func TestThreeVersionsAreKeptAndTheFirstAndThirdCompareExactly(t *testing.T) {
t.Errorf("the title's change is not in the difference: %v", d.Diff)
}
// Versions 2 and 3 differ in clause 2 alone.
d23, _ := w.k.History(Key{KindDecision, n}, 2, 3)
d23, _ := w.k.History(Key{KindDecision, n}, 2, 3, 0)
var changed []string
for _, l := range d23.Diff {
if !strings.HasPrefix(l, "@@") {
@@ -61,7 +61,7 @@ func TestThreeVersionsAreKeptAndTheFirstAndThirdCompareExactly(t *testing.T) {
"+2. **A batch is delivered by one walk.** At most one commit per repository."}) {
t.Fatalf("2 to 3: %v", d23.Diff)
}
if d22, _ := w.k.History(Key{KindDecision, n}, 2, 2); !d22.Same || len(d22.Diff) != 0 {
if d22, _ := w.k.History(Key{KindDecision, n}, 2, 2, 0); !d22.Same || len(d22.Diff) != 0 {
t.Fatalf("a version against itself: %+v", d22)
}
})
@@ -110,3 +110,42 @@ func TestDiffIsExactOnSmallTexts(t *testing.T) {
}
}
}
// history is bounded like the other answers: its work, and its answer, with a cut notice (the review of #206).
func TestHistoryIsBounded(t *testing.T) {
w := imported(t, newMemStore())
n := w.reserved(t, "author@g14")
p := valid(n)
big := strings.Repeat("A line of reasoning that is long enough to count.\n", 3000)
p.Sections[0].Text = big
if _, err := w.k.Propose("author@g14", p); err != nil {
t.Fatal(err)
}
p.Sections[0].Text = strings.ReplaceAll(big, "long enough", "longer than")
if _, err := w.k.Propose("author@g14", p); err != nil {
t.Fatal(err)
}
h, err := w.k.History(Key{KindDecision, n}, 2, 3, 0)
if err != nil {
t.Fatal(err)
}
if size(h) > DefaultBound || h.Cut == "" {
t.Fatalf("a large difference: %d bytes, cut %q", size(h), h.Cut)
}
// The difference stays exact when it is not cut, however large: its work is bounded, not its truth.
whole, _ := w.k.History(Key{KindDecision, n}, 2, 3, MostBound)
vs, _ := w.k.Store.Versions(t.Context(), Key{KindDecision, n})
if whole.Cut == "" && applyDiff(Render(vs[1].Record), whole.Diff) != Render(vs[2].Record) {
t.Fatal("the bounded difference is not exact")
}
if d := Diff(strings.Repeat("a\n", 5000)+"x", strings.Repeat("b\n", 5000)+"y"); applyDiff(strings.Repeat("a\n", 5000)+"x", d) != strings.Repeat("b\n", 5000)+"y" {
t.Fatal("a difference past the work bound is not exact")
}
}
func TestAQuestionIsBounded(t *testing.T) {
w := imported(t, newMemStore())
if _, err := w.k.Ask(strings.Repeat("merge window ", 200), 0); err == nil || !strings.Contains(err.Error(), "at most") {
t.Fatalf("a question of 2600 characters: %v", err)
}
}
@@ -293,12 +293,20 @@ func (k *KB) Propose(caller string, a ProposeAsk) (*Proposed, error) {
switch w := held.Working(); {
case held.Current.Status == StatusReserved:
r.ReservedBy, r.ReservedAt, r.ReservedFor = held.Current.ReservedBy, held.Current.ReservedAt, held.Current.ReservedFor
if proven(r.ReservedBy) && caller != r.ReservedBy {
return nil, fmt.Errorf("%s was reserved by %s: only who reserved a number proposes under it. Nothing was proposed", key, r.ReservedBy)
}
if why == "" {
why = "proposed"
}
case w.Status == StatusProposed && held.Pending == nil:
// A revision of a proposal: a new version, its earlier reviews kept with the versions they reviewed.
if !proven(caller) || caller != w.Author {
return nil, fmt.Errorf("%s is proposed by %s: only its author, proven by the runtime, revises it (this call is by %s). "+
"Nothing was proposed", key, w.Author, caller)
}
r.Author, r.Reviews, r.ReservedBy, r.ReservedAt, r.ReservedFor = w.Author, w.Reviews, w.ReservedBy, w.ReservedAt, w.ReservedFor
r.Writers = w.Writers
if a.Insight != "" {
refusals = append(refusals, Refusal{Rule: "reserved", Says: "a proposal not yet accepted is revised, not corrected by an insight"})
}
@@ -312,6 +320,10 @@ func (k *KB) Propose(caller string, a ProposeAsk) (*Proposed, error) {
}
current = false
r.Author = caller
r.Writers = append([]string{}, held.Current.Writers...)
if held.Current.Author != "" && !contains(r.Writers, held.Current.Author) {
r.Writers = append(r.Writers, held.Current.Author)
}
r.AcceptedAt = held.Current.AcceptedAt
r.Pending = &PendingState{Insight: Insight{Date: k.now().Format("2006-01-02"), Before: strings.TrimSpace(a.Insight), By: caller}}
r.Date = held.Current.Date
@@ -323,6 +335,14 @@ func (k *KB) Propose(caller string, a ProposeAsk) (*Proposed, error) {
return nil, fmt.Errorf("%s is %s and takes no proposal; nothing was proposed", key, held.Working().Status)
}
}
if current && held != nil && !held.Current.ReservedAt.IsZero() && r.Date < held.Current.ReservedAt.Format("2006-01-02") {
// A new record is of its own day: a date before its reservation would put it under rules older records keep.
refusals = append(refusals, Refusal{Rule: "date", Says: fmt.Sprintf("a new record is dated from the day its number was reserved, %s, not %s",
held.Current.ReservedAt.Format("2006-01-02"), r.Date)})
}
if !contains(r.Writers, caller) {
r.Writers = append(r.Writers, caller)
}
if !current && a.Kind == KindResearch && r.Effort == "" {
r.Effort = held.Current.Status
}
@@ -401,9 +421,22 @@ func (k *KB) Review(caller string, a ReviewAsk) (*Reviewed, error) {
"author, and none is taken until it does. Nothing was reviewed", key)
}
w := held.Working().Clone()
if caller == w.Author {
return nil, fmt.Errorf("%s was written by %s, who called this review: a record is reviewed by someone who did not write it. "+
"Nothing was reviewed", key, caller)
// The record's writers; an imported record's are hq's, reviewed there, and none of them is named here.
var writers []string
for _, by := range append([]string{w.Author}, w.Writers...) {
if by != "" {
writers = append(writers, by)
}
}
for _, by := range writers {
if !proven(by) {
return nil, fmt.Errorf("%s has a version written while the runtime proved no caller, so who wrote it cannot be told from "+
"who reviews it: its author is to propose it again under a new number. Nothing was reviewed", key)
}
}
if contains(writers, caller) {
return nil, fmt.Errorf("%s has a version %s wrote, who called this review: a record is reviewed by someone who wrote none "+
"of it. Nothing was reviewed", key, caller)
}
reviewed := held.CurrentVersion
if held.Pending != nil {
@@ -103,8 +103,10 @@ func serve(k *KB, status string) {
}
}
// callerOf is who called, as the runtime proves it. The runtime does not hand a module its caller yet (hq issue
// 365), so it is Unproven; it is never read from the call's arguments, so no caller can name another.
// callerOf is who called, as the runtime proves it: the one place the caller is read, so the change that brings it
// is this one function. The runtime does not hand a module its caller yet (hq issue 365): the SDK's tool call carries
// a name and arguments only, so the SDK must first pass the runtime's proven caller to a tool. Until it does, every
// caller is Unproven. It is never read from the call's arguments, so no caller can name another.
func callerOf(map[string]any) string { return Unproven }
// hear subscribes to the router's word on this module's asks, asked again until the runtime takes it.
@@ -638,7 +640,8 @@ func Tools(k *KB, caller func(map[string]any) string) []stdio.Tool {
"it is in force, what changed or superseded it, or that it is only drafted and not the record's word; each record says " +
"what changed it since and how to read its reasoning. One call, within a bound it states (4 KiB unless asked for more), " +
"saying what it cut.",
Input: schema([]string{"question"}, map[string]any{"question": text("the question, in plain words"), "bound": boundArg()}),
Input: schema([]string{"question"}, map[string]any{"question": map[string]any{"type": "string", "maxLength": MostQuestion,
"description": "the question, in plain words, at most 500 characters"}, "bound": boundArg()}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("ask", m, "question", "bound")
if err != nil {
@@ -736,11 +739,12 @@ func Tools(k *KB, caller func(map[string]any) string) []stdio.Tool {
}},
{Name: Seat + ".history",
Description: "A record's versions, every one kept: who made it, when, why, and whether it was a progressive insight; with " +
"from and to, the difference between those two versions, line by line.",
"from and to, the difference between those two versions, line by line. Within a bound it states, 4 KiB unless asked for " +
"more, saying what it cut.",
Input: schema([]string{"number"}, map[string]any{"kind": kindArg(), "number": integer("the record's number"),
"from": integer("the earlier version to compare"), "to": integer("the later version to compare")}),
"from": integer("the earlier version to compare"), "to": integer("the later version to compare"), "bound": boundArg()}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("history", m, "kind", "number", "from", "to")
a, err := readArgs("history", m, "kind", "number", "from", "to", "bound")
if err != nil {
return nil, err
}
@@ -756,7 +760,11 @@ func Tools(k *KB, caller func(map[string]any) string) []stdio.Tool {
if err != nil {
return nil, err
}
return k.History(key, from, to)
b, err := a.num("bound", false)
if err != nil {
return nil, err
}
return k.History(key, from, to, b)
}},
{Name: Seat + ".reserve",
Description: "Reserve the next decision or research number for a record you are about to propose, from the issue " +
@@ -279,7 +279,9 @@ type Record struct {
// Author is who proposed it, as the runtime proves the caller; Source names the commit and path of hq an
// imported record was read from.
Author string `json:"author,omitempty"`
Source string `json:"source,omitempty"`
// Writers are everyone who wrote a version of it, as the runtime proved them: none of them reviews it.
Writers []string `json:"writers,omitempty"`
Source string `json:"source,omitempty"`
// Reserved is who reserved the number, when, and for what title.
ReservedBy string `json:"reserved-by,omitempty"`
ReservedAt time.Time `json:"reserved-at,omitempty"`
@@ -29,7 +29,7 @@ func TestAReviewByTheAuthorIsRefused(t *testing.T) {
w := imported(t, s)
n := proposed(t, w, "author@g14", false)
_, err := w.k.Review("author@g14", ReviewAsk{Kind: KindDecision, Number: n, Verdict: VerdictApprove})
if err == nil || !strings.Contains(err.Error(), "someone who did not write it") {
if err == nil || !strings.Contains(err.Error(), "someone who wrote none") {
t.Fatalf("a review by the author: %v", err)
}
if e := mustGet(t, s, Key{KindDecision, n}); e.Current.Status != StatusProposed || len(e.Current.Reviews) != 0 {
@@ -121,7 +121,9 @@ func allText(r *Record) string {
func JudgeRecord(r *Record, held *Entry, lookup Lookup, ref *Reference) []Refusal {
var out []Refusal
refuse := func(rule, format string, a ...any) {
out = append(out, Refusal{Rule: rule, Says: fmt.Sprintf(format, a...)})
if Holds(rule, r.Date) {
out = append(out, Refusal{Rule: rule, Says: fmt.Sprintf(format, a...)})
}
}
decision := r.Kind == KindDecision
if r.Kind != KindDecision && r.Kind != KindResearch {
@@ -531,3 +533,30 @@ func JudgeSet(records []*Record) []Finding {
}
return out
}
// RuleFrom is the day each dated rule began to hold a record, as hq's checks hold it: a record dated earlier was
// never held to it, and a dated correction of it (a progressive insight, which keeps the record's date) is not.
// words.py held the retired words from 2026-10-07 and the daemons' lines from 2026-10-09; cycle.py the deferrals
// from 2026-10-09; records.py the clauses from 2026-10-11 (ADR 0290 rule 3). The decision record's sections, and
// a superseded record cited alone, were held by no check of hq's (words.py and live-citation never read decision
// records): they hold from the day the knowledge base takes the write, 2026-10-11. Every other rule holds every
// record, as hq's checks held it.
var RuleFrom = map[string]string{
"retired-word": "2026-10-07",
"daemon-line": "2026-10-09",
"deferral": "2026-10-09",
"has-clause": "2026-10-11",
"clause-numbering": "2026-10-11",
"clause-lead": "2026-10-11",
"clause-size": "2026-10-11",
"context": "2026-10-11",
"consequences": "2026-10-11",
"checked": "2026-10-11",
"live-citation": "2026-10-11",
}
// Holds says whether a rule holds a record of this date. A record with no day is held to every rule.
func Holds(rule, date string) bool {
from, dated := RuleFrom[rule]
return !dated || !isDay.MatchString(date) || date >= from
}
@@ -0,0 +1,82 @@
package main
import (
"strings"
"testing"
)
// Who may write and review (the review of mesh-catalog#206): a proposal is revised only by its author, proven; a
// number reserved by a proven caller is proposed only by that caller; a review is refused from anyone who wrote
// any version of the record, and for a record whose author was never proven.
func TestARevisionIsTakenOnlyFromTheRecordsAuthor(t *testing.T) {
w := imported(t, newMemStore())
n := proposed(t, w, "author@g14", false)
p := valid(n)
p.Sections[2].Text = "Merges land a little sooner."
if _, err := w.k.Propose("other@novox", p); err == nil || !strings.Contains(err.Error(), "only its author") {
t.Fatalf("a revision by someone else: %v", err)
}
if _, err := w.k.Propose(Unproven, p); err == nil || !strings.Contains(err.Error(), "only its author") {
t.Fatalf("a revision by an unproven caller: %v", err)
}
if mustGet(t, w.k.Store, Key{KindDecision, n}).Versions != 2 {
t.Fatal("a refused revision wrote a version")
}
if _, err := w.k.Propose("author@g14", p); err != nil {
t.Fatalf("the author's revision: %v", err)
}
}
func TestAReservedNumberIsProposedOnlyByWhoReservedIt(t *testing.T) {
w := imported(t, newMemStore())
n := w.reserved(t, "author@g14")
if _, err := w.k.Propose("other@novox", valid(n)); err == nil || !strings.Contains(err.Error(), "reserved by author@g14") {
t.Fatalf("a proposal under another's number: %v", err)
}
if _, err := w.k.Propose("author@g14", valid(n)); err != nil {
t.Fatal(err)
}
}
// A writer of any version, not only the first, does not review it.
func TestAReviewByAnyoneWhoWroteAVersionIsRefused(t *testing.T) {
w := imported(t, newMemStore())
n := w.reserved(t, "author@g14")
if _, err := w.k.Propose("author@g14", valid(n)); err != nil {
t.Fatal(err)
}
// An insight to an accepted record: its writers are the record's and the insight's.
if _, err := w.k.Review("reviewer@novox", ReviewAsk{Kind: KindDecision, Number: n, Verdict: VerdictApprove}); err != nil {
t.Fatal(err)
}
p := valid(n)
p.Sections[2].Text = "Merges land sooner, by a minute."
p.Insight = "it said only that merges land sooner"
if _, err := w.k.Propose("corrector@ace", p); err != nil {
t.Fatal(err)
}
for _, writer := range []string{"author@g14", "corrector@ace"} {
if _, err := w.k.Review(writer, ReviewAsk{Kind: KindDecision, Number: n, Verdict: VerdictApprove}); err == nil ||
!strings.Contains(err.Error(), "wrote") {
t.Fatalf("a review by %s, who wrote a version: %v", writer, err)
}
}
if _, err := w.k.Review("reviewer@novox", ReviewAsk{Kind: KindDecision, Number: n, Verdict: VerdictApprove}); err != nil {
t.Fatal(err)
}
}
// A record proposed while callers were unproven is never reviewable, even once callers are proven: its real
// author cannot be told from its reviewer. It is proposed again.
func TestARecordProposedUnprovenIsNeverReviewed(t *testing.T) {
w := imported(t, newMemStore())
n := proposed(t, w, Unproven, false)
_, err := w.k.Review("reviewer@novox", ReviewAsk{Kind: KindDecision, Number: n, Verdict: VerdictApprove})
if err == nil || !strings.Contains(err.Error(), "propose it again") {
t.Fatalf("a review of a record whose author is unproven: %v", err)
}
if mustGet(t, w.k.Store, Key{KindDecision, n}).Current.Status != StatusProposed {
t.Fatal("accepted")
}
}