Indexer: the seat mesh-catalog serves seat-definitions and says seat-definition-changed and -removed for trunks; a definition names its domain and a seat of the mesh's own contracts is taken only from its domain's module, a move there a handover in either order; seats listed from the module's seats/ directory, no index (D-29 verb, D-I3, D-I5, ADR 0636 §4, §5; issue 1966)

This commit is contained in:
2026-11-05 14:31:11 +01:00
parent a638776248
commit 4edc0ce90b
14 changed files with 503 additions and 77 deletions
@@ -5,6 +5,7 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/views"
"git.novox.be/novox/mesh-delivery/sdk/graph"
@@ -12,8 +13,9 @@ import (
)
// catalogRead reads the indexer's modules, manifests and graph states for the seat's verbs.
func catalogRead(modules, manifests, nodes stdio.KeptState) index.CatalogRead {
func catalogRead(modules, manifests, nodes stdio.KeptState, seatDefinitions, marks views.Source) index.CatalogRead {
return index.CatalogRead{
SeatDefinitions: seatDefinitions, Marks: marks,
Modules: func() (map[string]delivery.Module, error) {
keys, err := modules.Keys()
if err != nil {
@@ -74,5 +76,6 @@ func catalogRead(modules, manifests, nodes stdio.KeptState) index.CatalogRead {
// catalogTools are the seat's verbs over the indexer's own states, served beside the indexer's own tools, since the indexer's manifest claims the seat (ADR 0604 §2).
func catalogTools() []stdio.Tool {
return index.CatalogTools(catalogRead(stdio.State("modules"), stdio.State("manifests"), stdio.State(graph.State)))
return index.CatalogTools(catalogRead(stdio.State("modules"), stdio.State("manifests"), stdio.State(graph.State),
keptSource(index.SeatDefinitionsState), keptSource(views.MarksState)))
}
@@ -411,7 +411,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
Named: namedVia(calls.Call), Trunk: trunkVia(calls.Call), Contains: containsVia(calls.Call), Read: readVia(calls.Call),
ReadOpen: openVia(calls.Call), Packages: awaitedStore{packages}, Files: filesVia(calls.Call), GoSum: goSumVia(calls.Call), GoMod: goModVia(calls.Call),
OwnBuild: ownBuildVia(calls.Call, cred.Node), Graph: awaitedStore{graph},
SeatDefinitions: awaitedStore{seatDefs}, ReadFile: readFileVia(calls.Call), SaySeatDefinitionChanged: saySeatDefinitionChanged(js, cred),
SeatDefinitions: awaitedStore{seatDefs}, ReadFile: readFileVia(calls.Call), SaySeatDefinition: saySeatDefinition(js, cred),
Message: messageVia(calls.Call),
// The take-in step (ADR 0633): its state pending and its stream take-in, both this delivery's, bound when they appear;
// until then every merge is taken in at once, as before.
@@ -17,6 +17,7 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker"
@@ -80,8 +81,8 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
if seatDefs, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.SeatDefinitionsBucket, History: 5}); err != nil {
t.Fatal(err)
}
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "INDEXER_SEAT_DEFINITIONS",
Subjects: []string{"mesh.stream.mesh-delivery-indexer.event." + index.SeatDefinitionChangedEvent}}); err != nil {
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "SEAT_MESH_CATALOG",
Subjects: []string{"mesh.seat." + index.CatalogSeat + ".event.>"}}); err != nil {
t.Fatal(err)
}
// DLV-I19, I20: the indexer's findings and takeins states, and the checker's stream asks with the indexer's durable
@@ -212,9 +213,15 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
// request's head whose definition does not parse is kept as its entry naming why while the trunk's stays; it closes
// and its entry goes.
const contractsMerge, badHead = "5555555555555555555555555555555555555555", "6666666666666666666666666666666666666666"
git := `{"format":1,"seat":"git","scope":"mesh","grain":"seat-list","decision":"novox/hq ADR 0111","delivers":"git"}`
seatFiles[contractsMerge] = map[string]string{"seats/index.json": `{"format":1,"seats":["git"]}`, "seats/git/seat.json": git}
seatFiles[badHead] = map[string]string{"seats/index.json": `{"format":1,"seats":["git"]}`, "seats/git/seat.json": `{"format":1,"seat":"git"}`}
git := `{"format":1,"seat":"git","scope":"mesh","grain":"seat-list","decision":"novox/hq ADR 0111","domain":"Core","delivers":"git"}`
// No index names the seats: the indexer lists the module's `seats/` directory (ADR 0635 §6, D-I5).
seatFiles[contractsMerge] = map[string]string{"seats/git/seat.json": git}
seatFiles[badHead] = map[string]string{"seats/git/seat.json": `{"format":1,"seat":"git"}`}
// The indexer starts with no definition kept (only the bootstrapped seats are the readers'): the verb seat-definitions
// answers an empty list at once (ADR 0636 §4, §8).
if got, err := index.SeatDefinitionsOf(jsSource{ctx, seatDefs}, nil, ""); err != nil || func() string { raw, _ := json.Marshal(got); return string(raw) }() != `{"revision":0,"seats":[]}` {
t.Errorf("seat-definitions of an indexer that kept none = %+v, %v", got, err)
}
contracts := `{"module":"mesh-contracts","version":"1","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"],"series":"0.1","generated-from":"."}]}}`
merged("novox/mesh-contracts", 80, contractsMerge, forgemsg.Forge{Root: "dddddddddddddddddddddddddddddddddddddddd",
Trees: map[string]string{"": "dddddddddddddddddddddddddddddddddddddddd"}, Manifests: map[string]string{"": contracts}})
@@ -255,13 +262,17 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
t.Fatalf("%s is kept 20 s after its pull request closed", pk)
}
}
info, err := js.Stream(ctx, "INDEXER_SEAT_DEFINITIONS")
info, err := js.Stream(ctx, "SEAT_MESH_CATALOG")
if err != nil {
t.Fatal(err)
}
// Said: the trunk's git, and the closed pull request's removal; the unparsed head says nothing.
if n := info.CachedInfo().State.Msgs; n != 2 {
t.Errorf("seat-definition-changed said %d times, want 2", n)
// Said on the seat's subject: the trunk's git alone; a pull request's head (unparsed, then closed) says nothing (ADR 0636 §4).
if n := info.CachedInfo().State.Msgs; n != 1 {
t.Errorf("seat-definition-changed said %d times, want 1", n)
}
if m, err := info.GetLastMsgForSubject(ctx, "mesh.seat.mesh-catalog.event.seat-definition-changed"); err != nil ||
!strings.Contains(string(m.Data), `"seat":"git"`) || !strings.Contains(string(m.Data), `"source":"trunk"`) || !strings.Contains(string(m.Data), `"digest":"`+trunk.Digest+`"`) {
t.Errorf("seat-definition-changed = %v, %v; want {seat, source, digest} of the trunk's git", m, err)
}
// DLV-I20 [i]: a pull request's check verdict naming out-of-step files keeps them; a trunk build end of the builder's log
@@ -311,7 +322,7 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
// DLV-I19 [i]: indexer_seat_definitions_again for a pull request, asked through the takeins state as the tool asks it,
// reads the head with its own manifest; indexer_seat_definition then answers the pull request's entry beside the trunk's.
const goodHead = "7777777777777777777777777777777777777777"
seatFiles[goodHead] = map[string]string{"module.json": contracts, "seats/index.json": `{"format":1,"seats":["git"]}`,
seatFiles[goodHead] = map[string]string{"module.json": contracts,
"seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0200", 1)}
key, value, err := index.NewSeatDefinitionsAgain("mesh-contracts", 83, goodHead, "the forge did not answer", "person.op", time.Now())
if err != nil {
@@ -326,6 +337,65 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
a.Entry.Commit != goodHead || a.Trunk == nil || a.Trunk.Digest != trunk.Digest {
t.Errorf("indexer_seat_definition of the pull request = %+v, %v", a, err)
}
// ADR 0636 §5, D-I3, D-I5: a seat of the mesh's own contracts is taken only from its domain's contracts module, and a move
// there is a handover in either merge order of the two trunks. `wants` (Placement) and `vault` (Identity and access) are
// held by mesh-contracts as an older indexer kept them.
seat := func(name, domain string) string {
return `{"format":1,"seat":"` + name + `","scope":"mesh","grain":"seat-list","decision":"d","domain":"` + domain + `"}`
}
for name, domain := range map[string]string{"wants": "Placement", "vault": "Identity and access"} {
var d seatdef.Definition
if err := json.Unmarshal([]byte(seat(name, domain)), &d); err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(d)
held, _ := json.Marshal(index.SeatDefinitionEntry{Seat: name, Source: index.SourceTrunk, Repository: "novox/mesh-contracts", Module: "mesh-contracts",
Directory: ".", Commit: "old", Digest: "old-" + name, Definition: raw})
if _, err := seatDefs.Put(ctx, name+".trunk", held); err != nil {
t.Fatal(err)
}
}
const dropped, placement, identity = "8888888888888888888888888888888888888888", "9999999999999999999999999999999999999999", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
seatFiles[dropped] = map[string]string{"seats/git/seat.json": git} // mesh-contracts without wants and vault
seatFiles[placement] = map[string]string{"seats/wants/seat.json": seat("wants", "Placement")}
seatFiles[identity] = map[string]string{"seats/vault/seat.json": seat("vault", "Identity and access")}
domainModule := func(name string) string {
return strings.Replace(contracts, `"mesh-contracts"`, `"`+name+`"`, 1)
}
// Order 1: the domain's module merges first (its own repository) and takes wants over.
merged("novox/mesh-placement", 90, placement, forgemsg.Forge{Root: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
Trees: map[string]string{"": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"}, Manifests: map[string]string{"": domainModule("mesh-placement-contracts")}})
entry("wants.trunk", func(e index.SeatDefinitionEntry) bool {
return e.Module == "mesh-placement-contracts" && e.Commit == placement
})
// mesh-contracts then drops both: wants stays the domain module's; vault, whose module has not merged yet, is kept.
merged("novox/mesh-contracts", 91, dropped, forgemsg.Forge{Root: "dddddddddddddddddddddddddddddddddddddddd",
Trees: map[string]string{"": "dddddddddddddddddddddddddddddddddddddddd"}, Manifests: map[string]string{"": contracts}})
entry("git.trunk", func(e index.SeatDefinitionEntry) bool { return e.Commit == contractsMerge })
for _, k := range []string{"wants.trunk", "vault.trunk"} {
if _, err := seatDefs.Get(ctx, k); err != nil {
t.Fatalf("%s is gone after mesh-contracts dropped it: %v", k, err)
}
}
// Order 2 completed: the identity module merges afterwards and takes vault over.
merged("novox/mesh-identity", 92, identity, forgemsg.Forge{Root: "ffffffffffffffffffffffffffffffffffffffff",
Trees: map[string]string{"": "ffffffffffffffffffffffffffffffffffffffff"}, Manifests: map[string]string{"": domainModule("mesh-identity-and-access-contracts")}})
entry("vault.trunk", func(e index.SeatDefinitionEntry) bool {
return e.Module == "mesh-identity-and-access-contracts" && e.Commit == identity
})
// The seat list the verb answers: every trunk seat with its digest, the handed-over ones included, never a pull request's.
list, err := index.SeatDefinitionsOf(jsSource{ctx, seatDefs}, nil, "")
raw, _ := json.Marshal(list)
for _, want := range []string{`"seat":"git"`, `"seat":"vault"`, `"seat":"wants"`} {
if err != nil || !strings.Contains(string(raw), want) || strings.Contains(string(raw), `"source":"pull"`) {
t.Errorf("seat-definitions = %s, %v; want %s", raw, err, want)
}
}
// Said: git, wants and vault changed once each beyond the first count; no removal for the handed-over seats.
if m, err := info.GetLastMsgForSubject(ctx, "mesh.seat.mesh-catalog.event.seat-definition-removed"); err == nil {
t.Errorf("a handover said seat-definition-removed: %s", m.Data)
}
}
// seatFiles are mesh-contracts' files by commit, as gitea_get_file answers them.
@@ -354,6 +424,14 @@ func serveForge(t *testing.T, nc *nats.Conn, listing map[string]map[string]strin
if a.Repo == "mesh-sdk" && d == "go" && listing[a.Sha] != nil {
files[d] = listing[a.Sha]
}
if fs, ok := seatFiles[a.Sha]; ok && strings.HasPrefix(d, "seats") { // a contracts module's seats/ directory (D-I5)
files[d] = map[string]string{}
for p := range fs {
if rel, ok := strings.CutPrefix(p, d+"/"); ok {
files[d][rel] = "blob"
}
}
}
}
out["files"] = files
}
@@ -365,7 +443,7 @@ func serveForge(t *testing.T, nc *nats.Conn, listing map[string]map[string]strin
file, err := nc.Subscribe("mesh.mod.gitea.tool.gitea_get_file", func(m *nats.Msg) {
var a struct{ Repo, Path, Ref string }
_ = json.Unmarshal(m.Data, &a)
if a.Repo == "mesh-contracts" { // the seat definitions (DLV-I17): a path the commit does not hold is a 404
if _, held := seatFiles[a.Ref]; held { // the seat definitions (DLV-I17): a path the commit does not hold is a 404
if v, ok := seatFiles[a.Ref][a.Path]; ok {
reply(m, map[string]any{"file": map[string]any{"content": v}})
} else {
@@ -16,7 +16,7 @@ import (
)
// The seat definitions' wiring (DLV-I17-I20; design 26): the forge's get-file tool reads them, seat-definition-changed
// is said on the indexer's stream indexer-seat-definitions (kept seven days), and the checker's verdict event, heard on
// is said on the seat mesh-catalog's own subject (kept on the shared events stream, seven days), and the checker's verdict event, heard on
// the indexer's durable consumer of the checker's stream asks, carries a pull request's out-of-step files.
// checkerAsksStream is the checker's stream asks as the bus names it (its manifest's streams; StreamBusName).
@@ -28,16 +28,27 @@ func readFileVia(call toolCaller) func(ctx context.Context, repository, sha, pat
return goFileVia(call, "")
}
// saySeatDefinitionChanged publishes seat-definition-changed on the indexer's own stream, its duplicate id the seat,
// source and digest, so a message delivered again says it once.
func saySeatDefinitionChanged(js jetstream.JetStream, cred process.Account) func(context.Context, index.SeatDefinitionChanged) error {
// seatEventSubject is where the seat mesh-catalog, which the indexer holds, says an event: mesh.seat.<seat>.event.<name>
// (ADR 0636 §4). The events are the seat's, not the indexer's own, so they are not among the manifest's `emits`.
func seatEventSubject(event string) string {
return "mesh.seat." + index.CatalogSeat + ".event." + event
}
// saySeatDefinition publishes a trunk's definition moving as seat-definition-changed ({seat, source, digest}), or going as
// seat-definition-removed ({seat}), on the seat's subject; its duplicate id is the seat, event and digest, so a message
// delivered again says it once. Only a trunk's is said (the index package decides); a reader takes no pull request's.
func saySeatDefinition(js jetstream.JetStream, cred process.Account) func(context.Context, index.SeatDefinitionChanged) error {
return func(ctx context.Context, c index.SeatDefinitionChanged) error {
data, err := json.Marshal(c)
event, body := index.SeatDefinitionChangedEvent, any(map[string]string{"seat": c.Seat, "source": c.Source, "digest": c.Digest})
if c.Digest == "" {
event, body = index.SeatDefinitionRemovedEvent, map[string]string{"seat": c.Seat}
}
data, err := json.Marshal(body)
if err != nil {
return err
}
id := index.SeatDefinitionKey(c.Seat, c.Source, c.Repository, c.Pull) + "." + c.Digest
msg := nats.NewMsg(cred.StreamSubject(index.SeatDefinitionChangedEvent))
id := c.Seat + "." + event + "." + c.Digest
msg := nats.NewMsg(seatEventSubject(event))
msg.Header.Set("x-event-id", id)
msg.Header.Set("x-source", cred.Module)
msg.Header.Set("x-node", cred.Node)
@@ -83,8 +83,8 @@ type Indexer struct {
// ReadFile reads one file of a repository at a commit (gitea.gitea_get_file); a file the commit does not hold is
// nil with no error. Nil reads no seat definition.
ReadFile func(ctx context.Context, repository, sha, path string) ([]byte, error)
// SaySeatDefinitionChanged says seat-definition-changed (seatdefs.go); nil says nothing.
SaySeatDefinitionChanged func(ctx context.Context, c SeatDefinitionChanged) error
// SaySeatDefinition says seat-definition-changed (seatdefs.go); nil says nothing.
SaySeatDefinition func(ctx context.Context, c SeatDefinitionChanged) error
// Graph is the module graph's state (graph.Bucket, graph.go; novox/hq ADR 0603 §2); nil writes no graph.
Graph Store
// Files asks the forge for every blob under each directory at a commit, by directory then path relative to it, each
@@ -10,6 +10,7 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/views"
"git.novox.be/novox/mesh-delivery/sdk/graph"
)
@@ -28,6 +29,9 @@ type CatalogRead struct {
Manifest func(digest string) (string, error)
// Graph is the module graph, by module.
Graph func() (graph.Graph, error)
// SeatDefinitions and Marks are the indexer's state of the seat definitions and of the revisions its views said it
// up to, which the verb seat-definitions answers from (seatdefs.go); nil answers the verb nothing.
SeatDefinitions, Marks views.Source
}
func entryOf(name string, m delivery.Module) catalogue.CatalogEntry {
@@ -153,10 +157,7 @@ func (c CatalogRead) graphAndModules() (graph.Graph, map[string]delivery.Module,
// seat's contract gives them (novox/hq ADR 0604 §1, §2), answered from the indexer's own states. They are served once the
// indexer's manifest claims the seat, in the release after L3's (the lead's decision): until then main serves them not.
func CatalogTools(c CatalogRead) []stdio.Tool {
seat, ok := catalogue.SeatNamed(catalogue.CatalogSeat)
if !ok {
return nil
}
seat, _ := catalogue.SeatNamed(CatalogSeat)
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return s }
runs := map[string]func(map[string]any) (any, error){
"modules": func(map[string]any) (any, error) { return c.CatalogModulesAnswer() },
@@ -167,8 +168,24 @@ func CatalogTools(c CatalogRead) []stdio.Tool {
var out []stdio.Tool
for _, v := range seat.Serves {
if run, ok := runs[v.Name]; ok {
out = append(out, stdio.Tool{Name: catalogue.CatalogSeat + "." + v.Name, Description: v.Description, Input: v.Input, Run: run})
out = append(out, stdio.Tool{Name: CatalogSeat + "." + v.Name, Description: v.Description, Input: v.Input, Run: run})
}
}
if c.SeatDefinitions != nil {
out = append(out, stdio.Tool{Name: CatalogSeat + "." + SeatDefinitionsVerb, Description: SeatDefinitionsDescription, Input: SeatDefinitionsInput,
Run: func(a map[string]any) (any, error) {
return SeatDefinitionsOf(c.SeatDefinitions, c.Marks, str(a, "seat"))
}})
}
return out
}
// SeatDefinitionsDescription and SeatDefinitionsInput are the verb seat-definitions of the seat mesh-catalog (ADR 0636 §4).
const SeatDefinitionsDescription = "The seat list: the definitions of the seats the trunks hold. With no seat, every trunk seat with the digest of " +
"its definition and the revision of the state it is read from; with a seat, that seat's trunk definition, resolved. Only the " +
"merged definitions, never a pull request's (indexer_seat_definition answers those). Every reader of the seat list reads it " +
"here, and follows seat-definition-changed and seat-definition-removed."
// SeatDefinitionsInput is the verb's input.
var SeatDefinitionsInput = map[string]any{"type": "object", "properties": map[string]any{
"seat": map[string]any{"type": "string", "description": "one seat's name; absent for the list of every trunk seat"}}}
@@ -76,6 +76,11 @@ func catalogVerbsAnswerFromTheStates(t *testing.T) {
}
return g, nil
},
// The verb seat-definitions answers from the seat definitions state: the trunk's git, a pull request's git beside it.
SeatDefinitions: memSource{
"git.trunk": {Value: []byte(`{"seat":"git","source":"trunk","digest":"d1","definition":{"seat":"git"}}`)},
SeatDefinitionKey("git", SourcePull, "r", 1): {Value: []byte(`{"seat":"git","source":"pull","digest":"d2"}`)},
},
}
answer := func(v any, err error) []byte {
t.Helper()
@@ -97,13 +102,21 @@ func catalogVerbsAnswerFromTheStates(t *testing.T) {
var names []string
for _, tool := range CatalogTools(c) {
names = append(names, tool.Name)
if tool.Name == "mesh-catalog.seat-definitions" {
if got, err := tool.Run(nil); err != nil || string(answer(got, nil)) != `{"revision":0,"seats":[{"seat":"git","source":"trunk","digest":"d1"}]}` {
t.Errorf("mesh-catalog.seat-definitions answered %s, %v", answer(got, nil), err)
}
if got, err := tool.Run(map[string]any{"seat": "git"}); err != nil || !strings.Contains(string(answer(got, nil)), `"found":true`) {
t.Errorf("mesh-catalog.seat-definitions of git answered %s, %v", answer(got, nil), err)
}
}
if tool.Name == "mesh-catalog.modules" {
if got, err := tool.Run(nil); err != nil || !strings.Contains(string(answer(got, nil)), `"module":"avahi"`) {
t.Errorf("mesh-catalog.modules answered %v, %v", got, err)
}
}
}
if !slices.Equal(names, []string{"mesh-catalog.modules", "mesh-catalog.module", "mesh-catalog.provides", "mesh-catalog.dependents"}) {
if !slices.Equal(names, []string{"mesh-catalog.modules", "mesh-catalog.module", "mesh-catalog.provides", "mesh-catalog.dependents", "mesh-catalog.seat-definitions"}) {
t.Errorf("the tools bundle serves %v", names)
}
raw := answer(c.CatalogModulesAnswer())
@@ -13,6 +13,7 @@ import (
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"git.novox.be/novox/mesh-sdk/go/views"
)
@@ -122,23 +123,36 @@ func TestTheControlRecords(t *testing.T) {
t.Run("seat definitions", func(t *testing.T) {
const repo, m1, m2, m3, head = "novox/mesh-contracts", "m1", "m2", "m3", "h1"
manifest := `{"module":"mesh-contracts","version":"1","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"],"series":"0.1","generated-from":"."}]}}`
git := `{"format":1,"seat":"git","scope":"mesh","grain":"seat-list","decision":"novox/hq ADR 0111","delivers":"git"}`
git := `{"format":1,"seat":"git","scope":"mesh","grain":"seat-list","decision":"novox/hq ADR 0111","domain":"Core","delivers":"git"}`
files := map[string]map[string]string{
m1: {"seats/index.json": `{"format":1,"seats":["git","gone"]}`, "seats/git/seat.json": git, "seats/gone/seat.json": strings.Replace(git, `"git"`, `"gone"`, 1)},
m2: {"seats/index.json": `{"format":1,"seats":["git"]}`, "seats/git/seat.json": `{"format":1,"seat":"git","scope":"mesh","grain":"seat-list","decision":"x","oops":1}`},
m3: {"seats/index.json": `{"format":1,"seats":["git"]}`, "seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0112", 1)},
head: {"seats/index.json": `{"format":1,"seats":["git"]}`, "seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0200", 1)},
m1: {"seats/git/seat.json": git, "seats/gone/seat.json": strings.Replace(git, `"git"`, `"gone"`, 1)},
m2: {"seats/git/seat.json": `{"format":1,"seat":"git","scope":"mesh","grain":"seat-list","decision":"x","domain":"Core","oops":1}`},
m3: {"seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0112", 1)},
head: {"seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0200", 1)},
}
defs, findings := newMem(), newMem()
var said []SeatDefinitionChanged
ix := &Indexer{SeatDefinitions: defs, Findings: findings, Logf: t.Logf,
// The forge's listing of a contracts module's `seats/` directory: every blob under it at the commit (ADR 0635 §6).
Files: func(_ context.Context, _, sha string, dirs []string) (map[string]map[string]string, error) {
out := map[string]map[string]string{}
for _, d := range dirs {
out[d] = map[string]string{}
for p := range files[sha] {
if rel, ok := strings.CutPrefix(p, d+"/"); ok {
out[d][rel] = "blob"
}
}
}
return out, nil
},
ReadFile: func(_ context.Context, r, sha, p string) ([]byte, error) {
if v, ok := files[sha][p]; ok && r == repo {
return []byte(v), nil
}
return nil, nil
},
SaySeatDefinitionChanged: func(_ context.Context, c SeatDefinitionChanged) error { said = append(said, c); return nil }}
SaySeatDefinition: func(_ context.Context, c SeatDefinitionChanged) error { said = append(said, c); return nil }}
ctx := context.Background()
merged := func(merge string) {
t.Helper()
@@ -186,8 +200,8 @@ func TestTheControlRecords(t *testing.T) {
// Ruling (lead, on A2i's finding 4): another repository's trunk defining git is refused, the trunk's entry stands,
// nothing is said, and its finding names both repositories; its next merge without git clears the finding.
const other = "novox/other-contracts"
files["o1"] = map[string]string{"seats/index.json": `{"format":1,"seats":["git"]}`, "seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0300", 1)}
files["o2"] = map[string]string{"seats/index.json": `{"format":1,"seats":[]}`}
files["o1"] = map[string]string{"seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0300", 1)}
files["o2"] = map[string]string{}
read := ix.ReadFile
ix.ReadFile = func(ctx context.Context, r, sha, p string) ([]byte, error) {
if r == other {
@@ -271,7 +285,7 @@ func TestTheControlRecords(t *testing.T) {
ix.Manifests.(*memStore).vals[mkey] = kept
mod, _ := delivery.EncodeModule(delivery.Module{FP2: "src2:" + strings.Repeat("a", 64), Manifest: delivery.ManifestDigest([]byte(plain)), Repository: repo, Commit: m3, Path: delivery.RootPath})
ix.Modules.(*memStore).vals["mesh-contracts"] = mod
files["h2"] = map[string]string{"module.json": manifest, "seats/index.json": `{"format":1,"seats":["git"]}`, "seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0400", 1)}
files["h2"] = map[string]string{"module.json": manifest, "seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0400", 1)}
if r := ix.SeatDefinitionsAgain(ctx, "mesh-contracts", 9, "h2"); !r.Stored {
t.Errorf("again for the pull request's head = %+v, want it read with the head's generated-from", r)
}
@@ -291,8 +305,107 @@ func TestTheControlRecords(t *testing.T) {
if _, ok := defs.vals[OutOfStepKey(repo, 9)]; ok {
t.Error("a closed pull request's out-of-step files are kept")
}
if last := said[len(said)-1]; last != (SeatDefinitionChanged{Seat: "git", Source: SourcePull, Repository: repo, Pull: 9}) {
t.Errorf("last said %+v, want the pull request's removal", last)
for _, c := range said {
if c.Source != SourceTrunk {
t.Errorf("said %+v: only a trunk's definition is said (ADR 0636 §4)", c)
}
}
// ADR 0636 §5, D-I3, D-I5: a seat of the mesh's own contracts is taken only from its domain's contracts module; a
// move there is a handover in either order of the two trunks; a second definer is refused with a finding naming both.
// The seats `wants` (Placement) and `vault` (Identity and access) are held by mesh-contracts as an older indexer
// kept them; their definitions are listed from the module's `seats/` directory with no index.
seat := func(name, domain string) string {
return `{"format":1,"seat":"` + name + `","scope":"mesh","grain":"seat-list","decision":"d","domain":"` + domain + `"}`
}
for _, name := range []string{"wants", "vault"} {
dom := map[string]string{"wants": "Placement", "vault": "Identity and access"}[name]
var d seatdef.Definition
if err := json.Unmarshal([]byte(seat(name, dom)), &d); err != nil {
t.Fatal(err)
}
raw, digest, _ := digestOf(d)
data, _ := json.Marshal(SeatDefinitionEntry{Seat: name, Source: SourceTrunk, Repository: repo, Module: "mesh-contracts", Directory: ".",
Commit: "old", Digest: digest, Definition: raw})
defs.vals[name+".trunk"] = data
}
files["c1"] = map[string]string{"seats/wants/seat.json": seat("wants", "Placement"), "seats/vault/seat.json": seat("vault", "Identity and access"),
"seats/lone/seat.json": seat("lone", "Core")}
files["c2"] = map[string]string{"seats/lone/seat.json": seat("lone", "Core")}
files["p1"] = map[string]string{"seats/wants/seat.json": seat("wants", "Placement")}
files["i1"] = map[string]string{"seats/vault/seat.json": seat("vault", "Identity and access")}
domainManifest := func(module string) map[string]string {
return map[string]string{delivery.RootPath: strings.Replace(manifest, `"mesh-contracts"`, `"`+module+`"`, 1)}
}
mergeAt := func(repository, merge string, manifests map[string]string) {
t.Helper()
ix.ReadFile = func(_ context.Context, _, sha, p string) ([]byte, error) { return []byte(files[sha][p]), nil }
if err := ix.ApplySeatDefinitions(ctx, forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: repository, Number: 3, Merge: merge, Manifests: manifests}); err != nil {
t.Fatal(err)
}
}
outside := health.FindingKey(health.ScopeModule, findingID("seat-outside-its-domain", "mesh-contracts"), SeatOutsideItsDomainKind)
before = len(said)
// mesh-contracts defines wants and vault, seats of other domains: refused with a finding naming both modules, the
// held entries stand, and lone (Core) is taken.
mergeAt(repo, "c1", map[string]string{delivery.RootPath: manifest})
if v, ok := findings.vals[outside]; !ok || !strings.Contains(string(v), "mesh-placement-contracts") || !strings.Contains(string(v), "mesh-identity-and-access-contracts") ||
!strings.Contains(string(v), "mesh-contracts defines it") {
t.Errorf("finding %s = %s, want it naming both modules", outside, v)
}
if w, _ := entry("wants.trunk"); w.Commit != "old" {
t.Errorf("a refused definition replaced the held one: %+v", w)
}
if l, ok := entry("lone.trunk"); !ok || l.Commit != "c1" {
t.Errorf("a Core seat from mesh-contracts is not taken: %+v (%v)", l, ok)
}
// Order 1: the domain's module merges first (another repository) and takes wants over; mesh-contracts then drops it and
// the entry stays the domain module's, nothing removed.
mergeAt("novox/mesh-placement", "p1", domainManifest("mesh-placement-contracts"))
if w, _ := entry("wants.trunk"); w.Module != "mesh-placement-contracts" || w.Commit != "p1" {
t.Errorf("the domain's module did not take wants over: %+v", w)
}
mergeAt(repo, "c2", map[string]string{delivery.RootPath: manifest})
if w, _ := entry("wants.trunk"); w.Module != "mesh-placement-contracts" {
t.Errorf("mesh-contracts dropping wants removed or replaced the domain module's entry: %+v", w)
}
if _, ok := findings.vals[outside]; ok {
t.Error("the outside-its-domain finding stands once mesh-contracts defines only its own domain's seats")
}
// Order 2: mesh-contracts drops vault first, and the last trunk definition is kept (no removal said) until the
// domain's module defines it.
if v, _ := entry("vault.trunk"); v.Module != "mesh-contracts" {
t.Fatalf("vault was not kept by mesh-contracts after it dropped it: %+v", v)
}
mergeAt("novox/mesh-identity", "i1", domainManifest("mesh-identity-and-access-contracts"))
if v, _ := entry("vault.trunk"); v.Module != "mesh-identity-and-access-contracts" || v.Commit != "i1" {
t.Errorf("the domain's module did not take vault over: %+v", v)
}
for _, c := range said[before:] {
if c.Digest == "" && (c.Seat == "wants" || c.Seat == "vault") {
t.Errorf("said %+v: a handover removes nothing", c)
}
}
// A pull request of mesh-contracts defining a Placement seat fails its check as a definition that does not parse.
files["h2"] = map[string]string{"seats/wants/seat.json": seat("wants", "Placement")}
ix.ReadFile = func(_ context.Context, _, sha, p string) ([]byte, error) { return []byte(files[sha][p]), nil }
if err := ix.ApplySeatDefinitions(ctx, forgemsg.Forge{Kind: forgemsg.ForgePull, Action: forgemsg.PullUpdated, Repository: repo, Number: 11, Head: "h2",
Dirs: []string{delivery.RootPath}, Manifests: map[string]string{delivery.RootPath: manifest}}); err != nil {
t.Fatal(err)
}
if e, _ := entry(SeatDefinitionKey("wants", SourcePull, repo, 11)); !strings.Contains(e.Unparsed, "mesh-placement-contracts") || e.Definition != nil {
t.Errorf("a pull request's wants from mesh-contracts = %+v, want it refused naming the domain's module", e)
}
// The verb seat-definitions: no seat lists every trunk seat with its digest (never a pull request's); a seat answers it whole.
listed, err := SeatDefinitionsOf(src(), nil, "")
raw, _ := json.Marshal(listed)
if err != nil || !strings.Contains(string(raw), `"seat":"wants","source":"trunk","digest":"`) || strings.Contains(string(raw), "pull") || strings.Contains(string(raw), "_out-of-step") {
t.Errorf("the verb with no seat = %s, %v", raw, err)
}
one, err := SeatDefinitionsOf(src(), nil, "vault")
raw, _ = json.Marshal(one)
if err != nil || !strings.Contains(string(raw), `"found":true`) || !strings.Contains(string(raw), `"definition"`) {
t.Errorf("the verb with a seat = %s, %v", raw, err)
}
})
t.Run("packages per series", func(t *testing.T) {
@@ -0,0 +1,75 @@
package index
// Where a seat of the mesh's own contracts may be defined (novox/hq ADR 0636 §5, ADR 0634 §7): a definition names its
// glossary domain; the indexer accepts a seat of the mesh's own contracts only from that domain's contracts module
// (mesh-<domain>-contracts), or from mesh-contracts for a domain with none. A second definer is refused with a finding
// naming both. A definition moving from mesh-contracts to its domain's contracts module is a handover: the new
// definer takes the seat over whichever trunk merges first, and a removal from the module that is not the seat's
// domain's keeps the last trunk definition until the domain's module defines it.
import (
"encoding/json"
"fmt"
"slices"
"strings"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
// fallbackContracts is the mesh's contracts module for a domain with no contracts module of its own.
const fallbackContracts = "mesh-contracts"
// domainsWithContracts are the domains whose seats have a contracts module of their own (ADR 0634 §7): the seven
// the milestone creates. Every other domain's seats are defined in fallbackContracts.
var domainsWithContracts = []string{"Delivery", "Placement", "Provisioning", "Identity and access", "Health and repair",
"Connectivity", "Operator and conversation"}
// domainContracts is the module that defines a domain's seats: mesh-<domain>-contracts, the heading lowercased with
// its spaces as dashes, or fallbackContracts for a domain with none.
func domainContracts(domain string) string {
if !slices.Contains(domainsWithContracts, domain) {
return fallbackContracts
}
return "mesh-" + strings.ReplaceAll(strings.ToLower(domain), " ", "-") + "-contracts"
}
// ownContracts says a contracts module is one of the mesh's own: fallbackContracts or a domain's.
func ownContracts(module string) bool {
return module == fallbackContracts || slices.ContainsFunc(seatdef.Domains, func(d string) bool {
return module == domainContracts(d) && module != fallbackContracts
})
}
// domainRefusal is, for a definition a contracts module of the mesh's own holds, why it may not: the words naming the
// module and the domain's. Empty when the module may define the seat (a module outside the mesh's own contracts defines
// what it likes, ADR 0625 §7).
func domainRefusal(module string, d seatdef.Definition) string {
if !ownContracts(module) {
return ""
}
if want := domainContracts(d.Domain); want != module {
return fmt.Sprintf("it is a seat of the domain %q, whose seats %s defines, and %s defines it", d.Domain, want, module)
}
return ""
}
// domainOf is the domain a kept definition names ("" when it is none or does not decode).
func domainOf(raw json.RawMessage) string {
var d struct {
Domain string `json:"domain"`
}
_ = json.Unmarshal(raw, &d)
return d.Domain
}
// handsOver says a kept trunk entry is handed over to a definer: the entry is held by a module of the mesh's own
// that is not the seat's domain's, and the definer is the seat's domain's contracts module.
func handsOver(cur SeatDefinitionEntry, module string, d seatdef.Definition) bool {
return cur.Module != module && ownContracts(cur.Module) && domainContracts(d.Domain) == module
}
// handoverPending says a trunk entry its holder no longer defines waits for its domain's module: the holder is a
// module of the mesh's own and not the seat's domain's, so the last trunk definition stays until that module defines it.
func handoverPending(cur SeatDefinitionEntry) bool {
return ownContracts(cur.Module) && domainContracts(domainOf(cur.Definition)) != cur.Module
}
@@ -692,7 +692,7 @@ func (ix *Indexer) dry() *dryRun {
// The registry milestone's fields: what a take-in would write on the bus stays out of a dry run, so the states of the
// requirements, findings, seat definitions, holds and their bounds are not overlaid, nothing is said, and no hold is
// weighed (Holds and Counted nil hold nothing); ReadFile only reads.
Requirements: nil, Findings: nil, SeatDefinitions: nil, ReadFile: ix.ReadFile, SaySeatDefinitionChanged: nil,
Requirements: nil, Findings: nil, SeatDefinitions: nil, ReadFile: ix.ReadFile, SaySeatDefinition: nil,
Holds: nil, Counted: nil, AskEngines: nil, HoldBounds: nil}
x.Logf = func(format string, args ...any) { ix.logf("take-in, computed and not written: "+format, args...) }
x.Store, d.candidates = over(ix.Store)
@@ -521,7 +521,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
for _, f := range []string{"Store", "Modules", "Manifests", "Predefined", "ConfigCandidates", "Taken", "Verdicts", "OldSections",
"JudgeID", "SayRefused", "SayAccepted", "Packages", "Graph", "Files", "GoSum", "GoMod", "Now", "Builds", "Named", "Trunk",
"Read", "Contains", "Logf", "ReadOpen", "OwnBuild", "Message", "Pending", "SayTakeIn",
"Requirements", "Findings", "SeatDefinitions", "ReadFile", "SaySeatDefinitionChanged", "Holds", "Counted", "AskEngines", "HoldBounds"} {
"Requirements", "Findings", "SeatDefinitions", "ReadFile", "SaySeatDefinition", "Holds", "Counted", "AskEngines", "HoldBounds"} {
named[f] = true
}
ty := reflect.TypeOf(Indexer{})
@@ -7,7 +7,8 @@ package index
// and source, as its only writer. Only the trunk's entry is the seat's contract; a pull request's entries go when it
// closes or merges. A trunk definition that does not parse keeps the trunk's entry and writes the finding
// seat-definition-unparsed; a trunk defining a seat another repository's trunk already defines is refused with the
// finding seat-defined-twice naming both (a seat has one definition); generated types out of step on a trunk, said by
// finding seat-defined-twice naming both (a seat has one definition); a seat of the mesh's own contracts is taken only
// from its domain's contracts module, and a move there is a handover (ADR 0636 §5, domain.go); generated types out of step on a trunk, said by
// the builder's build end, write the finding generated-types-out-of-step, cleared only by a clean build end of the same
// contracts module.
@@ -36,15 +37,20 @@ const (
// SeatDefinitionsState is the indexer's state of the seat definitions (design 26), one key per seat and source.
SeatDefinitionsState = "seat-definitions"
SeatDefinitionsBucket = delivery.IndexerModule + "_" + SeatDefinitionsState
// SeatDefinitionChangedEvent is said, kept seven days on the indexer's stream SeatDefinitionsStream, only when a
// definition's digest changed (ADR 0608 §3).
// The seat mesh-catalog's verb and events (ADR 0636 §4): the indexer, which holds the seat, serves the verb and says
// the events on the seat's own subject, for the trunks' definitions only, changed when a digest moved and removed when
// a trunk's definition went.
CatalogSeat = "mesh-catalog"
SeatDefinitionsVerb = "seat-definitions"
SeatDefinitionChangedEvent = "seat-definition-changed"
SeatDefinitionsStream = "indexer-seat-definitions"
SeatDefinitionRemovedEvent = "seat-definition-removed"
// The findings (ADR 0608 §4, ADR 0564: the indexer writes findings, the condition keeper raises them).
SeatDefinitionUnparsedKind = "seat-definition-unparsed"
OutOfStepKind = "generated-types-out-of-step"
// SeatDefinedTwiceKind: a contracts module's trunk defines a seat another repository's trunk already defines.
SeatDefinedTwiceKind = "seat-defined-twice"
// SeatOutsideItsDomainKind: a contracts module of the mesh's own defines a seat of another domain (ADR 0636 §5).
SeatOutsideItsDomainKind = "seat-outside-its-domain"
// The three tools (design 26).
SeatDefinitionTool = "indexer_seat_definition"
SeatDefinitionsAgainTool = "indexer_seat_definitions_again"
@@ -75,8 +81,8 @@ type SeatDefinitionEntry struct {
At time.Time `json:"at"`
}
// SeatDefinitionChanged is the event's body: the seat, its source and the new digest ("" when the source holds the
// seat no more).
// SeatDefinitionChanged is what the indexer says of a definition: the seat, its source and the new digest ("" when the
// source holds the seat no more, which is the event seat-definition-removed). Only a trunk's is said.
type SeatDefinitionChanged struct {
Seat string `json:"seat"`
Source string `json:"source"`
@@ -143,9 +149,33 @@ func contractsModules(manifests map[string]string) []contractsModule {
return out
}
// readDefinitions reads a contracts module's definitions at a commit: its seats' index, each seat's definition and the
// parts it includes, each resolved. A seat that does not parse is answered by its words; a definitions directory
// with no index, or an index that does not parse, is an error naming it.
// listSeats names the seats a contracts module defines at a commit: every directory of its definitions' `seats/` that
// holds a `seat.json`, whatever the seat's grain (ADR 0635 §2, §6), from the forge's one listing of that directory. No
// index file names them. A listing the forge did not answer is a read it did not answer (the message is delivered again).
func (ix *Indexer) listSeats(ctx context.Context, repository, commit string, c contractsModule) ([]string, error) {
if ix.Files == nil {
return nil, fmt.Errorf("%w: the indexer has no listing of %s's seats", errForgeRead, c.module)
}
dir := path.Join(c.defs, "seats")
if c.defs == "." || c.defs == "" {
dir = "seats"
}
got, err := ix.Files(ctx, repository, commit, []string{dir})
if err != nil {
return nil, fmt.Errorf("%w: the seats of %s at %.12s: %w", errForgeRead, c.module, commit, err)
}
var seats []string
for p := range got[dir] {
if seat, ok := strings.CutSuffix(p, "/seat.json"); ok && seat != "" && !strings.Contains(seat, "/") {
seats = append(seats, seat)
}
}
slices.Sort(seats)
return seats, nil
}
// readDefinitions reads a contracts module's definitions at a commit: each seat's definition (listSeats) and the
// parts it includes, each resolved. A seat that does not parse is answered by its words.
func (ix *Indexer) readDefinitions(ctx context.Context, repository, commit string, c contractsModule) (map[string]seatdef.Definition, map[string]string, error) {
file := func(p string) ([]byte, error) {
if c.defs != "." && c.defs != "" {
@@ -160,17 +190,13 @@ func (ix *Indexer) readDefinitions(ctx context.Context, repository, commit strin
}
return raw, nil
}
raw, err := file("seats/index.json")
seats, err := ix.listSeats(ctx, repository, commit, c)
if err != nil {
return nil, nil, err
}
idx, err := seatdef.ParseIndex(raw)
if err != nil {
return nil, nil, fmt.Errorf("seats/index.json does not parse: %w", err)
}
defs, unparsed := map[string]seatdef.Definition{}, map[string]string{}
parts := map[string]seatdef.Part{}
for _, seat := range idx.Seats {
for _, seat := range seats {
raw, err := file("seats/" + seat + "/seat.json")
if err != nil {
if errors.Is(err, errForgeRead) {
@@ -372,8 +398,13 @@ func (ix *Indexer) keepTrunk(ctx context.Context, repository, commit string, c c
}
ix.unparsedFinding(ctx, c.module, unparsed)
var errs []error
twice := map[string]string{} // seat -> the repository and module whose trunk holds it
twice := map[string]string{} // seat -> the repository and module whose trunk holds it
outside := map[string]string{} // seat -> why its module may not define it (ADR 0636 §5)
for seat, d := range defs {
if why := domainRefusal(c.module, d); why != "" {
outside[seat] = why
continue
}
holder, err := ix.putDefinition(ctx, SeatDefinitionEntry{Seat: seat, Source: SourceTrunk, Repository: repository,
Module: c.module, Directory: c.dir, Commit: commit}, d)
if holder != "" {
@@ -382,7 +413,9 @@ func (ix *Indexer) keepTrunk(ctx context.Context, repository, commit string, c c
errs = append(errs, err)
}
ix.twiceFinding(ctx, repository, c.module, twice)
// A seat the module's trunk no longer defines leaves the state.
ix.outsideFinding(ctx, c.module, outside)
// A seat the module's trunk no longer defines leaves the state, unless it is handed over: a seat that is not its
// holder's domain's waits for the domain's module (domain.go).
keys, err := ix.SeatDefinitions.Keys(ctx)
if err != nil {
return errors.Join(append(errs, err)...)
@@ -395,7 +428,7 @@ func (ix *Indexer) keepTrunk(ctx context.Context, repository, commit string, c c
gone := false
err := write(ctx, ix.SeatDefinitions, key, func(value []byte, found bool) ([]byte, bool, error) {
var e SeatDefinitionEntry
gone = found && json.Unmarshal(value, &e) == nil && e.Repository == repository && e.Module == c.module
gone = found && json.Unmarshal(value, &e) == nil && e.Repository == repository && e.Module == c.module && !handoverPending(e)
return nil, gone, nil
})
errs = append(errs, err)
@@ -419,6 +452,10 @@ func (ix *Indexer) keepPull(ctx context.Context, repository string, pull int, he
base := SeatDefinitionEntry{Source: SourcePull, Repository: repository, Pull: pull, Module: c.module, Directory: c.dir, Commit: head}
var errs []error
for seat, d := range defs {
if why := domainRefusal(c.module, d); why != "" {
unparsed[seat] = why // its check fails on it, as on a definition that does not parse
continue
}
e := base
e.Seat = seat
_, err := ix.putDefinition(ctx, e, d)
@@ -442,8 +479,9 @@ func (ix *Indexer) keepPull(ctx context.Context, repository string, pull int, he
}
// putDefinition writes one parsed definition at its key, said when its digest moved. A trunk's definition of a seat
// another repository's (or module's) trunk already defines is refused, written nowhere: a seat has one definition. The
// refused one answers who holds it ("<repository> (<module>)").
// another repository's (or module's) trunk already defines is refused, written nowhere: a seat has one definition, unless
// the definer is the seat's domain's module taking it over from another module of the mesh's own (a handover, domain.go).
// The refused one answers who holds it ("<repository> (<module>)").
func (ix *Indexer) putDefinition(ctx context.Context, e SeatDefinitionEntry, d seatdef.Definition) (string, error) {
raw, digest, err := digestOf(d)
if err != nil {
@@ -455,11 +493,11 @@ func (ix *Indexer) putDefinition(ctx context.Context, e SeatDefinitionEntry, d s
var cur SeatDefinitionEntry
known := found && json.Unmarshal(value, &cur) == nil
moved, holder = false, ""
if known && e.Source == SourceTrunk && (cur.Repository != e.Repository || cur.Module != e.Module) {
if known && e.Source == SourceTrunk && (cur.Repository != e.Repository || cur.Module != e.Module) && !handsOver(cur, e.Module, d) {
holder = cur.Repository + " (" + cur.Module + ")"
return nil, false, nil
}
if known && cur.Digest == digest && cur.Unparsed == "" {
if known && cur.Digest == digest && cur.Unparsed == "" && cur.Repository == e.Repository && cur.Module == e.Module {
return nil, false, nil
}
moved = true
@@ -488,6 +526,22 @@ func (ix *Indexer) twiceFinding(ctx context.Context, repository, module string,
"No seat on "+module+"'s trunk is defined by another repository.")
}
// outsideFinding writes the contracts module's seat-outside-its-domain finding for the seats it defines that belong to
// another domain's module, naming both, or clears it when there are none.
func (ix *Indexer) outsideFinding(ctx context.Context, module string, outside map[string]string) {
var lines []string
for seat, why := range outside {
lines = append(lines, seat+": "+why)
}
slices.Sort(lines)
ix.finding(ctx, findingID("seat-outside-its-domain", module), SeatOutsideItsDomainKind, len(lines) > 0,
fmt.Sprintf("%s's trunk defines seats of another domain, refused: %s", module, strings.Join(lines, "; ")),
"A seat on "+module+"'s trunk belongs to another domain's contracts module",
"A seat of the mesh's own contracts is taken only from its domain's contracts module (mesh-contracts for a domain with none). "+
"Move the definition there; the next merge of "+module+" without it clears this.",
"Every seat on "+module+"'s trunk is of its domain.")
}
// dropPull removes a pull request's entries and its out-of-step files.
func (ix *Indexer) dropPull(ctx context.Context, repository string, pull int) error {
keys, err := ix.SeatDefinitions.Keys(ctx)
@@ -509,10 +563,10 @@ func (ix *Indexer) dropPull(ctx context.Context, repository string, pull int) er
}
func (ix *Indexer) sayChanged(ctx context.Context, c SeatDefinitionChanged) {
if ix.SaySeatDefinitionChanged == nil {
if ix.SaySeatDefinition == nil || c.Source != SourceTrunk {
return
}
if err := ix.SaySeatDefinitionChanged(ctx, c); err != nil {
if err := ix.SaySeatDefinition(ctx, c); err != nil {
ix.logf("seat definitions: %s of %s (%s) was not said: %v", SeatDefinitionChangedEvent, c.Seat, c.Source, err)
}
}
@@ -731,3 +785,60 @@ func OutOfStepOf(defs, findings views.Source) ([]OutOfStepRow, error) {
}
return out, nil
}
// SeatDefinitionList is the verb seat-definitions with no seat (ADR 0636 §4): every trunk seat with the digest of its
// definition, and the revision the views mark of the state names.
type SeatDefinitionList struct {
Revision uint64 `json:"revision"`
Seats []SeatDefinition `json:"seats"`
}
// SeatDefinition is one seat of the list: the seat, its source (always the trunk) and its digest.
type SeatDefinition struct {
Seat string `json:"seat"`
Source string `json:"source"`
Digest string `json:"digest"`
}
// SeatDefinitionsOf answers the verb seat-definitions of the seat mesh-catalog (ADR 0636 §4) from the state: with no
// seat, every trunk seat with its digest, by seat; with a seat, that seat's trunk definition (found false when the trunk
// holds none). Never a pull request's: a running part acts on what is merged.
func SeatDefinitionsOf(defs, marks views.Source, seat string) (any, error) {
if seat != "" {
a, err := SeatDefinitionOf(defs, marks, seat, "", 0)
if err != nil {
return nil, err
}
return struct {
Seat string `json:"seat"`
Found bool `json:"found"`
Entry *SeatDefinitionEntry `json:"entry,omitempty"`
Revision uint64 `json:"revision"`
}{a.Seat, a.Found, a.Entry, a.Revision}, nil
}
keys, err := defs.Keys()
if err != nil {
return nil, fmt.Errorf("the %s state cannot be listed: %w", SeatDefinitionsState, err)
}
slices.Sort(keys)
out := SeatDefinitionList{Revision: markOf(marks), Seats: []SeatDefinition{}}
for _, key := range keys {
name, ok := strings.CutSuffix(key, "."+SourceTrunk)
if !ok || strings.HasPrefix(key, outOfStepPrefix) {
continue
}
v, ok, err := defs.Get(key)
if err != nil {
return nil, fmt.Errorf("%s.%s cannot be read: %w", SeatDefinitionsState, key, err)
}
if !ok || v == nil {
continue
}
var e SeatDefinitionEntry
if err := json.Unmarshal(v.Value, &e); err != nil || e.Digest == "" {
continue // an entry that holds no definition is no seat of the list
}
out.Seats = append(out.Seats, SeatDefinition{Seat: name, Source: SourceTrunk, Digest: e.Digest})
}
return out, nil
}
+13 -8
View File
@@ -214,16 +214,21 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
}
}
// ADR 0615 §5, ADR 0619 §2: its views family on its old streams section, 7 days and 256 MiB, dropping its oldest.
// Design 26: seat-definition-changed is kept seven days (DLV-I17).
if want := []stream{{Name: index.ViewsStream, Events: []string{views.EventPattern}, MaxAgeDays: views.StreamDays, MaxMiB: views.StreamMiB},
{Name: index.SeatDefinitionsStream, Events: []string{index.SeatDefinitionChangedEvent}, MaxAgeDays: 7}}; !reflect.DeepEqual(m.Streams, want) {
// ADR 0636 §4: seat-definition-changed and -removed are the seat mesh-catalog's events, said on the seat's subject and kept
// on the shared events stream: no stream of the indexer's, and not among its own emits.
if want := []stream{{Name: index.ViewsStream, Events: []string{views.EventPattern}, MaxAgeDays: views.StreamDays, MaxMiB: views.StreamMiB}}; !reflect.DeepEqual(m.Streams, want) {
t.Errorf("streams = %+v, want %+v", m.Streams, want)
}
// ADR 0604 §2: it holds the seat mesh-catalog, serving its four verbs from its tools bundle (which stays without a
// request subject).
if len(m.Claims) != 1 || m.Claims[0].Name != catalogue.CatalogSeat || m.Claims[0].Scope != catalogue.ScopeMesh ||
!slices.Equal(m.Claims[0].Serves, []string{"modules", "module", "provides", "dependents"}) {
t.Errorf("claims = %+v, want mesh-catalog with its four verbs", m.Claims)
// ADR 0604 §2, ADR 0636 §4: it holds the seat mesh-catalog, serving its four verbs and seat-definitions from its tools
// bundle (which stays without a request subject).
if len(m.Claims) != 1 || m.Claims[0].Name != index.CatalogSeat || m.Claims[0].Scope != catalogue.ScopeMesh ||
!slices.Equal(m.Claims[0].Serves, []string{"modules", "module", "provides", "dependents", index.SeatDefinitionsVerb}) {
t.Errorf("claims = %+v, want mesh-catalog with its five verbs", m.Claims)
}
for _, e := range []string{index.SeatDefinitionChangedEvent, index.SeatDefinitionRemovedEvent} {
if slices.Contains(m.Emits, e) {
t.Errorf("emits lists %s, which the seat says (the validator refuses a seat's event among a holder's emits)", e)
}
}
// 1585/T11 (ADR 0633 §1, §5): the take-in events are its ordinary events, with no stream of its own.
if !slices.Contains(m.Emits, takein.TakeInPendingEvent) || !slices.Contains(m.Emits, takein.TakeInTakenEvent) {
+3 -3
View File
@@ -11,7 +11,8 @@
"modules",
"module",
"provides",
"dependents"
"dependents",
"seat-definitions"
]
}
],
@@ -88,8 +89,7 @@
{ "name": "pending", "history": 1 }
],
"streams": [
{ "name": "indexer-views", "events": ["views.**"], "max-age-days": 7, "max-mib": 256 },
{ "name": "indexer-seat-definitions", "events": ["seat-definition-changed"], "max-age-days": 7 }
{ "name": "indexer-views", "events": ["views.**"], "max-age-days": 7, "max-mib": 256 }
],
"emits": [
"manifest-refused",