Merge pull request 'Nothing reads decision records from hq: records_decisions retired, reserve asks the knowledge base, an empty import refuses (hq issue 488)' (#214) from fix/488-nothing-reads-records-from-hq into main
This commit was merged in pull request #214.
This commit is contained in:
@@ -28,6 +28,11 @@ operator, it is accepted only on the operator's Approve, given on a channel that
|
||||
`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).
|
||||
|
||||
Its tool `knowledge_highest` answers the highest decision and research number it holds, every number spent
|
||||
counted (accepted, proposed, reserved or withdrawn): the issue tracker's `reserve` starts and raises its counters
|
||||
above them (hq issue 488). It refuses while nothing is held. It is a tool of the module, not a verb of the seat,
|
||||
which serves the verbs ADR 0290 rule 6 names.
|
||||
|
||||
**Who acted** is the caller as the runtime proves it, never an argument: no verb takes one. The tool runner takes
|
||||
the caller's bus user from the caller-named subject a call arrived on and hands it through the SDK
|
||||
(`stdio.CallerOf`, hq issue 365). A call on the subject that names no caller (served beside it for one release, hq
|
||||
@@ -123,7 +128,8 @@ as text. `get` serves each, and any other field a record of hq wrote under `fiel
|
||||
## The import
|
||||
|
||||
`import` reads hq through the records module at the commit its checkout stands at. With `commit`, it refuses a
|
||||
checkout standing elsewhere. It writes every decision record and research effort whole, as version 1, naming the
|
||||
checkout standing elsewhere. At a commit where hq holds no decision record and no research effort (the records
|
||||
have left it, hq issue 426), it refuses and says so, rather than answering "nothing created" as a success. It writes every decision record and research effort whole, as version 1, naming the
|
||||
commit and path it came from. It creates only what is absent and never overwrites. It never asks for a number, so
|
||||
no counter moves. A second run changes nothing and says so. `dry-run` writes nothing. From a terminal,
|
||||
`mesh-knowledge dry-run <hq checkout>` gives the same dry run over a checkout on disk, against an empty knowledge
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package main
|
||||
|
||||
// The highest number held per kind (hq issue 488): what the issue tracker's reserve starts and raises its decision
|
||||
// and research counters above (ADR 0275 rule 8, ADR 0290 rule 5), now that the records live here and hq's folders
|
||||
// empty. Every number held counts — accepted, proposed, reserved or withdrawn — since each is spent.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
)
|
||||
|
||||
// HighestTool is the module's tool the issue tracker asks.
|
||||
const HighestTool = "knowledge_highest"
|
||||
|
||||
// Highest is the highest number held of each kind, and how many records are held.
|
||||
type Highest struct {
|
||||
Decision int `json:"decision"`
|
||||
Research int `json:"research"`
|
||||
Records int `json:"records"`
|
||||
Says string `json:"says"`
|
||||
}
|
||||
|
||||
// HighestHeld is the highest number held per kind. A knowledge base that holds no record answers none: a counter
|
||||
// started from it would start at 1.
|
||||
func (k *KB) HighestHeld() (*Highest, error) {
|
||||
all, err := k.Store.All(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the knowledge base cannot be read (%v); it answers no highest number", err)
|
||||
}
|
||||
h := &Highest{Records: len(all)}
|
||||
for _, e := range all {
|
||||
switch r := e.Current; r.Kind {
|
||||
case KindDecision:
|
||||
h.Decision = max(h.Decision, r.Number)
|
||||
case KindResearch:
|
||||
h.Research = max(h.Research, r.Number)
|
||||
}
|
||||
}
|
||||
if h.Records == 0 {
|
||||
return nil, errors.New("the knowledge base holds no record yet (it is imported from hq once): it answers no highest number")
|
||||
}
|
||||
h.Says = fmt.Sprintf("The highest numbers held, of %d records, every number spent counted: decision %04d, research %03d.",
|
||||
h.Records, h.Decision, h.Research)
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func highestTool(k *KB) stdio.Tool {
|
||||
return stdio.Tool{Name: HighestTool,
|
||||
Description: "The highest decision number and the highest research number the knowledge base holds, every number spent " +
|
||||
"counted: accepted, proposed, reserved or withdrawn. The issue tracker starts and raises its counters above them. " +
|
||||
"Refused while the knowledge base holds no record, or cannot be read: a number is never guessed.",
|
||||
Input: schema(nil, map[string]any{}),
|
||||
Run: func(m map[string]any) (any, error) {
|
||||
if _, err := readArgs(HighestTool, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return k.HighestHeld()
|
||||
}}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// hq issue 488: the issue tracker's reserve takes the highest decision and research number from the knowledge base,
|
||||
// which holds every record — reserved, withdrawn and proposed numbers included, since each is spent — rather than
|
||||
// reading hq's folders, which empty when the records leave.
|
||||
|
||||
func TestTheHighestToolAnswersTheHighestNumberHeldPerKind(t *testing.T) {
|
||||
eachStore(t, func(t *testing.T, s Store) {
|
||||
w := imported(t, s)
|
||||
tool := highestTool(w.k)
|
||||
answer := func() Highest {
|
||||
t.Helper()
|
||||
out, err := tool.Run(map[string]any{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, _ := json.Marshal(out)
|
||||
var h Highest
|
||||
if err := json.Unmarshal(raw, &h); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return h
|
||||
}
|
||||
if h := answer(); h.Decision != 290 || h.Research != 45 || h.Records != 11 {
|
||||
t.Fatalf("%+v", h)
|
||||
}
|
||||
// A number reserved, and later withdrawn, is spent: it is the highest.
|
||||
w.reserved(t, "author@g14") // 300
|
||||
w.clock = w.clock.Add(DefaultReservedFor + time.Hour)
|
||||
if _, err := w.k.Withdraw(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h := answer(); h.Decision != 300 || h.Research != 45 {
|
||||
t.Fatalf("after a reservation withdrawn: %+v", h)
|
||||
}
|
||||
if _, err := tool.Run(map[string]any{"kind": "decision"}); err == nil {
|
||||
t.Fatal("an argument it does not take was taken")
|
||||
}
|
||||
})
|
||||
// A knowledge base that holds nothing answers nothing: a 0 would start a counter at 1.
|
||||
empty := newWorld(t, newMemStore(), fixture())
|
||||
if _, err := highestTool(empty.k).Run(map[string]any{}); err == nil || !strings.Contains(err.Error(), "holds no record") {
|
||||
t.Fatalf("an empty knowledge base: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// hq issue 488: once the records have left hq, an import at a later commit finds none. It refuses, saying so,
|
||||
// rather than reporting nothing created as a success.
|
||||
func TestTheImportRefusesACommitWhereHqHoldsNoRecords(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
write := func(path, text string) {
|
||||
t.Helper()
|
||||
if err := writeFileAll(dir+"/"+path, text); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
write("COMMIT", "0123456789abcdef0123456789abcdef01234567\n")
|
||||
write("02-DECISIONS/README.md", "# Decisions\n\nThey live in the knowledge base.\n")
|
||||
write("01-RESEARCH/README.md", "# Research\n")
|
||||
w := newWorld(t, newMemStore(), dirRecords{dir: dir})
|
||||
for _, dry := range []bool{true, false} {
|
||||
out, err := w.k.Import("import@test", ImportAsk{DryRun: dry}, dirRecords{dir: dir})
|
||||
if err == nil || !strings.Contains(err.Error(), "holds no decision record or research effort") ||
|
||||
!strings.Contains(err.Error(), "0123456789ab") {
|
||||
t.Fatalf("dry %v: %+v %v", dry, out, err)
|
||||
}
|
||||
}
|
||||
if all, _ := w.k.Store.All(context.Background()); len(all) != 0 {
|
||||
t.Fatal("written")
|
||||
}
|
||||
}
|
||||
|
||||
func writeFileAll(path, text string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, []byte(text), 0o644)
|
||||
}
|
||||
@@ -127,6 +127,12 @@ func (k *KB) Import(caller string, a ImportAsk, records Records) (*Imported, err
|
||||
cands = append(cands, &importCandidate{path: ResearchFolder + "/" + f})
|
||||
}
|
||||
}
|
||||
if len(cands) == 0 {
|
||||
// The records left hq (hq issue 426): what it held is here, and an import from it would say "nothing
|
||||
// created" as a success (hq issue 488).
|
||||
return nil, fmt.Errorf("hq holds no decision record or research effort at commit %s: its %s and %s hold none. "+
|
||||
"The records live in the knowledge base now, and nothing was imported", short(commit), DecisionsFolder, ResearchFolder)
|
||||
}
|
||||
// Read and parse, a few at a time, every document at the one commit.
|
||||
var moved error
|
||||
var movedMu sync.Mutex
|
||||
|
||||
@@ -98,7 +98,7 @@ func serve(k *KB, status string) {
|
||||
k.kick = make(chan struct{}, 1)
|
||||
go hear(k)
|
||||
go background(k)
|
||||
if err := stdio.Serve("", append(Tools(k, callerOf), healthTool(status, k.now))); err != nil {
|
||||
if err := stdio.Serve("", append(Tools(k, callerOf), healthTool(status, k.now), highestTool(k))); err != nil {
|
||||
logf("%v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ func TestItsProcessesAreThePreparationTheActAndTheJudge(t *testing.T) {
|
||||
}
|
||||
h, _ := watch["health"].(map[string]any)
|
||||
if watch == nil || watch["run"].([]any)[1] != "watch" || h["kind"] != "tool" || h["tool"] != HealthTool ||
|
||||
!reflect.DeepEqual(m.Tools, []string{HealthTool}) {
|
||||
!reflect.DeepEqual(m.Tools, []string{HealthTool, HighestTool}) {
|
||||
t.Fatalf("watch %v, tools %v", watch, m.Tools)
|
||||
}
|
||||
if watch["env"].(map[string]any)["MESH_KNOWLEDGE_STATUS"] != m.Build.Artifacts[0].Env["MESH_KNOWLEDGE_STATUS"] {
|
||||
|
||||
@@ -51,7 +51,8 @@
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
"knowledge_health"
|
||||
"knowledge_health",
|
||||
"knowledge_highest"
|
||||
],
|
||||
"emits": [
|
||||
"reserved",
|
||||
|
||||
Reference in New Issue
Block a user