G §422, §698: the node-engine serves first-views, first-view-again and entry-again, and its manifest names the verbs it serves

first-views answers {first-views, entries, wants}, entry-again asks one entry again as a recorded hand act and
answers {asked, read}; first-view-again names no view the engine follows (it follows no branch) and refuses in
words. module.json's claim of node-engine lists every verb the engine serves.

Builds: design 47 G §422, §698
This commit is contained in:
2026-11-11 23:27:28 +01:00
parent 7703162c95
commit 97d3df3e59
5 changed files with 121 additions and 3 deletions
+24
View File
@@ -390,3 +390,27 @@ func composeCandidateFrom(node string, shadow *atomic.Pointer[selfcompose.Shadow
return json.RawMessage(raw), nil
}
}
// firstViewsFrom answers the seat's verb first-views from the composer of this run (design 47 G §698): the views the node-engine
// follows (none), the entries it keeps and the wants it composes from.
func firstViewsFrom(composer *atomic.Pointer[selfcompose.Composer]) func() (any, error) {
return func() (any, error) {
c := composer.Load()
if c == nil {
return nil, errors.New("this node composes none of its own modules now (it applies the controller's compositions, or its node agent is starting), so it keeps no entry")
}
views, entries, wants := c.FirstViews()
return map[string]any{"first-views": views, "entries": entries, "wants": wants}, nil
}
}
// entryAgainFrom asks one entry of the catalog half again, for the seat's verb entry-again (design 47 G §698).
func entryAgainFrom(composer *atomic.Pointer[selfcompose.Composer]) func(ctx context.Context, fingerprint string) error {
return func(ctx context.Context, fingerprint string) error {
c := composer.Load()
if c == nil {
return errors.New("this node composes none of its own modules now, so it keeps no entry to ask again")
}
return c.EntryAgain(ctx, fingerprint)
}
}
+1
View File
@@ -1019,6 +1019,7 @@ func runLink(ctx context.Context, opts options) error {
queue.Acts.Composition = compositionFrom(current)
queue.Acts.Compose = composeFrom(current, &composerNow)
queue.Acts.Packages = packagesFrom(mine.Node, queue, &composerNow)
queue.Acts.FirstViews, queue.Acts.EntryAgain = firstViewsFrom(&composerNow), entryAgainFrom(&composerNow)
queue.Acts.SeatList, queue.Acts.SeatListAgain = seatListRead, seatListAgain
queue.Acts.ProcessBuilds = func(module string) (any, error) {
d, err := declaredNow(opts.state, nil)
+59
View File
@@ -1,6 +1,7 @@
package link
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
@@ -55,6 +56,12 @@ const (
// (novox/hq ADR 0636 §7). The node-engine seat's definition carries both verbs.
ActSeatList = nodeengine.VerbSeatList
ActSeatListAgain = nodeengine.VerbSeatListAgain
// ActFirstViews reads the node-engine's first views: the views it follows, the entries it keeps of the builds its wants
// and runnings name, and the wants it composes from; ActFirstViewAgain asks a view again and ActEntryAgain an entry
// (design 47 G §422, §698).
ActFirstViews = nodeengine.VerbFirstViews
ActFirstViewAgain = nodeengine.VerbFirstViewAgain
ActEntryAgain = nodeengine.VerbEntryAgain
// ActProcessBuilds reads the build each process on this node was started with (design 47, *The fingerprint handover*);
// it reads only; who may call it is the seat definition's (design 47 G §440, §447).
ActProcessBuilds = nodeengine.VerbProcessBuilds
@@ -159,6 +166,10 @@ type HandActs struct {
// SeatList answers the seat list the engine holds; SeatListAgain makes it read the list now and answers what it holds.
SeatList func() (any, error)
SeatListAgain func() (any, error)
// FirstViews answers the verb first-views; EntryAgain asks one entry of the catalog half again, refusing in words one the
// engine keeps none of.
FirstViews func() (any, error)
EntryAgain func(ctx context.Context, fingerprint string) error
// ProcessBuilds answers the build each process of the node was started with, narrowed to a module when one is given.
ProcessBuilds func(module string) (any, error)
// ComposeCandidate composes a check's candidate (the indexer's candidate key) on this node in shadow and answers per
@@ -236,6 +247,21 @@ func (h *HandActs) Call(c EngineCall) (any, error) {
return nil, errors.New("this engine reads no seat list yet: its node agent has not started")
}
return h.SeatList()
case ActFirstViews:
if h.FirstViews == nil {
return nil, errors.New("this engine keeps no first views yet: its node agent has not started")
}
return h.FirstViews()
case ActFirstViewAgain:
if arg("owner") == "" || arg("state") == "" || arg("why") == "" {
return nil, errors.New("first-view-again names the owner, the state and why")
}
// The node-engine follows no branch and no view of another part's state: the views it holds are its entries (entry-again)
// and its seat list (seat-list-again).
return nil, fmt.Errorf("this engine follows no first view of %s's %s: its entries are asked again by entry-again, and its seat list by seat-list-again",
arg("owner"), arg("state"))
case ActEntryAgain:
return h.AskEntryAgain(c.Caller, arg("fingerprint"), arg("why"))
case ActProcessBuilds:
if h.ProcessBuilds == nil {
return nil, errors.New("this engine reads no process yet: its node agent has not started")
@@ -284,6 +310,39 @@ func (h *HandActs) Call(c EngineCall) (any, error) {
return nil, fmt.Errorf("the node seat %s serves no verb %q", EngineSeat, c.Verb)
}
// AskEntryAgain asks fingerprint's entry of the catalog half again by hand: a hand act recorded with who asked and why, then
// done. The answer is the ask and the verb that reads where it stands (design 47 G §698).
func (h *HandActs) AskEntryAgain(caller, fingerprint, why string) (any, error) {
if h.EntryAgain == nil {
return nil, errors.New("this engine reads no entry by fingerprint yet: its node agent has not started")
}
if fingerprint == "" || why == "" {
return nil, errors.New("entry-again names the build's fingerprint and why")
}
ctx, cancel := context.WithTimeout(context.Background(), VaultWithin)
defer cancel()
a := HandAct{ID: h.id(ActEntryAgain), Act: ActEntryAgain, Subject: fingerprint, Caller: caller, Why: why, At: h.now().UTC(), State: HandDone}
err := h.EntryAgain(ctx, fingerprint)
if err != nil {
a.State, a.Said = HandRefused, err.Error()
}
// A done ask is kept as an act; a refused one, which any caller may cause, rolls with the judgements.
record := h.record
if err != nil {
record = h.roll
}
if rerr := record(a); rerr != nil {
return nil, rerr
}
if err != nil {
return nil, err
}
return map[string]any{
"asked": map[string]any{"v": sdkkeys.Version, "fingerprint": fingerprint, "why": why, "by": caller, "at": a.At},
"read": ActFirstViews,
}, nil
}
// ---- the record --------------------------------------------------------------------------------------------------
// ActStore is the engine's own state, this node's keys alone (BusState prefixes them with the node): each act one key,
+17 -2
View File
@@ -271,6 +271,16 @@ func TestALinkThatHearsHandOversIsGivenTheQueues(t *testing.T) {
ManifestReads: func() ([]string, time.Time) {
return []string{"form:a", "seat:b"}, time.Date(2026, 11, 4, 10, 0, 0, 0, time.UTC)
},
// Design 47 G §422, §698: the first views and the entries asked again.
FirstViews: func() (any, error) {
return map[string]any{"first-views": []any{}, "entries": []any{}, "wants": []any{}}, nil
},
EntryAgain: func(_ context.Context, fingerprint string) error {
if fingerprint == "src2:gone" {
return errors.New("no entry of src2:gone is kept")
}
return nil
},
// Design 47: the build each process was started with, narrowed by module.
ProcessBuilds: func(module string) (any, error) {
return map[string]any{"processes": []any{}, "module": module}, nil
@@ -315,6 +325,11 @@ func TestALinkThatHearsHandOversIsGivenTheQueues(t *testing.T) {
{"seat-list-again", "mesh.seat.node-engine.call.seat-list-again.n1.ace~mesh-healers", `"read":true`},
{"seat-list-again", "mesh.seat.node-engine.call.seat-list-again.n1.ace~messenger", `"read":true`},
{"first-views", "mesh.seat.node-engine.call.first-views.n1.person~jochen", `"entries":[],"first-views":[],"wants":[]`},
{"first-view-again", "mesh.seat.node-engine.call.first-view-again.n1.person~jochen " + `{"owner":"mesh-catalog","state":"modules","why":"doubt"}`, `follows no first view of mesh-catalog's modules`},
{"entry-again", "mesh.seat.node-engine.call.entry-again.n1.person~jochen " + `{"fingerprint":"src2:a","why":"doubt"}`, `"fingerprint":"src2:a","v":1,"why":"doubt"},"read":"first-views"`},
{"entry-again", "mesh.seat.node-engine.call.entry-again.n1.person~jochen " + `{"fingerprint":"src2:gone","why":"doubt"}`, `no entry of src2:gone is kept`},
{"entry-again", "mesh.seat.node-engine.call.entry-again.n1.person~jochen " + `{"fingerprint":"src2:a"}`, `entry-again names the build's fingerprint and why`},
{"process-builds", "mesh.seat.node-engine.call.process-builds.n1.person~jochen " + `{"module":"web"}`, `"module":"web","processes":[]`},
{"process-builds", "mesh.seat.node-engine.call.process-builds.n1.ace~messenger", `"processes":[]`},
{"unserved", "mesh.seat.node-engine.call.unserved.n1.x", `"error":"the node seat node-engine serves no verb \"unserved\""`},
@@ -329,8 +344,8 @@ func TestALinkThatHearsHandOversIsGivenTheQueues(t *testing.T) {
}
}
acts, _ := h.Acts(0)
if got := acts.(map[string]any)["acts"].([]HandAct); len(got) != 3 || got[0].Caller != "unproven" ||
got[2].Caller != "shanks.node-tools" || got[2].Act != ActJudgeDirectories {
if got := acts.(map[string]any)["acts"].([]HandAct); len(got) != 5 || got[2].Caller != "unproven" ||
got[4].Caller != "shanks.node-tools" || got[4].Act != ActJudgeDirectories || got[0].Act != ActEntryAgain || got[0].State != HandRefused {
t.Errorf("the judgements are recorded as %+v", got)
}
})
+20 -1
View File
@@ -17,7 +17,26 @@
"claims": [
{
"name": "node-engine",
"scope": "node"
"scope": "node",
"serves": [
"hand-over",
"access-act",
"judge-directories",
"setuid-search",
"root-free",
"acts",
"packages",
"composition",
"compose",
"compose-candidate",
"manifest-reads",
"seat-list",
"seat-list-again",
"first-views",
"first-view-again",
"entry-again",
"process-builds"
]
}
],
"invokes": [