Compare commits

...
5 Commits
Author SHA1 Message Date
jschoubben d11e2423ff Merge pull request 'G start: the runner admits first-view seat verbs before the catalog half is read; the SDK declares the verbs it calls' (#195) from fix/47-G-start into main
Reviewed-on: #195
2026-11-13 19:07:59 +00:00
jschoubben ab355879f0 Bootstrap the seat mesh-delivery so a runner admits its source before the seat list is read (A-362)
The catalog half is read through mesh-delivery's source as well as mesh-catalog's verbs, and a runner with an unread
seat list admitted only the latter. The bootstrapped copy is mesh-delivery main 21eaa55b's contract file, byte for
byte; the tests' seat list and the captured beside copy take the same file.

Builds: design 47 G lead answers A-362, A-353

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-13 17:45:44 +01:00
jschoubben affde04396 Declare the seat verbs the Go package calls on a module's behalf (A-357)
Every module built against the SDK reads its catalog half through mesh-catalog's registrations, revision and entry and
mesh-delivery's source; naming them in the Go package's calls lets the indexer order such a module after the running
versions that serve its start.

Builds: design 47 G lead answer A-357 (the SDK half), §47 kind 6

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-13 17:30:03 +01:00
jschoubben d6ca425737 Let a tool runner's catalog-half reads through its gate while that half is unread (A-353)
A runner serving the indexer's tools refused every call until its own catalog half was read, and that read is a call of
the same seat verbs through the same gate: G could not start. The four seat verbs the catalog half is read by now run
unmarked while it is unread; every other call waits and is refused as before.

Builds: design 47 G lead answers A-353, A-359

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-13 17:28:58 +01:00
mesh-tools cf6bc7612f Merge pull request 'Milestone 47 G: mesh-sdk's part of the one release' (#194) from feat/47-G-join12 into main 2026-11-13 13:08:12 +00:00
9 changed files with 2578 additions and 11 deletions
+53
View File
@@ -2,10 +2,13 @@ package approval
import ( import (
"encoding/json" "encoding/json"
"errors"
"strings" "strings"
"testing" "testing"
"time"
"git.novox.be/novox/mesh-sdk/go/operatorask" "git.novox.be/novox/mesh-sdk/go/operatorask"
"git.novox.be/novox/mesh-sdk/go/seatdef"
) )
// call is a fixture call of the marked tool wipe. // call is a fixture call of the marked tool wipe.
@@ -78,3 +81,53 @@ func TestTheActAndItsAsk(t *testing.T) {
t.Errorf("headline %q", got) t.Errorf("headline %q", got)
} }
} }
// unread is a catalog half whose first view is not read yet.
type unread struct{}
func (unread) Ready() error { return errors.New("the registrations are not answered") }
func (unread) Revision() string { return "" }
func (unread) Served(string) (Entry, bool, string) { return Entry{}, false, "not read" }
func (unread) Newest(string) (Entry, bool) { return Entry{}, false }
func (unread) Definitions() map[string]seatdef.Definition { return nil }
func (unread) Entries() []Entry { return nil }
// Design 47 G lead answers A-353, A-359: while its catalog half is unread, the gate lets through the four seat verbs that
// half is read by, told from the call's seat and verb, and holds every other call as before.
func TestAnUnreadCatalogHalfLetsItsOwnReadsThrough(t *testing.T) {
waited := 0
g := New(Deps{Node: "n1", Runner: "node-tools", Catalog: unread{}, Wait: func(time.Duration) error {
waited++
return errors.New("still unread")
}})
for _, tc := range []struct {
seat, name string
runs bool
}{
{"mesh-catalog", "registrations", true},
{"mesh-catalog", "revision", true},
{"mesh-catalog", "entry", true},
{"mesh-delivery", "source", true},
{"mesh-catalog", "register", false},
{"mesh-delivery", "registrations", false},
{"", "registrations", false}, // a module's tool of that name is no seat verb
{"", "revision", false},
} {
ran := false
address := tc.seat + "." + tc.name
if tc.seat == "" {
address = "mesh-delivery-indexer." + tc.name
}
_, err := g.Run(Call{Address: address, Module: "mesh-delivery-indexer", Seat: tc.seat, Name: tc.name, Fingerprint: "fp1",
Caller: "node-tools"}, func() (any, error) { ran = true; return "ok", nil })
if ran != tc.runs || (err == nil) != tc.runs {
t.Errorf("%s: ran %v (%v), want ran %v", address, ran, err, tc.runs)
}
if !tc.runs && (err == nil || !strings.Contains(err.Error(), "are not read yet")) {
t.Errorf("%s: refused with %v, want the marks not read yet", address, err)
}
}
if waited != 4 {
t.Errorf("the other calls waited for the catalog half %d times, want 4 (the four start reads wait for nothing)", waited)
}
}
+17 -2
View File
@@ -3,6 +3,7 @@ package approval
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"slices"
"strings" "strings"
"git.novox.be/novox/mesh-sdk/go/operatorask" "git.novox.be/novox/mesh-sdk/go/operatorask"
@@ -67,13 +68,27 @@ func (g *Gate) Run(c Call, run func() (any, error)) (any, error) {
// Binding is a Store that says whether the runner's membership binds the state now (§13). // Binding is a Store that says whether the runner's membership binds the state now (§13).
type Binding interface{ Bound() bool } type Binding interface{ Bound() bool }
// marksOf is the module's marks as the catalog half has them now; before its first view, the call waits for it within // startReads are the seat verbs the catalog half is read by, as `<seat>.<verb>`: while it is unread the gate lets them
// RecordWithin and is then judged, refused naming mesh_first_views when it is still unread (§19). // through, so the runner that serves them answers the reads its own catalog half waits on (design 47 G lead answers
// A-353, A-359).
var startReads = []string{"mesh-catalog.registrations", "mesh-catalog.revision", "mesh-catalog.entry", "mesh-delivery.source"}
// startRead says the call is one of startReads, told from its seat and verb.
func startRead(c Call) bool {
return c.Seat != "" && slices.Contains(startReads, c.Seat+"."+c.Name)
}
// marksOf is the module's marks as the catalog half has them now; before its first view, a call of startReads runs
// unmarked (A-353, A-359) and any other waits for it within RecordWithin and is then judged, refused naming
// mesh_first_views when it is still unread (§19).
func (g *Gate) marksOf(c Call) (Marks, error) { func (g *Gate) marksOf(c Call) (Marks, error) {
if g.d.Catalog == nil { if g.d.Catalog == nil {
return Marks{}, g.refused(c, "", "the tool runner reads no catalog half, so its marks are not read") return Marks{}, g.refused(c, "", "the tool runner reads no catalog half, so its marks are not read")
} }
if err := g.d.Catalog.Ready(); err != nil { if err := g.d.Catalog.Ready(); err != nil {
if startRead(c) {
return Marks{Module: c.Module}, nil
}
if g.d.Wait != nil { if g.d.Wait != nil {
err = g.d.Wait(RecordWithin) err = g.d.Wait(RecordWithin)
} }
+7
View File
@@ -35,6 +35,13 @@ func TestOwnManifestUsesNoWordTheRunningControllerRefuses(t *testing.T) {
if g := packages["go"]; g.Name != "go" || !slices.Equal(g.Paths, []string{"go"}) { if g := packages["go"]; g.Name != "go" || !slices.Equal(g.Paths, []string{"go"}) {
t.Errorf("mesh-sdk's Go package is %+v, want go built from go", g) t.Errorf("mesh-sdk's Go package is %+v, want go built from go", g)
} }
// The seat verbs its code calls on a module's behalf, in the invokes form, so the indexer orders every module built
// against it after the running versions that serve its start (design 47 G lead answer A-357).
for _, verb := range []string{"seat:mesh-catalog.registrations", "seat:mesh-catalog.revision", "seat:mesh-catalog.entry", "seat:mesh-delivery.source"} {
if !slices.Contains(packages["go"].Calls, verb) {
t.Errorf("mesh-sdk's Go package calls %v, want %s among them", packages["go"].Calls, verb)
}
}
// Its named paths, so the indexer holds them and a change to them is a changed package (design 47 G A-285). // Its named paths, so the indexer holds them and a change to them is a changed package (design 47 G A-285).
if l := packages["typescript"]; l.Name != "lib" || !slices.Equal(l.Paths, []string{"package.json", "package-lock.json", "tsconfig.json", "src"}) { if l := packages["typescript"]; l.Name != "lib" || !slices.Equal(l.Paths, []string{"package.json", "package-lock.json", "tsconfig.json", "src"}) {
t.Errorf("mesh-sdk's TypeScript package is %+v, want lib built from package.json, package-lock.json, tsconfig.json and src", l) t.Errorf("mesh-sdk's TypeScript package is %+v, want lib built from package.json, package-lock.json, tsconfig.json and src", l)
File diff suppressed because it is too large Load Diff
+1
View File
@@ -26,6 +26,7 @@ func TestEveryBootstrappedDefinitionEqualsItsContractsFile(t *testing.T) {
{"operator-channel", "mesh-contracts", "operator-and-conversation/seats/operator-channel/seat.json"}, {"operator-channel", "mesh-contracts", "operator-and-conversation/seats/operator-channel/seat.json"},
{"mesh-catalog", "mesh-delivery", "modules/mesh-delivery-contracts/seats/mesh-catalog/seat.json"}, {"mesh-catalog", "mesh-delivery", "modules/mesh-delivery-contracts/seats/mesh-catalog/seat.json"},
{"mesh-delivery-checker", "mesh-delivery", "modules/mesh-delivery-contracts/seats/mesh-delivery-checker/seat.json"}, {"mesh-delivery-checker", "mesh-delivery", "modules/mesh-delivery-contracts/seats/mesh-delivery-checker/seat.json"},
{"mesh-delivery", "mesh-delivery", "modules/mesh-delivery-contracts/seats/mesh-delivery/seat.json"},
} }
var have []string var have []string
for _, d := range Bootstrapped() { for _, d := range Bootstrapped() {
+8
View File
@@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"reflect" "reflect"
"slices"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -182,6 +183,13 @@ func TestTheBootstrapIsHeldAndNothingNewIsActedOnUntilTheSeatAnswers(t *testing.
if got := len(r.Definitions()); got != len(boot) { if got := len(r.Definitions()); got != len(boot) {
t.Fatalf("the reader holds %d definitions, the bootstrap has %d", got, len(boot)) t.Fatalf("the reader holds %d definitions, the bootstrap has %d", got, len(boot))
} }
// Design 47 G lead answers A-353, A-362: before the list is read the runner admits the seat verbs its catalog half is
// read by, mesh-delivery's source beside mesh-catalog's registrations, revision and entry.
for _, sv := range [][2]string{{"mesh-catalog", "registrations"}, {"mesh-catalog", "revision"}, {"mesh-catalog", "entry"}, {"mesh-delivery", "source"}} {
if !slices.ContainsFunc(r.Definitions()[sv[0]].Verbs, func(v seatdef.Verb) bool { return v.Name == sv[1] }) {
t.Errorf("with the seat list unread the reader holds no %s.%s", sv[0], sv[1])
}
}
st := r.Status() st := r.Status()
if st.Heard || len(st.Waiting) != len(boot) || st.LastError == "" || st.Health() == "" { if st.Heard || len(st.Waiting) != len(boot) || st.LastError == "" || st.Health() == "" {
t.Fatalf("status %+v: want the seat not heard, every bootstrapped seat waiting, the error and the health said", st) t.Fatalf("status %+v: want the seat not heard, every bootstrapped seat waiting, the error and the health said", st)
@@ -101,18 +101,15 @@
}, },
{ {
"name": "source", "name": "source",
"description": "One watched branch's source revision (design 47 *Watched branches and the catalog graph per branch*: source.<branch> in place of source.main): The source revision the gate holds: every repository it names, its trunk branch and that trunk's head as the gate merged or seeded it, and the group that moved it last. Read, never written: the node-engines read it to call a reverted build's catch-up tool once per repository, never the gate's state (design 47).", "description": "One watched branch's source revision (design 47 *Watched branches and the catalog graph per branch*: source.<branch> in place of source.main): The source revision the gate holds: every repository it names, its trunk branch and that trunk's head as the gate merged or seeded it, and the group that moved it last. Read, never written: the node-engines read it to call a reverted build's catch-up tool once per repository, never the gate's state (design 47). Given no branch it answers branches alone, the branch of each source key the gate keeps; given a branch, that branch's source revision (design 47 G section 690).",
"input": { "input": {
"type": "object", "type": "object",
"properties": { "properties": {
"branch": { "branch": {
"type": "string", "type": "string",
"description": "the watched branch, escaped as a key name is" "description": "the watched branch, escaped as a key name is; absent: the answer is the list of the branches the gate keeps a source revision of (design 47 G section 690)"
} }
}, }
"required": [
"branch"
]
}, },
"answer": { "answer": {
"type": "object", "type": "object",
@@ -139,6 +136,13 @@
"branch": { "branch": {
"type": "string", "type": "string",
"description": "the watched branch the source revision is of" "description": "the watched branch the source revision is of"
},
"branches": {
"type": "array",
"items": {
"type": "string"
},
"description": "given no branch: the branch name of each source.<branch> key the gate keeps, which every reader of the catalog half follows before the first registrations (design 47 G section 614, section 690)"
} }
} }
}, },
@@ -304,6 +308,22 @@
"type": "boolean", "type": "boolean",
"description": "a merge made at the forge, with no Approve" "description": "a merge made at the forge, with no Approve"
}, },
"repository": {
"type": "string",
"description": "for a forge-made record, the repository, <owner>/<name>"
},
"pull": {
"type": "integer",
"description": "for a forge-made record, the pull request's number"
},
"commit": {
"type": "string",
"description": "for a forge-made record, the merge commit"
},
"branch": {
"type": "string",
"description": "for a forge-made record, the branch the merge moved"
},
"at": { "at": {
"type": "string", "type": "string",
"format": "date-time", "format": "date-time",
@@ -429,6 +449,14 @@
"type": "string", "type": "string",
"description": "the repository" "description": "the repository"
}, },
"module": {
"type": "string",
"description": "the module the merge changed, from the delivery record"
},
"branch": {
"type": "string",
"description": "the watched branch the merge moved"
},
"commit": { "commit": {
"type": "string", "type": "string",
"description": "the merge commit" "description": "the merge commit"
@@ -453,6 +481,65 @@
"why" "why"
] ]
} }
},
{
"name": "approval-carried",
"subject": "approval-carried.<repository>.<pull>",
"keep": {
"max-age-days": 90
},
"body": {
"type": "object",
"description": "an Approve given before G, carried as that pull request's approval step: said at G's switch for each merges key the old bridge had not performed (design 47 G 542)",
"properties": {
"repository": {
"type": "string",
"description": "the repository"
},
"pull": {
"type": "integer",
"description": "the pull request's number",
"minimum": 1
},
"commit": {
"type": "string",
"description": "the head the Approve named"
},
"approver": {
"type": "string",
"description": "who approved: the operator"
},
"at": {
"type": "string",
"description": "when it was said, RFC 3339",
"format": "date-time"
}
},
"required": [
"repository",
"pull",
"commit",
"approver",
"at"
]
}
},
{
"name": "pins-stated",
"body": {
"type": "object",
"description": "the gate states every pin it holds once its pinned events are said, so an indexer whose state pins was made later hears them all; kept as pinned is, on the seat's family (design 47 G 579, 652)",
"properties": {
"at": {
"type": "string",
"description": "when it was said, RFC 3339",
"format": "date-time"
}
},
"required": [
"at"
]
}
} }
], ],
"states": [ "states": [
@@ -992,9 +1079,24 @@
"catalog": { "catalog": {
"type": "array", "type": "array",
"items": { "items": {
"type": "integer" "type": "object",
"description": "one catalog graph revision, named by its watched branch and its number on the seat mesh-catalog (revisions are numbered per branch)",
"properties": {
"branch": {
"type": "string",
"description": "the watched branch the revision belongs to"
},
"revision": {
"type": "integer",
"description": "its number on that branch"
}
},
"required": [
"branch",
"revision"
]
}, },
"description": "catalog graph revisions, by their number on the seat mesh-catalog" "description": "catalog graph revisions, by watched branch and number on the seat mesh-catalog"
}, },
"assignment": { "assignment": {
"type": "array", "type": "array",
+2 -1
View File
@@ -5,7 +5,8 @@
"build": { "build": {
"artifacts": [ "artifacts": [
{ "name": "lib", "kind": "package", "language": "typescript", "paths": ["package.json", "package-lock.json", "tsconfig.json", "src"], "calls": ["finding-calls"] }, { "name": "lib", "kind": "package", "language": "typescript", "paths": ["package.json", "package-lock.json", "tsconfig.json", "src"], "calls": ["finding-calls"] },
{ "name": "go", "kind": "package", "language": "go", "paths": ["go"], "calls": ["finding-calls", "ledger-upkeep-calls", "put-sessions", "sent-objects-calls"] } { "name": "go", "kind": "package", "language": "go", "paths": ["go"], "calls": ["finding-calls", "ledger-upkeep-calls", "put-sessions", "sent-objects-calls",
"seat:mesh-catalog.registrations", "seat:mesh-catalog.revision", "seat:mesh-catalog.entry", "seat:mesh-delivery.source"] }
] ]
}, },
"resources": [] "resources": []