Add the checker's owed bucket, the mesh/check status context and the previews layout, and correct the checker header: the checker decides the status, the bridge sets it (issue 1673)

This commit is contained in:
2026-10-21 19:21:22 +02:00
parent 6d0c2076a3
commit b944d07158
4 changed files with 386 additions and 4 deletions
+5 -3
View File
@@ -9,9 +9,11 @@ import (
)
// The checker's keys (novox/hq ADR 0447 clauses 1 to 3, ADR 0444, ADR 0446 clause 5, ADR 0453 clause 4): the verdicts of
// its checks and the passing verdicts it keeps per package and level. The checker writes them from R4c, dark; the
// forge bridge writes the status `mesh/check` from a verdict (unrequired until R4f), the gate reads it from R4f, and
// the view and the watchman read them (a running check carries a pulse).
// its checks and the passing verdicts it keeps per package and level. The checker writes them from R4c, dark, and
// writes the status `mesh/check` it decides as an owed act in its own bucket (CheckerOwedBucket, the gate's owed shape);
// the forge bridge, the only part that calls the forge, performs that act (unrequired until R4f), once per revision of
// the key (ADR 0484 clause 2). The gate reads the status from R4f, and the view and the watchman read the verdicts (a
// running check carries a pulse).
//
// **The verdict's key is the group and the heads, not the fingerprints** (the lead's limit for R4a). Research 063
// section 01 drew `verdicts.<g>.check.<fpset>`; ADR 0447 clause 1 judges a pull request's head merged onto its
+188
View File
@@ -328,3 +328,191 @@ func TestVerdictOldAndNewReadersAgree(t *testing.T) {
t.Errorf("the old reader of a new value: %+v, %v", ov, err)
}
}
func sampleCheckerOwed() Owed {
return Owed{Act: OwedStatus, Context: StatusContextCheck, State: "success", Target: "https://mesh.example/delivery/check/36",
Text: "pass 4b5491557853 on aaaa: unit 12 run 301 kept, integration 3 run 40 kept; composition: not yet run"}
}
// 1673/AC1: the checker's owed bucket is the checker module plus _owed, and its key is the gate's owed key with the act
// status=mesh/check.
func TestCheckerOwedKeyAndBucket(t *testing.T) {
if CheckerOwedBucket != "mesh-delivery-checker_owed" || CheckerOwedHistory != 5 || PreviewsBucket != "mesh-delivery-checker_previews" || PreviewsHistory != 5 {
t.Errorf("buckets %s %d %s %d", CheckerOwedBucket, CheckerOwedHistory, PreviewsBucket, PreviewsHistory)
}
key, err := CheckerOwedKey("novox/mesh-sdk", 36, "aaaa")
if err != nil || key != "novox/mesh-sdk.36.aaaa.status=mesh/check" {
t.Fatalf("key = %q, %v", key, err)
}
repo, number, head, err := ParseCheckerOwedKey(key)
if err != nil || repo != "novox/mesh-sdk" || number != 36 || head != "aaaa" {
t.Errorf("parse = %q %d %q %v", repo, number, head, err)
}
other, _ := OwedKey("novox/mesh-sdk", 36, "aaaa", "status=mesh/merge-gate")
for _, bad := range []string{other, "novox/mesh-sdk.36.aaaa.view", "novox/mesh-sdk.36.aaaa.note", "novox/mesh-sdk.36.aaaa.comment.1", ""} {
if _, _, _, err := ParseCheckerOwedKey(bad); err == nil {
t.Errorf("checker owed key %q accepted", bad)
}
}
if _, err := CheckerOwedKey("nope", 36, "aaaa"); err == nil {
t.Error("a bad repository accepted")
}
}
// 1673/AC2: an owed value with the context mesh/check is accepted and an unknown context is refused, in the act and in the value.
func TestOwedAcceptsMeshCheckAndRefusesAnUnknownContext(t *testing.T) {
for _, ctx := range []string{StatusContextCheck, StatusContextMergeGate} {
if kind, got, err := ParseOwedAct("status=" + ctx); err != nil || kind != OwedStatus || got != ctx {
t.Errorf("ParseOwedAct(status=%s) = %q %q %v", ctx, kind, got, err)
}
}
for _, bad := range []string{"status=mesh/other", "status=check", "status=mesh/Check"} {
if _, err := OwedKey("novox/mesh-sdk", 36, "aaaa", bad); err == nil || !strings.Contains(err.Error(), bad[len("status="):]) {
t.Errorf("owed act %q: %v, want a refusal naming the context", bad, err)
}
}
data, err := EncodeCheckerOwed(sampleCheckerOwed())
if err != nil {
t.Fatal(err)
}
if got, err := DecodeCheckerOwed(data); err != nil || !SameOwed(got, sampleCheckerOwed()) && got.Context != StatusContextCheck {
t.Errorf("round trip = %+v, %v", got, err)
}
unknown := sampleCheckerOwed()
unknown.Context = "mesh/other"
if _, err := EncodeOwed(unknown); err == nil || !strings.Contains(err.Error(), "mesh/other") {
t.Errorf("an owed status of the context mesh/other: %v", err)
}
// The checker's bucket holds only its own status.
gate := sampleCheckerOwed()
gate.Context = StatusContextMergeGate
for name, o := range map[string]Owed{"the gate's context": gate, "a view": {Act: OwedView, Text: "x"}, "a note": {Act: OwedNote, Text: "x", Ref: "mesh-plan"}} {
if _, err := EncodeCheckerOwed(o); err == nil {
t.Errorf("the checker's bucket takes %s", name)
}
}
for _, state := range []string{"pending", "success", "failure"} {
o := sampleCheckerOwed()
o.State = state
if _, err := EncodeCheckerOwed(o); err != nil {
t.Errorf("state %s: %v", state, err)
}
}
}
func samplePreview() Preview {
return Preview{Repository: "novox/mesh-delivery", Head: "aaaa", Trunk: "bbbb", Merge: "cccc",
Modules: map[string]PreviewModule{
"modules/mesh-delivery-checker": {Tree: "t1", Manifest: strings.Repeat("1", 64), Locks: map[string]string{"go.sum": strings.Repeat("2", 64)}},
"modules/mesh-delivery-view": {Tree: "t2", Manifest: strings.Repeat("3", 64)},
}}
}
// 1673/AC3: the previews key is <repository>.<head>.<trunk> and parses back.
func TestPreviewKey(t *testing.T) {
key, err := PreviewKey("novox/mesh-delivery", "aaaa", "bbbb")
if err != nil || key != "novox/mesh-delivery.aaaa.bbbb" {
t.Fatalf("key = %q, %v", key, err)
}
repo, head, trunk, err := ParsePreviewKey(key)
if err != nil || repo != "novox/mesh-delivery" || head != "aaaa" || trunk != "bbbb" {
t.Errorf("parse = %q %q %q %v", repo, head, trunk, err)
}
for _, bad := range [][3]string{{"nope", "aaaa", "bbbb"}, {"novox/x", "", "bbbb"}, {"novox/x", "aaaa", ""}, {"novox/x", "a.b", "bbbb"}, {"novox/x", "aaaa", "b b"}} {
if _, err := PreviewKey(bad[0], bad[1], bad[2]); err == nil {
t.Errorf("PreviewKey%v accepted", bad)
}
}
for _, bad := range []string{"", "novox/x.aaaa", "novox/x.aaaa.bbbb.cccc", "nope.aaaa.bbbb", "novox/x..bbbb", "novox/x.aaaa."} {
if _, _, _, err := ParsePreviewKey(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") {
t.Errorf("ParsePreviewKey(%q) = %v, want a refusal naming it", bad, err)
}
}
}
// 1673/AC4: a preview round trips, and an invalid one is refused naming the field.
func TestPreviewRoundTripAndRefusals(t *testing.T) {
data, err := EncodePreview(samplePreview())
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(string(data), `{"v":1,`) {
t.Errorf("at %s", data)
}
got, err := DecodePreview(data)
want := samplePreview()
want.V = Version
if err != nil || !SamePreview(got, want) {
t.Errorf("round trip = %+v, %v", got, err)
}
if _, err := EncodePreview(Preview{Repository: "novox/x", Head: "aaaa", Trunk: "bbbb", Merge: "cccc"}); err != nil {
t.Errorf("a preview of a head that changes no module refused: %v", err)
}
cases := map[string]struct {
edit func(*Preview)
words []string
}{
"no repository": {func(p *Preview) { p.Repository = "" }, []string{"repository"}},
"a bad repository": {func(p *Preview) { p.Repository = "nope" }, []string{"nope"}},
"no head": {func(p *Preview) { p.Head = "" }, []string{"head"}},
"no trunk": {func(p *Preview) { p.Trunk = "" }, []string{"trunk"}},
"no merge": {func(p *Preview) { p.Merge = "" }, []string{"merge"}},
"a module with no tree": {func(p *Preview) {
p.Modules["modules/mesh-delivery-view"] = PreviewModule{Manifest: strings.Repeat("3", 64)}
}, []string{"modules/mesh-delivery-view", "tree"}},
"a bad manifest digest": {func(p *Preview) {
p.Modules["modules/mesh-delivery-view"] = PreviewModule{Tree: "t2", Manifest: "xyz"}
}, []string{"modules/mesh-delivery-view", "xyz"}},
"an empty directory": {func(p *Preview) { p.Modules[""] = PreviewModule{Tree: "t", Manifest: strings.Repeat("3", 64)} }, []string{"directory"}},
"a lock with no hash": {func(p *Preview) {
p.Modules["modules/mesh-delivery-view"] = PreviewModule{Tree: "t2", Manifest: strings.Repeat("3", 64), Locks: map[string]string{"go.sum": ""}}
}, []string{"modules/mesh-delivery-view", "go.sum"}},
}
for name, c := range cases {
p := samplePreview()
c.edit(&p)
_, err := EncodePreview(p)
if err == nil {
t.Errorf("%s accepted", name)
continue
}
for _, w := range c.words {
if !strings.Contains(err.Error(), w) {
t.Errorf("%s: error %q does not name %q", name, err, w)
}
}
}
if _, err := DecodePreview([]byte(`{"v":2,"repository":"novox/x"}`)); !errors.Is(err, ErrUnknownVersion) {
t.Errorf("a preview of version 2: %v", err)
}
if _, err := DecodePreview([]byte(`{"v":1,"repository":"novox/x","head":"a","trunk":"b"}`)); err == nil {
t.Error("decoded a preview with no merge")
}
}
// 1673/AC5: a reader of today's checker shapes ignores a field it does not know, and the checker's owed value is the
// gate's shape, so the gate's reader reads it (as indexer2_test.go does for the indexer's values).
func TestCheckerOwedIsReadableByTheGatesReaderAndAnOldOwedReaderIgnoresNewFields(t *testing.T) {
data, err := EncodeCheckerOwed(sampleCheckerOwed())
if err != nil {
t.Fatal(err)
}
var old struct {
V int `json:"v"`
Act string `json:"act"`
Context string `json:"context"`
State string `json:"state"`
}
if err := json.Unmarshal(data, &old); err != nil || old.Act != OwedStatus || old.Context != "mesh/check" {
t.Errorf("the old reader: %+v %v", old, err)
}
withNew := strings.Replace(string(data), `{"v":1,`, `{"v":1,"a-field-of-tomorrow":7,`, 1)
if _, err := DecodeOwed([]byte(withNew)); err != nil {
t.Errorf("an owed value with a field of tomorrow: %v", err)
}
p, _ := EncodePreview(samplePreview())
withNew = strings.Replace(string(p), `{"v":1,`, `{"v":1,"a-field-of-tomorrow":7,`, 1)
if _, err := DecodePreview([]byte(withNew)); err != nil {
t.Errorf("a preview with a field of tomorrow: %v", err)
}
}
+25 -1
View File
@@ -28,6 +28,17 @@ const (
AnswersHistory = 5
)
// The contexts of the statuses the mesh owes the forge: the gate's, the delivery's view of a head and of a group
// (design 47), and the checker's, which is not required until R4f (ADR 0484 clause 2, ADR 0453 clause 4).
const (
StatusContextMergeGate = "mesh/merge-gate"
StatusContextDelivery = "mesh/delivery"
StatusContextDeliveryGroup = "mesh/delivery-group"
StatusContextCheck = "mesh/check"
)
var statusContexts = []string{StatusContextMergeGate, StatusContextDelivery, StatusContextDeliveryGroup, StatusContextCheck}
// AskKindMerge is the kind of the gate's ask for a group's merge, the first part of its key.
const AskKindMerge = "merge"
@@ -131,6 +142,16 @@ func AskingID(askKey string, asking int) string {
return AskID(askKey + "." + strconv.Itoa(asking))
}
// checkStatusContext refuses a status context the mesh does not owe the forge.
func checkStatusContext(context string) error {
for _, c := range statusContexts {
if context == c {
return nil
}
}
return fmt.Errorf("delivery: the status context %q is not one of %s", context, strings.Join(statusContexts, ", "))
}
func checkOwedAct(act string) (kind, context string, err error) {
kind, context, hasContext := strings.Cut(act, "=")
switch kind {
@@ -138,7 +159,7 @@ func checkOwedAct(act string) (kind, context string, err error) {
if !hasContext {
return "", "", fmt.Errorf("delivery: the owed act %q is a status without its context, status=<context>", act)
}
if err := checkName("status context", context); err != nil {
if err := checkStatusContext(context); err != nil {
return "", "", err
}
return kind, context, nil
@@ -345,6 +366,9 @@ func (o Owed) check() error {
if o.Context == "" {
return fmt.Errorf("delivery: an owed status has no context")
}
if err := checkStatusContext(o.Context); err != nil {
return err
}
if o.Target == "" {
return fmt.Errorf("delivery: an owed status has no target")
}
+168
View File
@@ -0,0 +1,168 @@
package delivery
import (
"encoding/json"
"fmt"
"reflect"
"strings"
)
// The checker's other buckets (ADR 0484 clauses 2 and 3): owed, the acts it owes the forge, which the forge bridge
// performs once per revision of a key, and previews, what the fetch step answered for a head merged onto a trunk commit.
const (
// CheckerOwedBucket is the checker's bucket of owed acts, in the gate's owed shape: a status with the context
// StatusContextCheck for each head it judged. One part writes each key: the checker.
CheckerOwedBucket = CheckerModule + "_owed"
// CheckerOwedHistory is the revisions the checker's owed bucket keeps per key.
CheckerOwedHistory = 5
// PreviewsBucket is the checker's bucket of merge previews; the indexer reads it for the candidates' fingerprints.
PreviewsBucket = CheckerModule + "_previews"
// PreviewsHistory is the revisions the previews bucket keeps per key.
PreviewsHistory = 5
)
// CheckerOwedKey is the key, in CheckerOwedBucket, of the status the checker owes on a pull request at a head: the
// gate's OwedKey with the act `status=mesh/check`, the only act the checker owes.
func CheckerOwedKey(repository string, number int, head string) (string, error) {
return OwedKey(repository, number, head, OwedStatus+"="+StatusContextCheck)
}
// ParseCheckerOwedKey is the inverse of CheckerOwedKey; it refuses an owed key of any other act.
func ParseCheckerOwedKey(key string) (repository string, number int, head string, err error) {
repository, number, head, act, err := ParseOwedKey(key)
if err != nil {
return "", 0, "", err
}
if act != OwedStatus+"="+StatusContextCheck {
return "", 0, "", fmt.Errorf("delivery: %q is not a checker owed key: the checker owes only status=%s, not %q", key, StatusContextCheck, act)
}
return repository, number, head, nil
}
// EncodeCheckerOwed is o as JSON at Version; it refuses what EncodeOwed refuses and any act but a status with the
// context StatusContextCheck.
func EncodeCheckerOwed(o Owed) ([]byte, error) {
if err := checkCheckerOwed(o); err != nil {
return nil, err
}
return EncodeOwed(o)
}
func checkCheckerOwed(o Owed) error {
if o.Act != OwedStatus || o.Context != StatusContextCheck {
return fmt.Errorf("delivery: the checker owes only a status with the context %s, not the act %q with the context %q", StatusContextCheck, o.Act, o.Context)
}
return nil
}
// DecodeCheckerOwed reads an owed act of the checker's bucket.
func DecodeCheckerOwed(data []byte) (Owed, error) {
o, err := DecodeOwed(data)
if err != nil {
return Owed{}, err
}
if err := checkCheckerOwed(o); err != nil {
return Owed{}, err
}
return o, nil
}
// PreviewKey is the key, in PreviewsBucket, of a head merged onto a trunk commit: `<repository>.<head>.<trunk>`.
func PreviewKey(repository, head, trunk string) (string, error) {
repo, err := escapeRepository(repository)
if err != nil {
return "", err
}
if err := checkName("head", head); err != nil {
return "", err
}
if err := checkName("trunk commit", trunk); err != nil {
return "", err
}
return repo + "." + head + "." + trunk, nil
}
// ParsePreviewKey is the inverse of PreviewKey.
func ParsePreviewKey(key string) (repository, head, trunk string, err error) {
parts := strings.Split(key, ".")
if len(parts) != 3 {
return "", "", "", fmt.Errorf("delivery: %q is not a preview key, <repository>.<head>.<trunk>", key)
}
repository, err = unescapeRepository(parts[0])
if err != nil {
return "", "", "", fmt.Errorf("delivery: %q is not a preview key: %w", key, err)
}
if _, err := PreviewKey(repository, parts[1], parts[2]); err != nil {
return "", "", "", fmt.Errorf("delivery: %q is not a preview key: %w", key, err)
}
return repository, parts[1], parts[2], nil
}
// PreviewModule is what the preview holds of one module directory: the tree sha of the directory, the digest of its
// manifest (the key in the manifests bucket) and the hex sha256 of its lock files by path relative to it.
type PreviewModule struct {
Tree string `json:"tree"`
Manifest string `json:"manifest"`
Locks map[string]string `json:"locks,omitempty"`
}
// Preview is the value of a preview key: the repository, the head and the trunk commit it was merged onto, the merge
// commit and, for each module directory of the merge, a PreviewModule.
type Preview struct {
V int `json:"v"`
Repository string `json:"repository"`
Head string `json:"head"`
Trunk string `json:"trunk"`
Merge string `json:"merge"`
Modules map[string]PreviewModule `json:"modules,omitempty"`
}
func (p Preview) check() error {
if _, err := PreviewKey(p.Repository, p.Head, p.Trunk); err != nil {
return err
}
if p.Merge == "" {
return fmt.Errorf("delivery: a preview names no merge commit")
}
for dir, m := range p.Modules {
if dir == "" {
return fmt.Errorf("delivery: a preview names a module with an empty directory")
}
if m.Tree == "" {
return fmt.Errorf("delivery: the preview's module %q has no tree", dir)
}
if _, err := ManifestsKey(m.Manifest); err != nil {
return fmt.Errorf("delivery: the preview's module %q: %w", dir, err)
}
for path, hash := range m.Locks {
if path == "" || hash == "" {
return fmt.Errorf("delivery: the lock %q of the preview's module %q has an empty path or hash", path, dir)
}
}
}
return nil
}
// EncodePreview is p as JSON at Version.
func EncodePreview(p Preview) ([]byte, error) {
if err := p.check(); err != nil {
return nil, err
}
p.V = Version
return json.Marshal(p)
}
// DecodePreview reads a preview.
func DecodePreview(data []byte) (Preview, error) {
var p Preview
if err := decode(data, &p); err != nil {
return Preview{}, err
}
if err := p.check(); err != nil {
return Preview{}, err
}
return p, nil
}
// SamePreview says whether two previews are the same value.
func SamePreview(a, b Preview) bool { return reflect.DeepEqual(a, b) }