W16.6: the tool runner reads which modules exist and their manifests from the catalog half through meshcatalogclient's branch follower on its own consumer (milestone 47/G)

firstview adopts the shared branch follower over a wiring of its own consumer (views held with the assigner's, findings through its
held writer, events routed to each adopted view); the console's Source gives Modules from the catalog entries, the manifest an entry
carries whole; the indexer's modules and manifests first views and their consumes are removed, the registrations and the gate's
source-moved are consumed.

Builds: design 47 G §196, §594, §614; lead answers A-5, A-27, A-103

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-11 23:40:23 +01:00
parent 4bfae38a73
commit 3501164f4b
9 changed files with 333 additions and 131 deletions
+20 -56
View File
@@ -19,7 +19,7 @@ package console
import (
assigner "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
meshcatalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"context"
@@ -95,8 +95,7 @@ func discovery() []map[string]any {
"bus, and when it was last heard — and every runtime or machine expected that was not heard. Read-only; " +
"for when an address is said to be missing or on another machine."},
{"name": verbFirstViews, "inputSchema": obj(map[string]any{}),
"description": "The tool runner's first views of the four states it reads of the indexer and the assigner (the " +
"assigner's assignments and seat-holders, the indexer's manifests and modules): for each, the owner, the tool " +
"description": "The tool runner's first views: the assigner's assignments and seat-holders, and the catalog half's registrations, the gate's source and the view of each watched branch (which modules exist, each with its manifest): for each, the owner, the tool " +
"asked, the revision held and when it was made, how many keys, when it was asked and answered, how often " +
"it was asked and how often by hand, whether its condition is raised, and why it was last asked. Read-only."},
{"name": verbFirstAgain, "inputSchema": obj(map[string]any{
@@ -359,14 +358,14 @@ func indexOn(conn *bus.Conn, src Source, held announce.Known, before func(announ
return x, nil
}
// Source is where the console reads the four states of the indexer and the assigner it indexes the mesh by: the assigner's
// assignments and seat-holders, the indexer's modules and manifests, by `<owner>.<state>` (firstview.AssignmentsRead and the
// rest). The tool runner's is its first views (firstview.Views), never the owners' state (novox/hq ADR 0615 §1).
// Source is where the console reads what it indexes the mesh by: the assigner's assignments and seat-holders, by
// `<owner>.<state>` (firstview.AssignmentsRead and SeatHoldersRead), and the modules of the catalog half with their entries.
// The tool runner's is its first views (firstview.Views), never the owners' state (novox/hq ADR 0615 §1; design 47 G §196, §594).
type Source interface {
// State is every current value of a state by key; an error says why it cannot be told yet.
State(name string) (map[string][]byte, error)
// Entry is one value of a state, nil when it holds none.
Entry(name, key string) ([]byte, error)
// Modules is every module the catalog half holds, by name, with its entry; an error says why it cannot be told yet.
Modules() (map[string]meshcatalog.CatalogEntry, error)
}
// seatHoldersOn reads the state seat-holders whole (novox/hq ADR 0571 clause 4), as firstview.SeatHoldersRead.
@@ -374,22 +373,12 @@ func seatHoldersOn(src Source) (map[string][]byte, error) {
return src.State(firstview.SeatHoldersRead)
}
// The indexer's states the console reads (novox/hq ADR 0571 clauses 4 and 5): every module, and each manifest by its
// digest, by the names a reader's manifest declares.
var (
modulesRead = firstview.ModulesRead
manifestsRead = firstview.ManifestsRead
)
// manifestFacts is what the console takes of a manifest: the tools it declares and what each replaces. A manifest's
// digest never names another, so what is read once is kept.
// manifestFacts is what the console takes of a manifest: the tools it declares and what each replaces.
type manifestFacts struct {
Tools []string `json:"tools"`
Replaces map[string][]string `json:"replaces"`
}
var manifestsKept sync.Map // digest → manifestFacts
// recordsOn is every module the indexer holds, with where the assigner seat places it and whether it declares tools,
// and every machine the assignments name (moduleNames, assigner.MachineNames). What cannot be read is said, one line
// per state, for the listing's not answering.
@@ -411,18 +400,19 @@ func recordsOn(src Source) (recorded []recordedModule, machines []string, why []
on[m.Module] = append(on[m.Module], node)
}
}
modules, err := src.State(modulesRead)
entries, err := src.Modules()
if err != nil {
return nil, machines, append(why, modulesRead+" (which modules there are could not be read: "+err.Error()+")")
return nil, machines, append(why, "the catalog half's modules (which modules there are could not be read: "+err.Error()+")")
}
names, err := moduleNames(modules)
if err != nil {
return nil, machines, append(why, modulesRead+" ("+err.Error()+")")
names := make([]string, 0, len(entries))
for name := range entries {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
r := recordedModule{Module: name, On: on[name]}
if m, err := meshcatalogclient.DecodeModule(modules[name]); err == nil && m.Manifest != "" {
facts, err := manifestOf(src, m.Manifest)
if m := entries[name].Manifest; len(m) > 0 {
facts, err := manifestOf(m)
if err != nil {
why = append(why, name+" (its manifest could not be read, so whether it answers tools is not known: "+err.Error()+")")
} else {
@@ -434,42 +424,16 @@ func recordsOn(src Source) (recorded []recordedModule, machines []string, why []
return recorded, machines, why
}
// moduleNames is every module the indexer holds, sorted, from its state `modules` as a reader holds its keys (key to
// value; a deleted key absent). A key that is not a module's is refused, naming it.
func moduleNames(keys map[string][]byte) ([]string, error) {
out := make([]string, 0, len(keys))
for k := range keys {
module, err := meshcatalogclient.ParseModulesKey(k)
if err != nil {
return nil, err
}
out = append(out, module)
}
sort.Strings(out)
return out, nil
}
// manifestOf is what the console takes of the manifest of a digest, read from the indexer's manifests once.
func manifestOf(src Source, digest string) (manifestFacts, error) {
if kept, ok := manifestsKept.Load(digest); ok {
return kept.(manifestFacts), nil
}
entry, err := src.Entry(manifestsRead, digest)
if err != nil {
return manifestFacts{}, err
}
if entry == nil {
return manifestFacts{}, fmt.Errorf("the indexer holds no manifest %s", digest)
}
m, err := meshcatalogclient.DecodeManifest(entry)
// manifestOf is what the console takes of the manifest an entry of the catalog half carries whole (design 47 G §443).
func manifestOf(manifest map[string]any) (manifestFacts, error) {
raw, err := json.Marshal(manifest)
if err != nil {
return manifestFacts{}, err
}
var facts manifestFacts
if err := json.Unmarshal([]byte(m.Manifest), &facts); err != nil {
if err := json.Unmarshal(raw, &facts); err != nil {
return manifestFacts{}, err
}
manifestsKept.Store(digest, facts)
return facts, nil
}
+2 -2
View File
@@ -32,8 +32,8 @@ func (s *Surface) firstViewsAnswer(ctx context.Context, name string, args map[st
}
// A hand act, recorded through the seat mesh-hand-acts (novox/hq ADR 0615 §6, ADR 0565 §10) before it is done, and in the
// runner's log. One that cannot be recorded is said, and the ask is not made.
log.Printf("[mesh-tools] hand act: %s asked the four first views again: %s", s.who, why)
if err := s.recordAct(map[string]any{"what": "asked the tool runner's four first views again", "why": why,
log.Printf("[mesh-tools] hand act: %s asked the first views again: %s", s.who, why)
if err := s.recordAct(map[string]any{"what": "asked the tool runner's first views again", "why": why,
"cause": "first-view-again", "verb": verbFirstAgain}); err != nil {
return failure("the hand act was not recorded through mesh-hand-acts, so the first views were not asked again: " + err.Error())
}
@@ -43,13 +43,13 @@ func TestTheManifestNamesTheFirstFamiliesOneByOne(t *testing.T) {
t.Errorf("a family named by a wildcard: %s", w)
}
}
// A wait is no consumer (ADR 0367 §2): what it consumes is the four views families of its first views (ADR 0615 §5, issue
// 1966 TLS-5) and the two events of the seat that serves the seat list (ADR 0636 §4), and nothing else.
// Design 47 G §196, §387: the catalog half by `catalog-revision` (never the indexer's `views.modules`), and the keeper's
// `findings-asked`; the finding stream below. `views.modules` goes with the first view that follows it (T-2).
wantConsumes := []string{"mesh-catalog.catalog-revision", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed",
"mesh-conditions.findings-asked", "mesh-delivery-assigner.views.assignments", "mesh-delivery-assigner.views.seat-holders",
"mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules"}
// A wait is no consumer (ADR 0367 §2): what it consumes is the assigner's two views families (ADR 0615 §5, issue 1966
// TLS-5), the events of the catalog half's branch follower (design 47 G §196, §614: `catalog-revision`,
// `registration-changed`, and the gate's `source-moved`; never the indexer's `views.modules` or `views.manifests`), the two
// events of the seat that serves the seat list (ADR 0636 §4) and the keeper's `findings-asked`, and nothing else.
wantConsumes := []string{"mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-catalog.seat-definition-changed",
"mesh-catalog.seat-definition-removed", "mesh-conditions.findings-asked", "mesh-delivery-assigner.views.assignments",
"mesh-delivery-assigner.views.seat-holders", "mesh-delivery.source-moved"}
if !reflect.DeepEqual(m.Consumes, wantConsumes) {
t.Errorf("the tool runner consumes %v, want %v", m.Consumes, wantConsumes)
}
@@ -1,9 +1,13 @@
package console
import (
"encoding/json"
"sync"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"github.com/novox/mesh-tools/node-tools/internal/bus"
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
)
// busSource is a Source over a bus connection's own states, which a test's fixture mesh fills: the console's tests stand a
@@ -37,3 +41,20 @@ func (b busSource) Entry(name, key string) ([]byte, error) {
}
return entry.Value, nil
}
// Modules reads the test bed's entries of the catalog half (meshtest.ModulesRead), as the tool runner's first views hold them.
func (b busSource) Modules() (map[string]meshcatalog.CatalogEntry, error) {
raw, err := b.State(mt.ModulesRead)
if err != nil {
return nil, err
}
out := map[string]meshcatalog.CatalogEntry{}
for module, value := range raw {
var e meshcatalog.CatalogEntry
if err := json.Unmarshal(value, &e); err != nil {
return nil, err
}
out[module] = e
}
return out, nil
}