Follow L2 under L3: the vault and mesh-config read the seat mesh-placement's placement, and the check that only listed modules read the controller's assignments moves into noplacement, as L2 deleted the placement check (issue 774, ADR 0567, ADR 0549 §3, §6)

This commit is contained in:
2026-10-28 21:54:36 +01:00
parent 24fad2b92a
commit 5be6576117
17 changed files with 139 additions and 31 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# The modules of this catalogue that still read the controller's assignments (mesh-controller.assignments), each with
# why. The check of this directory refuses any other module that reads them, and an entry here whose module no longer
# why. The check of this directory (noplacement) refuses any other module that reads them, and an entry here whose module no longer
# does: the list only shrinks, and it is empty before the controller's lease is written to a holder that is no
# controller (novox/hq ADR 0549 §3, §6; issue 774).
gitea the provisioner's plan of a module (go/provider) rides only in an assignment's value; its home after the lease is issue 774's provisioner-plan subtask
+4
View File
@@ -1,3 +1,7 @@
module git.novox.be/novox/mesh-catalog/checks/noplacement
go 1.22
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
+73 -2
View File
@@ -8,7 +8,13 @@
// 2. a `particulars` file at the root (the mesh's node names come from the checker's run, §16);
//
// 3. a manifest naming a state, stream, event or bucket of the indexer's placement (`mesh-delivery-indexer.placement…`
// or `mesh-delivery-indexer_placement…`) in any string value, such as `reads`.
// or `mesh-delivery-indexer_placement…`) in any string value, such as `reads`;
//
// 4. a manifest, other than mesh-controller's, that reads the controller's assignments (`mesh-controller.assignments`):
// every reader takes placement from the seat mesh-placement (novox/hq ADR 0549 §3, ADR 0567), and the lease write is
// refused while one still reads them (ADR 0549 §6; issue 774). The modules that still read them for what the
// placement does not carry are named in checks/noplacement/assignments-readers, each with why; that list only
// shrinks: an entry whose module no longer reads them is refused too.
//
// go run . <catalogue root>
//
@@ -55,9 +61,74 @@ func Findings(root string) ([]string, error) {
out = append(out, fmt.Sprintf("%s: names %q, which the indexer no longer keeps; placement is the seat mesh-placement's (novox/hq ADR 0567 §12)", module, s))
}
}
readers, err := assignmentsReaders(root)
if err != nil {
return nil, err
}
out = append(out, readers...)
sort.Strings(out)
return out, nil
}
// AssignmentsRead is the controller's assignments as a manifest reads them, and Allowed the file, relative to the
// catalogue's root, that names the modules still reading them: `<module> <why>` per line, `#` a comment.
const (
AssignmentsRead = "mesh-controller.assignments"
Allowed = "checks/noplacement/assignments-readers"
)
// assignmentsReaders is every finding on the reads of the controller's assignments: a module other than the controller
// reading them that Allowed does not name, and a module Allowed names that no longer reads them (issue 774).
func assignmentsReaders(root string) ([]string, error) {
allowed := map[string]bool{}
raw, err := os.ReadFile(filepath.Join(root, Allowed))
if err != nil && !os.IsNotExist(err) {
return nil, err
}
for _, line := range strings.Split(string(raw), "\n") {
if f := strings.Fields(line); len(f) > 0 && !strings.HasPrefix(f[0], "#") {
allowed[f[0]] = true
}
}
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
var found []string
reads := map[string]bool{}
for _, path := range manifests {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m struct {
Module string `json:"module"`
Reads []string `json:"reads"`
}
if json.Unmarshal(raw, &m) != nil || m.Module == "mesh-controller" {
continue // a manifest that does not read is the manifest checks' to refuse
}
for _, r := range m.Reads {
if strings.TrimSpace(r) != AssignmentsRead {
continue
}
reads[m.Module] = true
if !allowed[m.Module] {
found = append(found, fmt.Sprintf("%s reads %s: read where modules run from mesh-delivery-assigner.placement, "+
"the seat mesh-placement's (novox/hq ADR 0549 §3, ADR 0567); the lease write is refused while a module reads "+
"the assignments (ADR 0549 §6)", m.Module, AssignmentsRead))
}
}
}
for module := range allowed {
if !reads[module] {
found = append(found, fmt.Sprintf("%s names %s, which no longer reads %s: take it off the list, which only shrinks",
Allowed, module, AssignmentsRead))
}
}
return found, nil
}
// named collects the strings of a decoded manifest that start with one of the Prefixes.
func named(v any, into []string) []string {
switch v := v.(type) {
@@ -95,5 +166,5 @@ func main() {
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("no placement directory, and no manifest reads the indexer's placement")
fmt.Println("no placement directory, no manifest reads the indexer's placement, and only the modules listed read the controller's assignments")
}
+34 -1
View File
@@ -5,6 +5,8 @@ import (
"path/filepath"
"strings"
"testing"
"git.novox.be/novox/mesh-catalog/checks/trunk"
)
func TestFindingsRefuseWhatThePlacementPathLeft(t *testing.T) {
@@ -13,12 +15,18 @@ func TestFindingsRefuseWhatThePlacementPathLeft(t *testing.T) {
files map[string]string
want string // empty: clean
}{
{"clean", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-controller.assignments"]}`}, ""},
{"clean", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-delivery-assigner.placement"]}`}, ""},
{"a placement directory", map[string]string{"placement/ace.json": `{}`}, "placement: the repository holds no placement"},
{"a particulars file", map[string]string{"particulars": "ace\n"}, "particulars: the repository lists no node names"},
{"a read of the indexer's placement", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-delivery-indexer.placement"]}`}, `a: names "mesh-delivery-indexer.placement"`},
{"a read of its candidates", map[string]string{"modules/b/module.json": `{"module":"b","reads":["mesh-delivery-indexer.placement-candidates"]}`}, "placement-candidates"},
{"its event", map[string]string{"modules/c/module.json": `{"module":"c","on":[{"event":"mesh-delivery-indexer.placement-changed"}]}`}, "placement-changed"},
// Issue 774 (ADR 0549 §3, §6): a reader of the controller's assignments is refused unless the list names it, the
// controller's own manifest is not judged, and a name on the list whose module no longer reads them is refused.
{"an unlisted reader of the assignments", map[string]string{"modules/r/module.json": `{"module":"r","reads":["mesh-controller.assignments"]}`}, "r reads mesh-controller.assignments"},
{"a listed reader and the controller", map[string]string{"modules/l/module.json": `{"module":"l","reads":["mesh-controller.assignments"]}`,
"modules/mesh-controller/module.json": `{"module":"mesh-controller","reads":["mesh-controller.assignments"]}`, Allowed: "# why\nl its plan\n"}, ""},
{"a listed module that reads them no more", map[string]string{Allowed: "gone no longer reads\n"}, "names gone, which no longer reads"},
{"the seat's own state is allowed", map[string]string{"modules/d/module.json": `{"module":"d","reads":["mesh-placement.placement"]}`}, ""},
} {
t.Run(row.name, func(t *testing.T) {
@@ -52,4 +60,29 @@ func TestTheCatalogueHoldsNoPlacementOfTheRepositoryPath(t *testing.T) {
for _, f := range got {
t.Error(f)
}
// Issue 774 (ADR 0549 §3, §6; the comparison of novox/hq ADR 0529 clause 2): the list of modules still reading the
// controller's assignments gains none the trunk's copy at the merge base does not hold. Its first copy gains nothing.
path, ok := trunk.Copy(t, Allowed)
if !ok {
return
}
names := func(path string) map[string]bool {
raw, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
out := map[string]bool{}
for _, line := range strings.Split(string(raw), "\n") {
if f := strings.Fields(line); len(f) > 0 && !strings.HasPrefix(f[0], "#") {
out[f[0]] = true
}
}
return out
}
base := names(path)
for module := range names(filepath.Join("..", "..", Allowed)) {
if !base[module] {
t.Errorf("%s gains %s, which the trunk's copy does not hold: the list only shrinks", Allowed, module)
}
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ account alone writes its buckets.
## How a value resolves
For every module the catalogue's placement puts on a node (`mesh-delivery-indexer.placement`, ADR 0549 §3), from the
For every module the seat mesh-placement's placement puts on a node (`mesh-delivery-assigner.placement`, ADR 0567), from the
controller's mirror of each module's declarations (`mesh-controller.declared-config`): the manifest's default; mesh config for the predefined key the value falls back
to; node config for that key on the node; the module's own value on that node, the most specific winning. A value
naming no predefined key resolves from its default and the module's own value only (ADR 0348 §2). It resolves at
@@ -121,13 +121,13 @@ func (m *memStore) mirror(t *testing.T, read, key, raw string) {
m.states[read][key] = []byte(raw)
}
// assign puts module on node in the catalogue's placement (ADR 0549 §3), beside what is placed there already.
// assign puts module on node in the seat's placement (ADR 0549 §3), beside what is placed there already.
func (m *memStore) assign(t *testing.T, node, module string) {
t.Helper()
m.place(t, node, func(modules []string) []string { return append(modules, module) })
}
// unassign takes module off node in the catalogue's placement.
// unassign takes module off node in the seat's placement.
func (m *memStore) unassign(t *testing.T, node, module string) {
t.Helper()
m.place(t, node, func(modules []string) []string {
@@ -592,7 +592,7 @@ func TestTheVerbsArgumentsAreReadStrictly(t *testing.T) {
}
}
// Issue 774 (ADR 0549 §3, §5): the catalogue's placement alone says where modules stand. Before any node's placement
// Issue 774 (ADR 0549 §3, §5): the seat's placement alone says where modules stand. Before any node's placement
// is read nothing is resolved anew and what is resolved stays; a placement that does not read resolves nothing on its
// node, said.
func TestThePlacementAloneSaysWhereValuesResolve(t *testing.T) {
@@ -121,7 +121,7 @@ type Published struct {
Commit string `json:"commit"`
}
// Assignment is a module the catalogue's placement puts on a node (ADR 0549 §3): what a resolution resolves values for.
// Assignment is a module the seat's placement puts on a node (ADR 0549 §3): what a resolution resolves values for.
type Assignment struct {
Node string
Module string
@@ -2,7 +2,7 @@ package main
import "testing"
// 1126 (ADR 0549 §3), issue 774: a module the catalogue's placement names on a node resolves there; it goes when the
// 1126 (ADR 0549 §3), issue 774: a module the seat's placement names on a node resolves there; it goes when the
// placement no longer names it. A module another pulls in is placed in its own right (the seed placed what runs).
func TestAModulePlacedHasItsValuesResolvedOnItsNode(t *testing.T) {
c, m := aMesh(t)
@@ -122,10 +122,10 @@ type Resolved struct {
// ResolvedKey is a resolved value's key in mesh-config_resolved.
func ResolvedKey(node, module, key string) string { return node + "." + module + "." + key }
// Sources is everything a resolution reads: where modules stand (the catalogue's placement), every module's
// Sources is everything a resolution reads: where modules stand (the seat's placement), every module's
// declarations, the published list of predefined keys, and every value set.
type Sources struct {
// Assignments is every module the catalogue's placement puts on a node (ADR 0549 §3), and Placed whether any node's
// Assignments is every module the seat's placement puts on a node (ADR 0549 §3), and Placed whether any node's
// placement was read: without one nothing is resolved anew.
Assignments []Assignment
Placed bool
+1 -1
View File
@@ -140,7 +140,7 @@ func watchWants(env *Env) bool {
return true
}
// placement reads the indexer's placement through the runtime under the vault's own account (its manifest reads it).
// placement reads the seat mesh-placement's placement through the runtime under the vault's own account (its manifest reads it).
var placement = PlacementReader{
Keys: func() ([]string, error) { return stdio.State(PlacementRead).Keys() },
Read: func(key string) ([]byte, error) {
@@ -317,7 +317,7 @@ func TestTheVaultWholeMintsOnTheBusOnlyFromTheLeaseWrite(t *testing.T) {
t.Fatal(err)
}
for _, b := range []string{"mesh-vault_bus-accounts", "mesh-grants_accounts", "mesh-controller_lease",
"mesh-controller_assignments", "node-engine_have", "mesh-delivery-indexer_placement"} {
"mesh-controller_assignments", "node-engine_have", "mesh-delivery-assigner_placement"} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 2}); err != nil {
t.Fatal(err)
}
+6 -6
View File
@@ -1,8 +1,8 @@
package main
// Where modules stand, from the catalogue's placement (novox/hq ADR 0549 §3, §5; ADR 0561; issue 774). The controller's
// assignments mirror goes with its lease, so the vault reads which module stands on which node from the indexer's
// placement first, and the assignments beside it while both stand: a module either names stands. The provisioner's plan,
// Where modules stand, from the placement (novox/hq ADR 0549 §3, §5; ADR 0561; issue 774), the seat mesh-placement's
// state held by the assigner (ADR 0567). The controller's assignments mirror goes with its lease, so the vault reads which
// module stands on which node from the seat's placement first, and the assignments beside it while both stand: a module either names stands. The provisioner's plan,
// which only an assignment's value carries, is read from the assignments still: its home after the lease is a subtask of
// its own (issue 774).
@@ -16,10 +16,10 @@ import (
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// PlacementRead is the indexer's placement, as the vault's manifest reads it.
const PlacementRead = delivery.IndexerModule + "." + delivery.PlacementState
// PlacementRead is the seat mesh-placement's placement, as the vault's manifest reads it (mesh-sdk's contract).
const PlacementRead = delivery.PlacementRead
// PlacementReader reads the indexer's placement: every key, and one key's value (nil when there is none).
// PlacementReader reads the seat's placement: every key, and one key's value (nil when there is none).
type PlacementReader struct {
Keys func() ([]string, error)
Read func(key string) ([]byte, error)
@@ -7,7 +7,7 @@ import (
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// placementsOf is the indexer's placement of each node, as the vault reads it.
// placementsOf is the seat mesh-placement's placement of each node, as the vault reads it.
func placementsOf(t *testing.T, nodes map[string][]string) PlacementReader {
t.Helper()
values := map[string][]byte{}
@@ -35,7 +35,7 @@ func placementsOf(t *testing.T, nodes map[string][]string) PlacementReader {
}
}
// 774/AC11 (novox/hq ADR 0561, ADR 0549 §3, §5): with no assignments to read, the vault tells from the catalogue's
// 774/AC11 (novox/hq ADR 0561, ADR 0549 §3, §5): with no assignments to read, the vault tells from the seat's
// placement which node holds the bus and whether a consumer's module stands on its node; while both stand, a module
// either names stands.
func TestTheVaultReadsWhereModulesStandFromPlacement(t *testing.T) {
@@ -47,15 +47,15 @@ func TestTheVaultReadsWhereModulesStandFromPlacement(t *testing.T) {
asked := func(node, module string) CredentialAsked {
return CredentialAsked{Role: "consumer", Provision: "postgres", Consumer: node, ConsumerModule: module, Provider: "novox"}
}
if why := v.credentialRefusal("ace", asked("ace", "shop")); why != "" {
if why := v.credentialRefusal("ace", asked("ace", "shop"), v.plansOf("ace")); why != "" {
t.Errorf("shop stands on ace by placement, refused: %s", why)
}
if why := v.credentialRefusal("ace", asked("ace", "web")); !strings.Contains(why, "is not on ace") {
if why := v.credentialRefusal("ace", asked("ace", "web"), v.plansOf("ace")); !strings.Contains(why, "is not on ace") {
t.Errorf("web is not placed on ace, and is answered %q", why)
}
// While both stand, an assignment still names a module placement does not yet.
v.Askers = Declared{Read: assignments{"ace.web": `{"node":"ace","module":"web"}`}.read, Store: v.Store}
if why := v.credentialRefusal("ace", asked("ace", "web")); why != "" {
if why := v.credentialRefusal("ace", asked("ace", "web"), v.plansOf("ace")); why != "" {
t.Errorf("web assigned on ace, refused: %s", why)
}
// Two nodes placed with the bus: the vault adopts from neither.
+1 -1
View File
@@ -312,7 +312,7 @@ func (v *Vault) internalName(node string) (string, error) {
return "", fmt.Errorf(notYetPrefix+"the placement of %s could not be read: %w", node, err)
}
if raw == nil {
return "", fmt.Errorf("%s has no placement in the catalogue, so it has no name inside the mesh", node)
return "", fmt.Errorf("%s has no placement of the seat mesh-placement, so it has no name inside the mesh", node)
}
p, err := delivery.DecodePlacement(raw)
if err != nil {
+1 -1
View File
@@ -64,7 +64,7 @@ type Vault struct {
adoption adoptionKept
// WantRead reads one wanted account afresh (issue-account): nil reads the wants as the watch handed them.
WantRead func(user string) ([]byte, bool, error)
// Placement reads the catalogue's placement (placed.go): where modules stand once the assignments are gone.
// Placement reads the seat's placement (placed.go): where modules stand once the assignments are gone.
Placement PlacementReader
// LeaseRead reads the controller's lease afresh before a mint (lease.go): nil reads it only as the watch hands it.
LeaseRead func() ([]byte, bool, error)
@@ -134,7 +134,7 @@ func filesUnder(t *testing.T, dir string) map[string]string {
// (ADR 0547 clause 9), opened in accounts.go alone. It reads five buckets of others', and only reads them: the
// controller's assignments, for what each node's plan names (askers.go, design 57 V4), the node-engines' haves, where
// the bus node hands the loaded list's hashes sealed to the vault (adopt.go, ADR 0547 clause 9), the grants part's wanted
// accounts and the controller's lease (mint.go, lease.go, ADR 0561 §2, §3), and the catalogue's placement (placed.go), in
// accounts and the controller's lease (mint.go, lease.go, ADR 0561 §2, §3), and the seat's placement (placed.go), in
// main.go alone.
func TestNoBucketHoldsASecret(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
@@ -149,7 +149,7 @@ func TestNoBucketHoldsASecret(t *testing.T) {
t.Errorf("the manifest declares buckets %s: a secret is in no bucket, sealed or not (ADR 0201 §13), and the "+
"buckets are the accounts' hashes (ADR 0547 clause 9) and the signing keys' public halves (ADR 0550 §2)", s)
}
if r, _ := json.Marshal(manifest["reads"]); string(r) != `["mesh-controller.assignments","node-engine.have","mesh-grants.accounts","mesh-controller.lease","mesh-delivery-indexer.placement"]` {
if r, _ := json.Marshal(manifest["reads"]); string(r) != `["mesh-controller.assignments","node-engine.have","mesh-grants.accounts","mesh-controller.lease","mesh-delivery-assigner.placement"]` {
t.Errorf("the manifest reads %s: the vault reads the controller's assignments, the haves and the grants part's "+
"wanted accounts alone", r)
}
@@ -189,7 +189,7 @@ func TestNoBucketHoldsASecret(t *testing.T) {
// And the controller's lease, one read and one watch (ADR 0561 §3).
code = []byte(strings.Replace(string(code), "stdio.State(LeaseRead).Get(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(LeaseRead).Watch(", "", 1))
// And the catalogue's placement, where modules stand (placed.go): its keys and one key, read.
// And the seat's placement, where modules stand (placed.go): its keys and one key, read.
code = []byte(strings.Replace(string(code), "stdio.State(PlacementRead).Keys(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(PlacementRead).Get(", "", 1))
}
+1 -1
View File
@@ -25,7 +25,7 @@
"node-engine.have",
"mesh-grants.accounts",
"mesh-controller.lease",
"mesh-delivery-indexer.placement"
"mesh-delivery-assigner.placement"
],
"state": [
{