DLV-M6: the enrolment follows the assigner's placement by its read tool's first view and its views family, never its state

ADR 0615 §1: views.Reader asks mesh-delivery-assigner.assigner_read_placement and hears views.placement;
the holder reads placement from the picture and every other state through the runtime. The manifest drops
the read, invokes the tool, consumes the event, keeps findings and serves enrolment_first_views and
enrolment_first_view_again.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 13:56:31 +01:00
parent 28c3b27fec
commit 1ce394db97
5 changed files with 108 additions and 14 deletions
@@ -20,6 +20,7 @@ func TestIssueTokensAndTheManifest(t *testing.T) {
{"S54/AC1 tokens answers each stage and relays the sealed token of one node", tokensAnswersEachStageAndRelaysTheSealedTokenOfOneNode},
{"S54/AC2 the manifest says what the code does", theManifestSaysWhatTheCodeDoes},
{"S56 the bus node's loaded list is read from the grants part's pass", theLoadedListIsReadFromTheGrantsPass},
{"DLV-M6 the placement is read from its first view, never from the assigner's state", thePlacementIsReadFromItsFirstView},
} {
t.Run(c.name, c.run)
}
@@ -59,3 +60,30 @@ func theLoadedListIsReadFromTheGrantsPass(t *testing.T) {
t.Error("no pass composed read as an answer")
}
}
// thePlacementIsReadFromItsFirstView: the holder's placement comes from the picture of the assigner's placement (its first
// view and views family, ADR 0615 §1), every other state through the runtime; the runtime is never asked for placement.
func thePlacementIsReadFromItsFirstView(t *testing.T) {
pics := memPictures{PlacementRead: {"joiner.node": `{"v":1,"node":"joiner","decided":{"address":"10.9.0.11"},"modules":[]}`}}
rest := memRead{PlacementRead: {"stale.node": `{"v":1,"node":"stale","modules":[]}`}, "node-engine.have": {}}
h := &Holder{Read: followedReader{pics: pics, rest: rest}}
p, err := h.placements()
if err != nil {
t.Fatal(err)
}
if _, ok := p["joiner"]; !ok || len(p) != 1 {
t.Errorf("placements = %v, want joiner's alone, from the first view", p)
}
if keys, err := h.Read.Keys("node-engine.have"); err != nil || len(keys) != 0 {
t.Errorf("node-engine.have read as %v, %v: want it through the runtime", keys, err)
}
}
// memPictures is the followed states (views.Reader), `<owner>.<state>` then key.
type memPictures map[string]map[string]string
func (p memPictures) Follows(state string) bool { _, ok := p[state]; return ok }
func (p memPictures) Keys(state string) ([]string, error) {
return memRead(p).Keys(state)
}
func (p memPictures) Get(state, key string) ([]byte, bool, error) { return memRead(p).Get(state, key) }
@@ -189,13 +189,13 @@ func (h *Holder) openEnrolments() (map[string]Row, map[string]Enrolment, error)
// placements reads every node's placement, assembled from its keys.
func (h *Holder) placements() (map[string]delivery.Placement, error) {
keys, err := h.Read.Keys("mesh-delivery-assigner.placement")
keys, err := h.Read.Keys(PlacementRead)
if err != nil {
return nil, fmt.Errorf("placement cannot be listed: %w", err)
}
held := map[string][]byte{}
for _, k := range keys {
if raw, found, err := h.Read.Get("mesh-delivery-assigner.placement", k); err != nil {
if raw, found, err := h.Read.Get(PlacementRead, k); err != nil {
return nil, fmt.Errorf("placement key %s cannot be read: %w", k, err)
} else if found {
held[k] = raw
@@ -5,6 +5,7 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
@@ -13,7 +14,9 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asker"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/handacts"
"git.novox.be/novox/mesh-sdk/go/views"
)
func logf(format string, a ...any) { fmt.Fprintf(os.Stderr, format+"\n", a...) }
@@ -84,14 +87,58 @@ func (runtimeReader) Get(state, key string) ([]byte, bool, error) {
return e.Value, true, nil
}
// pictures are the states the enrolment follows rather than reads (views.Reader): the assigner's placement (ADR 0615 §1).
type pictures interface {
Follows(state string) bool
Keys(state string) ([]string, error)
Get(state, key string) ([]byte, bool, error)
}
// followedReader reads a followed state from its picture and every other state through rest.
type followedReader struct {
pics pictures
rest Reader
}
func (r followedReader) Keys(state string) ([]string, error) {
if r.pics != nil && r.pics.Follows(state) {
return r.pics.Keys(state)
}
return r.rest.Keys(state)
}
func (r followedReader) Get(state, key string) ([]byte, bool, error) {
if r.pics != nil && r.pics.Follows(state) {
return r.pics.Get(state, key)
}
return r.rest.Get(state, key)
}
// PlacementRead is the assigner's placement as the enrolment reads it, and PlacementTool the assigner's read tool the
// manifest invokes for its first view (ADR 0615, DLV-M6).
const (
PlacementRead = delivery.AssignerModule + "." + delivery.PlacementState
PlacementTool = delivery.AssignerModule + ".assigner_read_placement"
)
// followed is the enrolment's pictures of the assigner's placement, started at once: its first view asked through
// PlacementTool, then the assigner's views family; past its bound the finding in the enrolment's own findings.
func followed() *views.Reader {
v := &views.Reader{Module: Module, Logf: func(f string, a ...any) { logf("["+Module+"] "+f, a...) }, Findings: views.OwnFindings{},
Acts: handacts.Recorder{Ask: stdio.Ask}}
v.Follow(delivery.AssignerModule, delivery.PlacementState, PlacementTool)
v.Start(context.Background())
return v
}
// SeatEventSubject is where the seat says an event: mesh.seat.<seat>.event.<name>.
func SeatEventSubject(event string) string { return "mesh.seat." + Seat + ".event." + event }
// newHolder is the holder over the runtime.
func newHolder() *Holder {
func newHolder(v *views.Reader) *Holder {
call := Call(stdio.Ask)
rec := handacts.Recorder{Ask: stdio.Ask}
rd := runtimeReader{}
rd := followedReader{pics: v, rest: runtimeReader{}}
client := asker.New("asked")
return &Holder{
Store: runtimeStore{}, Read: rd, Vault: vaultOver{call: call, read: rd}, Asker: client,
@@ -169,10 +216,11 @@ func loadedVia(rd Reader) func(string) (bool, error) {
}
func main() {
h := newHolder()
v := followed()
h := newHolder(v)
client := h.Asker.(*asker.Client)
h.Run(func() error { return client.Listen(h.Warranted, h.Said) })
if err := stdio.Serve("", Tools(h)); err != nil {
if err := stdio.Serve("", append(Tools(h), v.Tools("enrolment")...)); err != nil {
logf("%v", err)
os.Exit(1)
}
@@ -82,9 +82,11 @@ func theManifestSaysWhatTheCodeDoes(t *testing.T) {
Scope string `json:"scope"`
Serves []string `json:"serves"`
} `json:"claims"`
Emits []string `json:"emits"`
Reads []string `json:"reads"`
State []json.RawMessage `json:"state"`
Emits []string `json:"emits"`
Reads []string `json:"reads"`
Invokes []string `json:"invokes"`
Tools []string `json:"tools"`
State []json.RawMessage `json:"state"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
@@ -96,10 +98,20 @@ func theManifestSaysWhatTheCodeDoes(t *testing.T) {
if slices.Contains(m.Reads, "mesh-controller.lease") {
t.Error("the manifest reads the controller's lease, which the code no longer reads (issue 1126)")
}
// The assigner's placement is followed, never read (ADR 0615 §1, DLV-M6): its read tool invoked, its views family consumed.
for _, r := range m.Reads {
if strings.HasPrefix(r, "mesh-delivery-indexer.") || strings.HasPrefix(r, "mesh-delivery-assigner.") {
t.Errorf("reads %s: the indexer's and the assigner's state is read by their tools and views families alone", r)
}
}
if !slices.Contains(m.Invokes, PlacementTool) || !slices.Equal(m.Tools, []string{"enrolment_first_views", "enrolment_first_view_again"}) {
t.Errorf("invokes %v, tools %v: want %s and the first-view tools", m.Invokes, m.Tools, PlacementTool)
}
var heard []string
for _, e := range EndEvents {
heard = append(heard, Seat+"."+e)
}
heard = append(heard, "mesh-delivery-assigner.views.placement")
if !slices.Equal(m.Consumes, heard) {
t.Errorf("consumes %v, the end events are %v", m.Consumes, heard)
}
@@ -112,7 +124,7 @@ func theManifestSaysWhatTheCodeDoes(t *testing.T) {
t.Errorf("the verb %s has no tool", v)
}
}
if len(m.State) != 2 || string(m.State[0]) == "" || !slices.ContainsFunc(m.State, func(s json.RawMessage) bool {
if len(m.State) != 3 || string(m.State[0]) == "" || !slices.ContainsFunc(m.State, func(s json.RawMessage) bool {
var d struct {
Name string `json:"name"`
TTL bool `json:"key-ttl"`
+10 -4
View File
@@ -20,7 +20,8 @@
"mesh-enrolment.node-enrolled",
"mesh-enrolment.enrolment-refused",
"mesh-enrolment.enrolment-revoked",
"mesh-enrolment.enrolment-expired"
"mesh-enrolment.enrolment-expired",
"mesh-delivery-assigner.views.placement"
],
"uses": [
"operator-channel"
@@ -31,10 +32,10 @@
"seat:mesh-placement.propose",
"seat:mesh-vault.enrolment-token",
"seat:mesh-vault.node-account",
"seat:mesh-vault.sign"
"seat:mesh-vault.sign",
"mesh-delivery-assigner.assigner_read_placement"
],
"reads": [
"mesh-delivery-assigner.placement",
"mesh-grants.user-list",
"mesh-vault.signers",
"node-engine.have"
@@ -47,7 +48,12 @@
{
"name": "asked",
"history": 5
}
},
"findings"
],
"tools": [
"enrolment_first_views",
"enrolment_first_view_again"
],
"own-secrets": {
"broker": "${dir:state}/broker"