From 97d3df3e59ce9ac02cb0d6138247b8356eb26e48 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 23:27:28 +0100 Subject: [PATCH] =?UTF-8?q?G=20=C2=A7422,=20=C2=A7698:=20the=20node-engine?= =?UTF-8?q?=20serves=20first-views,=20first-view-again=20and=20entry-again?= =?UTF-8?q?,=20and=20its=20manifest=20names=20the=20verbs=20it=20serves?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- cmd/mesh-host/engineseat.go | 24 ++++++++++++++ cmd/mesh-host/main.go | 1 + internal/link/handacts.go | 59 ++++++++++++++++++++++++++++++++++ internal/link/handover_test.go | 19 +++++++++-- module.json | 21 +++++++++++- 5 files changed, 121 insertions(+), 3 deletions(-) diff --git a/cmd/mesh-host/engineseat.go b/cmd/mesh-host/engineseat.go index 31c7731c..2b14c308 100644 --- a/cmd/mesh-host/engineseat.go +++ b/cmd/mesh-host/engineseat.go @@ -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) + } +} diff --git a/cmd/mesh-host/main.go b/cmd/mesh-host/main.go index cf5c9e84..193efcc6 100644 --- a/cmd/mesh-host/main.go +++ b/cmd/mesh-host/main.go @@ -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) diff --git a/internal/link/handacts.go b/internal/link/handacts.go index 28ad5861..83ae1106 100644 --- a/internal/link/handacts.go +++ b/internal/link/handacts.go @@ -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, diff --git a/internal/link/handover_test.go b/internal/link/handover_test.go index 3fb5491d..563baf0c 100644 --- a/internal/link/handover_test.go +++ b/internal/link/handover_test.go @@ -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) } }) diff --git a/module.json b/module.json index 2fef2be6..f6f38d1f 100644 --- a/module.json +++ b/module.json @@ -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": [