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
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:
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user