Join item 2: the readers follow through mesh-sdk's views.ProcessReader; sdk/firstview is gone

mesh-delivery's sdk/firstview moved whole into mesh-sdk go/views (J3): the builder, checker, watchman, teller and view
follow each owner's state with views.ProcessReader.Follow(owner, state, tool, stream), naming the owner's read tool as
their invokes name it and its views family as the owner's manifest's `streams` names it ("indexer-views",
"assigner-views"); a refused Follow stops the start. Their beds serve the owners with viewstest.Serve and grant with
viewstest.Grants by the same names. The view's tools ask the owners' read tools by their literal names. The assigner
names its own views family and the indexer's itself (views.AssignerStream and IndexerStream are gone).
Every views.StatusTools call takes handacts.Recorder{Ask: stdio.Ask}: a first view asked again by hand is a hand act,
recorded through seat:mesh-hand-acts.hand-act, which the builder's, checker's, watchman's, teller's and view's
manifests now invoke (a row in each manifest test, each failed first).
Row (A2j's written-time switch): TestRunNeedsItsCredential "a want stood since the assigner's bus wrote it": the
watchman's want time (createdOr) is the time the owner's bus wrote the key, through ProcessReader's first view (failed
with createdOr ignoring the entry's time). sdk/go.mod: nats.go is indirect now.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 15:10:36 +01:00
parent 2fe8d01e86
commit 952dedd0ed
32 changed files with 178 additions and 743 deletions
@@ -10,6 +10,7 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/handacts"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-sdk/go/process"
@@ -18,8 +19,12 @@ import (
"mesh-delivery-assigner/internal/assign"
)
// viewsStream is the assigner's views family on its old streams section, beside placement (ADR 0619 §2, ADR 0615 §5).
const viewsStream = views.AssignerStream
// viewsStream is the assigner's views family on its old streams section, beside placement (ADR 0619 §2, ADR 0615 §5),
// as its manifest's `streams` names it; indexerViewsStream is the indexer's, as the indexer's manifest names it.
const (
viewsStream = "assigner-views"
indexerViewsStream = "indexer-views"
)
// marksBucket is the assigner's state of what its views family said each state up to.
var marksBucket = delivery.AssignerModule + "_" + views.MarksState
@@ -67,7 +72,7 @@ func viewTools(getenv func(string) string, state func(string) views.Source, now
out = append(out, views.ReadTool(delivery.AssignerModule, t.tool, t.state, t.words, state(t.state), marks, now))
}
// Its own first views, of the indexer's manifests and modules (DLV-F3r): read, and asked again by hand.
return append(out, views.StatusTools(delivery.AssignerModule, "assigner_first_views", "assigner_first_view_again", stdio.State(views.StatusState), now)...)
return append(out, views.StatusTools(delivery.AssignerModule, "assigner_first_views", "assigner_first_view_again", stdio.State(views.StatusState), handacts.Recorder{Ask: stdio.Ask}, now)...)
}
func keptSource(name string) views.Source { return views.KeptSource{State: stdio.State(name)} }
@@ -169,7 +174,7 @@ func (iv *indexerViews) start(ctx context.Context, w *world, wake func(), settle
}
})
}
stream := catalogue.StreamBusName(views.IndexerStream)
stream := catalogue.StreamBusName(indexerViewsStream)
if cons, err := iv.account.EventConsumer(ctx, iv.js, stream); err != nil {
iv.logf("the consumer %s on %s does not exist yet (%v): the indexer's manifests and modules are the first views' alone until the assigner starts again",
iv.account.EventConsumerName(), stream, err)
@@ -102,9 +102,9 @@ func TestManifest(t *testing.T) {
}
}
// ADR 0619 §2, ADR 0615 §5: its views family beside placement on its old streams section.
if len(m.Streams) != 2 || m.Streams[1].Name != views.AssignerStream || !slices.Equal(m.Streams[1].Events, []string{views.EventPattern}) ||
if len(m.Streams) != 2 || m.Streams[1].Name != "assigner-views" || !slices.Equal(m.Streams[1].Events, []string{views.EventPattern}) ||
m.Streams[1].MaxAgeDays != views.StreamDays || m.Streams[1].MaxMiB != views.StreamMiB || m.Streams[1].Retention != "" {
t.Errorf("streams = %+v, want placement and %s", m.Streams, views.AssignerStream)
t.Errorf("streams = %+v, want placement and assigner-views", m.Streams)
}
if m.Streams[0].Name != delivery.PlacementStream || m.Streams[0].MaxAgeDays != delivery.PlacementStreamDays ||
!slices.Equal(slices.Sorted(slices.Values(m.Streams[0].Events)), slices.Sorted(slices.Values(delivery.PlacementEvents))) {
@@ -17,6 +17,7 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/handacts"
"git.novox.be/novox/mesh-sdk/go/views"
"git.novox.be/novox/mesh-delivery/sdk/enginereads"
@@ -403,7 +404,7 @@ func main() {
tools = append(tools, enginereads.Tool(EngineReadsTool, "the builder", enginereads.KeptState(stdio.State("engine-reads")), true, time.Now))
// builder_first_views and builder_first_view_again: where the builder's first views of the indexer's and the assigner's
// states stand, and asking one again by hand (ADR 0615, DLV-M1).
tools = append(tools, views.StatusTools(delivery.BuilderModule, "builder_first_views", "builder_first_view_again", stdio.State(views.StatusState), time.Now)...)
tools = append(tools, views.StatusTools(delivery.BuilderModule, "builder_first_views", "builder_first_view_again", stdio.State(views.StatusState), handacts.Recorder{Ask: stdio.Ask}, time.Now)...)
if err := stdio.Serve("", tools); err != nil {
fmt.Fprintf(os.Stderr, "[mesh-delivery-builder-tools] %v\n", err)
os.Exit(1)
@@ -27,12 +27,12 @@ import (
runner "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
"git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry"
"git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry"
"git.novox.be/novox/mesh-delivery/sdk/firstview/firstviewtest"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/views/viewstest"
"mesh-delivery-builder/internal/build"
"mesh-delivery-builder/internal/versions"
@@ -291,9 +291,9 @@ func startBedWith(t *testing.T, closed bool) *bed {
// (ADR 0615, DLV-M1); n1 and n2 each have their durable consumer.
readers := []string{"n1_" + delivery.BuilderModule, "n2_" + delivery.BuilderModule}
for _, st := range []string{modulesState, manifestsState, candidatesState} {
firstviewtest.Serve(t, ctx, js, ctl, delivery.IndexerModule, st, readers...)
viewstest.Serve(t, ctx, js, ctl, delivery.IndexerModule, st, delivery.IndexerModule+".indexer_read_"+st, indexerViews, readers...)
}
firstviewtest.Serve(t, ctx, js, ctl, delivery.AssignerModule, delivery.PlacementState, readers...)
viewstest.Serve(t, ctx, js, ctl, delivery.AssignerModule, delivery.PlacementState, delivery.AssignerModule+".assigner_read_placement", assignerViews, readers...)
b.builds = mk(delivery.BuilderBuildsBucket, delivery.BuilderBuildsHistory)
b.instances = mk(instancesBucket, 1)
// The tag asks and their bounds (design 51 *Tagged versions*): the bounds declared key-ttl, so an ended bound leaves the
@@ -28,8 +28,6 @@ import (
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/views"
"git.novox.be/novox/mesh-delivery/sdk/firstview"
"mesh-delivery-builder/internal/build"
"mesh-delivery-builder/internal/versions"
)
@@ -75,14 +73,26 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
return err
}
defer calls.Close()
reader := &firstview.Reader{Module: delivery.BuilderModule, Call: calls.Call, Logf: logger.Printf}
reader := &views.ProcessReader{Module: delivery.BuilderModule, Call: calls.Call, Logf: logger.Printf}
findingsKV := process.AwaitBucket(ctx, js, conn, findingsBucket, "the builder's findings, a first view unanswered past its bound among them, are said in its log only", logger.Printf)
reader.Findings = findingsKV.KV
modules := reader.Follow(delivery.IndexerModule, modulesState)
manifests := reader.Follow(delivery.IndexerModule, manifestsState)
modules, err := reader.Follow(delivery.IndexerModule, modulesState, delivery.IndexerModule+".indexer_read_modules", indexerViews)
if err != nil {
return err
}
manifests, err := reader.Follow(delivery.IndexerModule, manifestsState, delivery.IndexerModule+".indexer_read_manifests", indexerViews)
if err != nil {
return err
}
// The candidates' fingerprints are built too (issue 1684).
candidates := reader.Follow(delivery.IndexerModule, candidatesState)
placement := reader.Follow(delivery.AssignerModule, delivery.PlacementState)
candidates, err := reader.Follow(delivery.IndexerModule, candidatesState, delivery.IndexerModule+".indexer_read_candidates", indexerViews)
if err != nil {
return err
}
placement, err := reader.Follow(delivery.AssignerModule, delivery.PlacementState, delivery.AssignerModule+".assigner_read_placement", assignerViews)
if err != nil {
return err
}
bound := defaultStepBound
if v := strings.TrimSpace(getenv("MESH_BUILDER_STEP_BOUND")); v != "" {
if bound, err = time.ParseDuration(v); err != nil {
@@ -414,8 +424,11 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// keeper raises a first view unanswered past its bound (ADR 0615 §6).
var firstViewsBucket = delivery.BuilderModule + "_" + views.StatusState
// The indexer's states the builder follows (delivery.ModulesBucket, ManifestsBucket, CandidatesBucket).
// The indexer's states the builder follows (delivery.ModulesBucket, ManifestsBucket, CandidatesBucket), and the owners'
// views families as their manifests' `streams` name them.
const (
indexerViews = "indexer-views"
assignerViews = "assigner-views"
modulesState = "modules"
manifestsState = "manifests"
candidatesState = "candidates"
@@ -423,8 +436,8 @@ const (
// buildFromModules builds from the indexer's modules and candidates once the first views of the modules, the manifests and
// the candidates are answered (never at the start: an indexer that is down holds no other part of it), and follows them.
func buildFromModules(ctx context.Context, b *build.Builder, modules, manifests, candidates *firstview.Picture) error {
for _, p := range []*firstview.Picture{modules, manifests, candidates} {
func buildFromModules(ctx context.Context, b *build.Builder, modules, manifests, candidates *views.Picture) error {
for _, p := range []*views.Picture{modules, manifests, candidates} {
select {
case <-ctx.Done():
return nil
@@ -119,7 +119,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
// Design 51 *The floor*: the floor's three sources, the registry seats' versions, bridge_tags and indexer_requirements.
if want := []string{"mesh-delivery-assigner.assigner_read_placement", "mesh-delivery-forge-bridge.bridge_tags", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests",
"mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_requirements", "seat:container-image-registry.remove", "seat:container-image-registry.versions",
"seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:node-engine.manifest-reads", "seat:npm-package-registry.remove",
"seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-hand-acts.hand-act", "seat:node-engine.manifest-reads", "seat:npm-package-registry.remove",
"seat:npm-package-registry.versions", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions"}; !reflect.DeepEqual(sorted(m.Invokes), want) || len(m.Config) != 0 {
t.Errorf("invokes = %v, config = %v, want %v and no config", m.Invokes, m.Config, want)
}
@@ -60,6 +60,7 @@
"seat:go-package-registry.remove",
"seat:go-package-registry.versions",
"seat:mesh-artifact-sweep.held",
"seat:mesh-hand-acts.hand-act",
"seat:node-engine.manifest-reads",
"seat:npm-package-registry.remove",
"seat:npm-package-registry.versions",
@@ -13,6 +13,7 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/handacts"
"git.novox.be/novox/mesh-sdk/go/views"
"mesh-delivery-checker/internal/check"
@@ -329,7 +330,7 @@ func main() {
tools := append(append(append(Tools(r), TryTools(trying)...), SeatTryTools(trying)...), RecoveryTools(r, written)...)
// checker_first_views and checker_first_view_again: where the checker's first views of the indexer's and the assigner's
// states stand, and asking one again by hand (ADR 0615, DLV-M2).
tools = append(tools, views.StatusTools(delivery.CheckerModule, "checker_first_views", "checker_first_view_again", stdio.State(views.StatusState), time.Now)...)
tools = append(tools, views.StatusTools(delivery.CheckerModule, "checker_first_views", "checker_first_view_again", stdio.State(views.StatusState), handacts.Recorder{Ask: stdio.Ask}, time.Now)...)
if err := stdio.Serve("", logged(tools)); err != nil {
fmt.Fprintf(os.Stderr, "[mesh-delivery-checker-tools] %v\n", err)
os.Exit(1)
@@ -29,10 +29,10 @@ import (
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-delivery/sdk/firstview/firstviewtest"
"git.novox.be/novox/mesh-sdk/go/announce"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/views/viewstest"
"mesh-delivery-checker/internal/check"
)
@@ -446,9 +446,9 @@ func startBed(t *testing.T) *bed {
// The indexer and the assigner as the owners of the states the checker follows: their read tools and views families
// (ADR 0615, DLV-M2).
for _, st := range []string{candidatesState, modulesState, manifestsState, delivery.PackagesState, delivery.IndexerPredefinedState, seatDefinitionsState} {
firstviewtest.Serve(t, ctx, js, ctl, delivery.IndexerModule, st, "n1_"+delivery.CheckerModule)
viewstest.Serve(t, ctx, js, ctl, delivery.IndexerModule, st, delivery.IndexerModule+".indexer_read_"+strings.ReplaceAll(st, "-", "_"), indexerViews, "n1_"+delivery.CheckerModule)
}
firstviewtest.Serve(t, ctx, js, ctl, delivery.AssignerModule, delivery.PlacementState, "n1_"+delivery.CheckerModule)
viewstest.Serve(t, ctx, js, ctl, delivery.AssignerModule, delivery.PlacementState, delivery.AssignerModule+".assigner_read_placement", assignerViews, "n1_"+delivery.CheckerModule)
// 1929/AC1: the indexer's value of the repository's module, with no TypeScript manifest, so the Go build module alone
// judges the repository (an unknown repository is judged by both, issue 1887 A12).
known, err := delivery.EncodeModule(delivery.Module{Repository: repo, Commit: trunk, Path: "modules/m", None2: "the fingerprint is computed with its chain"})
@@ -8,6 +8,7 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
@@ -26,8 +27,6 @@ import (
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/views"
"git.novox.be/novox/mesh-delivery/sdk/firstview"
"mesh-delivery-checker/internal/check"
)
@@ -77,8 +76,11 @@ const (
// seatDefinitionsState is the indexer's state of the seat definitions (design 26), keyed `<seat>.trunk` and
// `<seat>.pull.<escaped repository>.<number>`.
seatDefinitionsState = "seat-definitions"
modulesState = "modules"
manifestsState = "manifests"
// The owners' views families, as their manifests' `streams` name them.
indexerViews = "indexer-views"
assignerViews = "assigner-views"
modulesState = "modules"
manifestsState = "manifests"
)
// run is the program: it returns when ctx ends or the bus cannot be used.
@@ -139,16 +141,26 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// first view of their read tools and then their views families, never their buckets (ADR 0615 §1, ADR 0596, DLV-M2).
// An owner that is down never holds the start: the checks start once every first view is answered, and until then the
// unanswered first view is the checker's finding.
reader := &firstview.Reader{Module: delivery.CheckerModule, Call: caller.Call, Logf: logger.Printf}
reader := &views.ProcessReader{Module: delivery.CheckerModule, Call: caller.Call, Logf: logger.Printf}
reader.Findings = process.AwaitBucket(ctx, js, conn, findingsBucket, "a first view unanswered past its bound is said in the log alone", logger.Printf).KV
candidates := reader.Follow(delivery.IndexerModule, candidatesState)
modules := reader.Follow(delivery.IndexerModule, modulesState)
manifests := reader.Follow(delivery.IndexerModule, manifestsState)
packages := reader.Follow(delivery.IndexerModule, delivery.PackagesState)
predefined := reader.Follow(delivery.IndexerModule, delivery.IndexerPredefinedState)
placement := reader.Follow(delivery.AssignerModule, delivery.PlacementState)
// Each by the owner's read tool and views family as the owner's manifest names them.
var followed error
followOwner := func(owner, state, tool, stream string) *views.Picture {
p, err := reader.Follow(owner, state, owner+"."+tool, stream)
followed = errors.Join(followed, err)
return p
}
candidates := followOwner(delivery.IndexerModule, candidatesState, "indexer_read_candidates", indexerViews)
modules := followOwner(delivery.IndexerModule, modulesState, "indexer_read_modules", indexerViews)
manifests := followOwner(delivery.IndexerModule, manifestsState, "indexer_read_manifests", indexerViews)
packages := followOwner(delivery.IndexerModule, delivery.PackagesState, "indexer_read_packages", indexerViews)
predefined := followOwner(delivery.IndexerModule, delivery.IndexerPredefinedState, "indexer_read_predefined", indexerViews)
placement := followOwner(delivery.AssignerModule, delivery.PlacementState, "assigner_read_placement", assignerViews)
// The seventh: the indexer's seat definitions, so a pull request whose head's definition does not parse fails.
seatDefs := reader.Follow(delivery.IndexerModule, seatDefinitionsState)
seatDefs := followOwner(delivery.IndexerModule, seatDefinitionsState, "indexer_read_seat_definitions", indexerViews)
if followed != nil {
return followed
}
stepBound, err := durationEnv(getenv, "MESH_CHECKER_STEP_BOUND", check.DefaultStepBound)
if err != nil {
@@ -124,7 +124,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
"mesh-delivery-assigner.assigner_read_placement", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests",
"mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_read_predefined",
"mesh-delivery-indexer.indexer_read_seat_definitions",
"seat:mesh-config.renames", "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
"seat:mesh-config.renames", "seat:mesh-hand-acts.hand-act", "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
t.Errorf("invokes = %v, want the forge's reads and compose-candidate", m.Invokes)
}
if want := []string{"node-build-go", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) {
@@ -82,6 +82,7 @@
"mesh-delivery-indexer.indexer_read_predefined",
"mesh-delivery-indexer.indexer_read_seat_definitions",
"seat:mesh-config.renames",
"seat:mesh-hand-acts.hand-act",
"seat:node-engine.compose-candidate"
],
@@ -13,6 +13,7 @@ import (
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/handacts"
"git.novox.be/novox/mesh-sdk/go/views"
"mesh-delivery-teller/internal/ends"
@@ -106,7 +107,7 @@ func main() {
// teller_first_views and teller_first_view_again: where the teller's first view of the assigner's asks stands, and
// asking it again by hand (ADR 0615, design 25 § 12.2).
tools := append(Tools(askEnd(stdio.State(ends.State), 25*time.Second), time.Now),
views.StatusTools("mesh-delivery-teller", "teller_first_views", "teller_first_view_again", stdio.State(views.StatusState), time.Now)...)
views.StatusTools("mesh-delivery-teller", "teller_first_views", "teller_first_view_again", stdio.State(views.StatusState), handacts.Recorder{Ask: stdio.Ask}, time.Now)...)
if err := stdio.Serve("", tools); err != nil {
fmt.Fprintf(os.Stderr, "[mesh-delivery-teller-tools] %v\n", err)
os.Exit(1)
@@ -8,7 +8,7 @@ import (
"fmt"
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
"git.novox.be/novox/mesh-delivery/sdk/firstview/firstviewtest"
"git.novox.be/novox/mesh-sdk/go/views/viewstest"
"os"
"path/filepath"
"strings"
@@ -189,7 +189,7 @@ func fullConf(dir string) string {
teller = append(teller, readOf(viewAnswers)...) // issue 1297 B10: the manifest's reads
// The assigner's asks by its read tool and its views family (ADR 0615, DLV-M3), and the teller's own first-views and
// findings; the bucket itself is no longer the teller's to read.
teller = append(append(append(teller, firstviewtest.Grants(delivery.AssignerModule, "asks", tellerConsumer)...), keepsOf(firstViewsBucket)...), keepsOf(findingsBucket)...)
teller = append(append(append(teller, viewstest.Grants(delivery.AssignerModule, delivery.AssignerModule+".assigner_read_asks", "assigner-views", tellerConsumer)...), keepsOf(firstViewsBucket)...), keepsOf(findingsBucket)...)
view := append(append(append(readOf(tellerAnswers), readOf(gateAsksKV)...), readOf(assignerAsks)...), keepsOf(viewAnswers)...)
assigner := keepsOf(assignerAsks)
tools := append(keepsOf(gateAsksKV), readOf(tellerAnswers)...)
@@ -273,7 +273,7 @@ func startFull(t *testing.T) *fullBed {
t.Fatal(err)
}
// The assigner as the owner of its asks: its read tool and its views family (ADR 0615).
firstviewtest.Serve(t, ctx, js, ctl, delivery.AssignerModule, "asks", tellerConsumer)
viewstest.Serve(t, ctx, js, ctl, delivery.AssignerModule, "asks", delivery.AssignerModule+".assigner_read_asks", "assigner-views", tellerConsumer)
return &fullBed{srv: srv, ctl: ctl, js: js, ctx: ctx}
}
@@ -24,8 +24,6 @@ import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/sdk/firstview"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-teller/internal/ends"
@@ -94,10 +92,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
return fmt.Errorf("the teller's connection for its tool calls: %w", err)
}
defer calls.Close()
reader := &firstview.Reader{Module: delivery.TellerModule, Call: calls.Call, Logf: logger.Printf}
reader := &views.ProcessReader{Module: delivery.TellerModule, Call: calls.Call, Logf: logger.Printf}
findings := process.AwaitBucket(ctx, js, conn, findingsBucket, "a first view unanswered past its bound is said in the log alone", logger.Printf)
reader.Findings = findings.KV
assignerKV := jetstream.KeyValue(reader.Follow(delivery.AssignerModule, assignerAsksState))
asks, err := reader.Follow(delivery.AssignerModule, assignerAsksState, delivery.AssignerModule+".assigner_read_asks", "assigner-views")
if err != nil {
return err
}
assignerKV := jetstream.KeyValue(asks)
reader.Start(ctx, js, cred, process.AwaitBucket(ctx, js, conn, firstViewsBucket, "teller_first_views reads nothing and teller_first_view_again is not heard", logger.Printf))
viewKV := process.OptionalBucket(ctx, js, delivery.ViewAnswersBucket, teller, logger.Printf)
// The assigner's asks are raised on the operator channel whether or not the view's answers can be read (issue 1906).
@@ -96,7 +96,8 @@ func TestManifestDeclarations(t *testing.T) { // 1240/AC1
t.Errorf("reads %s: the indexer's and the assigner's state is read by their tools and views families alone", r)
}
}
if want := []string{"mesh-delivery-assigner.assigner_read_asks"}; !reflect.DeepEqual(m.Invokes, want) {
// The join: its first view asked again by hand is a hand act, recorded through the seat (views.StatusTools).
if want := []string{"mesh-delivery-assigner.assigner_read_asks", "seat:mesh-hand-acts.hand-act"}; !reflect.DeepEqual(m.Invokes, want) {
t.Errorf("invokes = %v, want %v", m.Invokes, want)
}
if want := []string{"mesh-delivery-assigner.views.asks"}; !reflect.DeepEqual(m.Consumes, want) {
+2 -1
View File
@@ -8,7 +8,8 @@
"teller_first_view_again"
],
"invokes": [
"mesh-delivery-assigner.assigner_read_asks"
"mesh-delivery-assigner.assigner_read_asks",
"seat:mesh-hand-acts.hand-act"
],
"consumes": [
"mesh-delivery-assigner.views.asks"
@@ -13,9 +13,8 @@ import (
"mesh-delivery-view/internal/view"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/handacts"
"git.novox.be/novox/mesh-delivery/sdk/firstview"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-sdk/go/views"
@@ -394,14 +393,16 @@ func read(state stdio.KeptState) view.StateRead {
// by the bundle on each call (ADR 0615 §1, design 25 § 12.2: the view keeps its tools and answers them by asking the
// owners), never their buckets.
func ownersRead(ask func(key string, body any) (json.RawMessage, error)) view.Reader {
of := func(owner, state string) view.StateRead { return view.OwnerRead(ask, firstview.ReadTool(owner, state)) }
return view.Reader{State: of(delivery.IndexerModule, "modules"), CandidatesState: of(delivery.IndexerModule, "candidates"),
WantsState: of(delivery.AssignerModule, "wants"), DifferencesState: of(delivery.AssignerModule, "differences"),
HeardState: of(delivery.IndexerModule, "heard"),
PredefinedState: of(delivery.IndexerModule, delivery.IndexerPredefinedState),
ConfigCandidatesState: of(delivery.IndexerModule, delivery.IndexerConfigCandidatesState),
PackagesState: of(delivery.IndexerModule, delivery.PackagesState),
GraphState: of(delivery.IndexerModule, graph.State)}
// Each owner's read tool as the view's invokes names it.
indexer := func(tool string) view.StateRead { return view.OwnerRead(ask, delivery.IndexerModule+"."+tool) }
assigner := func(tool string) view.StateRead { return view.OwnerRead(ask, delivery.AssignerModule+"."+tool) }
return view.Reader{State: indexer("indexer_read_modules"), CandidatesState: indexer("indexer_read_candidates"),
WantsState: assigner("assigner_read_wants"), DifferencesState: assigner("assigner_read_differences"),
HeardState: indexer("indexer_read_heard"),
PredefinedState: indexer("indexer_read_predefined"),
ConfigCandidatesState: indexer("indexer_read_config_candidates"),
PackagesState: indexer("indexer_read_packages"),
GraphState: indexer("indexer_read_graph")}
}
func main() {
@@ -423,7 +424,7 @@ func main() {
}
// view_first_views and view_first_view_again: where the view's process stands with its first views of the indexer's and
// the assigner's states (ADR 0615 §6), and a re-read by hand.
tools := append(Tools(r, u), views.StatusTools(delivery.ViewModule, "view_first_views", "view_first_view_again", stdio.State(views.StatusState), time.Now)...)
tools := append(Tools(r, u), views.StatusTools(delivery.ViewModule, "view_first_views", "view_first_view_again", stdio.State(views.StatusState), handacts.Recorder{Ask: stdio.Ask}, time.Now)...)
if err := stdio.Serve("", tools); err != nil {
fmt.Fprintf(os.Stderr, "[mesh-delivery-view-tools] %v\n", err)
os.Exit(1)
@@ -11,7 +11,6 @@ import (
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/views"
"git.novox.be/novox/mesh-delivery/sdk/firstview"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"mesh-delivery-view/internal/view"
@@ -68,7 +67,8 @@ func ownersAnswerTheToolsAsBefore(t *testing.T) {
tools := map[string]string{}
for name := range states {
owner, state, _ := strings.Cut(name, ".")
tools[firstview.ReadTool(owner, state)] = name
short := map[string]string{delivery.IndexerModule: "indexer_read_", delivery.AssignerModule: "assigner_read_"}[owner]
tools[owner+"."+short+strings.ReplaceAll(state, "-", "_")] = name
}
asked := map[string]int{}
ask := func(key string, body any) (json.RawMessage, error) {
@@ -21,7 +21,7 @@ import (
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-delivery/sdk/firstview/firstviewtest"
"git.novox.be/novox/mesh-sdk/go/views/viewstest"
"mesh-delivery-view/internal/view"
)
@@ -31,9 +31,8 @@ import (
// A bucket the test made is kept; the others are made empty.
func serveOwners(t *testing.T, ctx context.Context, js jetstream.JetStream, nc *nats.Conn) {
t.Helper()
for _, bucket := range ownersBuckets {
owner, state, _ := strings.Cut(bucket, "_")
firstviewtest.Serve(t, ctx, js, nc, owner, state, "n_"+delivery.ViewModule)
for _, o := range ownersFollowed {
viewstest.Serve(t, ctx, js, nc, o.owner, o.state, o.tool, o.stream, "n_"+delivery.ViewModule)
}
}
@@ -27,8 +27,6 @@ import (
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/views"
"git.novox.be/novox/mesh-delivery/sdk/firstview"
"mesh-delivery-view/internal/view"
)
@@ -118,11 +116,12 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
return err
}
defer calls.Close()
reader := &firstview.Reader{Module: delivery.ViewModule, Call: calls.Call, Logf: logger.Printf}
reader := &views.ProcessReader{Module: delivery.ViewModule, Call: calls.Call, Logf: logger.Printf}
reader.Findings = process.AwaitBucket(ctx, js, conn, findingsBucket, "a first view unanswered past its bound is said in the log alone", logger.Printf).KV
for _, bucket := range ownersBuckets {
owner, state, _ := strings.Cut(bucket, "_")
reader.Follow(owner, state)
for _, o := range ownersFollowed {
if _, err := reader.Follow(o.owner, o.state, o.tool, o.stream); err != nil {
return err
}
}
open := buckets{js: js, reader: reader}
for _, bucket := range []string{delivery.HaveBucket, delivery.PlacementBucket, delivery.PresenceBucket, delivery.StuckBucket, delivery.CandidatesBucket, delivery.ModulesBucket, delivery.AsksBucket, delivery.AnswersBucket, delivery.BuilderBuildsBucket, delivery.AssignerWantsBucket, delivery.SourceBucket, delivery.AssignerAsksBucket, delivery.ViewAnswersBucket, delivery.TakenBucket} {
@@ -180,15 +179,22 @@ var (
findingsBucket = delivery.ViewModule + "_findings"
)
// ownersBuckets are the indexer's and the assigner's states the view's process follows by their first views
// (`<owner>_<state>`); the view's tools ask the other six through the owners' read tools on each call.
var ownersBuckets = []string{delivery.PlacementBucket, delivery.AssignerWantsBucket, delivery.AssignerAsksBucket,
delivery.CandidatesBucket, delivery.ModulesBucket, delivery.TakenBucket}
// ownersFollowed are the indexer's and the assigner's states the view's process follows by their first views, each by its
// owner's read tool (as the view's invokes names it) and views family (as the owner's manifest's `streams` names it); the
// view's tools ask the other six through the owners' read tools on each call.
var ownersFollowed = []struct{ owner, state, tool, stream string }{
{delivery.AssignerModule, delivery.PlacementState, delivery.AssignerModule + ".assigner_read_placement", "assigner-views"},
{delivery.AssignerModule, "wants", delivery.AssignerModule + ".assigner_read_wants", "assigner-views"},
{delivery.AssignerModule, "asks", delivery.AssignerModule + ".assigner_read_asks", "assigner-views"},
{delivery.IndexerModule, "candidates", delivery.IndexerModule + ".indexer_read_candidates", "indexer-views"},
{delivery.IndexerModule, "modules", delivery.IndexerModule + ".indexer_read_modules", "indexer-views"},
{delivery.IndexerModule, "taken", delivery.IndexerModule + ".indexer_read_taken", "indexer-views"},
}
// buckets opens what the view watches: an owner's state as its picture (firstview), any other bucket on the bus.
type buckets struct {
js jetstream.JetStream
reader *firstview.Reader
reader *views.ProcessReader
}
func (b buckets) KeyValue(ctx context.Context, bucket string) (jetstream.KeyValue, error) {
@@ -106,6 +106,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
"mesh-delivery-assigner.assigner_read_wants", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_config_candidates",
"mesh-delivery-indexer.indexer_read_graph", "mesh-delivery-indexer.indexer_read_heard", "mesh-delivery-indexer.indexer_read_modules",
"mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_read_predefined", "mesh-delivery-indexer.indexer_read_taken",
"seat:mesh-hand-acts.hand-act", // the join: its first view asked again by hand is a hand act (views.StatusTools)
"seat:node-engine.manifest-reads"}; !reflect.DeepEqual(m.Invokes, want) { // ADR 0618 §3: node_manifest_reads asks the node-engine seat's verb of every node
t.Errorf("invokes = %v, want %v", m.Invokes, want)
}
+1
View File
@@ -62,6 +62,7 @@
"mesh-delivery-indexer.indexer_read_packages",
"mesh-delivery-indexer.indexer_read_predefined",
"mesh-delivery-indexer.indexer_read_taken",
"seat:mesh-hand-acts.hand-act",
"seat:node-engine.manifest-reads"
],
"reads": [
@@ -13,6 +13,7 @@ import (
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/handacts"
"git.novox.be/novox/mesh-sdk/go/views"
"mesh-delivery-watchman/internal/socket"
@@ -163,7 +164,7 @@ func Tools(states func(string) state, ask func(socket.Ask) (json.RawMessage, err
Description: "The watchman's findings (novox/hq ADR 0564 §2): each thing it observes wrong now, <scope>.<id>.<kind>, with its " +
"severity, subject, evidence and the row that observed it; the condition keeper raises and clears the conditions from " +
"them. Give key for one, prefix to narrow. Read-only."},
}, views.StatusTools(watch.Observer, ToolFirstViews, ToolFirstViewAgain, stdio.State(views.StatusState), time.Now)...)
}, views.StatusTools(watch.Observer, ToolFirstViews, ToolFirstViewAgain, stdio.State(views.StatusState), handacts.Recorder{Ask: stdio.Ask}, time.Now)...)
}
func main() {
@@ -17,10 +17,10 @@ import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/sdk/firstview/firstviewtest"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/views/viewstest"
"mesh-delivery-watchman/internal/deadletters"
"mesh-delivery-watchman/internal/socket"
@@ -62,8 +62,8 @@ accounts {
// families (DLV-M5, ADR 0615): the watchman (n1) follows both, never their buckets.
func serveOwners(t *testing.T, ctx context.Context, js jetstream.JetStream, nc *nats.Conn) {
t.Helper()
firstviewtest.Serve(t, ctx, js, nc, delivery.AssignerModule, wantsState, "n1_"+delivery.WatchmanModule)
firstviewtest.Serve(t, ctx, js, nc, delivery.IndexerModule, modulesState, "n1_"+delivery.WatchmanModule)
viewstest.Serve(t, ctx, js, nc, delivery.AssignerModule, wantsState, delivery.AssignerModule+".assigner_read_wants", assignerViews, "n1_"+delivery.WatchmanModule)
viewstest.Serve(t, ctx, js, nc, delivery.IndexerModule, modulesState, delivery.IndexerModule+".indexer_read_modules", indexerViews, "n1_"+delivery.WatchmanModule)
}
// TestWatchmanWhole starts the program the way the node does (its entry, run, with its environment and a broker file)
@@ -20,8 +20,6 @@ import (
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/views"
"git.novox.be/novox/mesh-delivery/sdk/firstview"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
@@ -128,7 +126,7 @@ func settle(ctx context.Context, kv jetstream.KeyValue, each func(jetstream.KeyV
// readLate reads an owner's state the watchman follows by its first view (pic), on a goroutine of its own once that is
// answered, so an owner that is down never holds the start: every key it holds now, then the start's end for its bucket
// (watch.ReadLate), then each change until ctx ends.
func readLate(ctx context.Context, w *watch.Watchman, pic *firstview.Picture, bucket string, each func(jetstream.KeyValueEntry)) error {
func readLate(ctx context.Context, w *watch.Watchman, pic *views.Picture, bucket string, each func(jetstream.KeyValueEntry)) error {
select {
case <-ctx.Done():
return nil
@@ -188,6 +186,9 @@ var firstViewsBucket = delivery.WatchmanModule + "_" + views.StatusState
const (
wantsState = "wants"
modulesState = "modules"
// The owners' views families, as their manifests' `streams` name them.
indexerViews = "indexer-views"
assignerViews = "assigner-views"
)
// run is the program: it returns when ctx ends or the bus cannot be used.
@@ -232,10 +233,16 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
return fmt.Errorf("the watchman's connection for its tool calls: %w", err)
}
defer calls.Close()
reader := &firstview.Reader{Module: delivery.WatchmanModule, Call: calls.Call, Logf: logger.Printf,
reader := &views.ProcessReader{Module: delivery.WatchmanModule, Call: calls.Call, Logf: logger.Printf,
Findings: func() jetstream.KeyValue { return kvs.findings }}
wants := reader.Follow(delivery.AssignerModule, wantsState)
modules := reader.Follow(delivery.IndexerModule, modulesState)
wants, err := reader.Follow(delivery.AssignerModule, wantsState, delivery.AssignerModule+".assigner_read_wants", assignerViews)
if err != nil {
return err
}
modules, err := reader.Follow(delivery.IndexerModule, modulesState, delivery.IndexerModule+".indexer_read_modules", indexerViews)
if err != nil {
return err
}
w := watch.New(&kvOut{ctx: ctx, presence: kvs.presence, pulse: kvs.pulse, stuk: kvs.stuck, findings: kvs.findings}, time.Now)
w.Logf(logger.Printf)
@@ -2,6 +2,7 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
@@ -9,7 +10,9 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/views"
"mesh-delivery-watchman/internal/watch"
@@ -75,6 +78,34 @@ func TestRunNeedsItsCredential(t *testing.T) { // 1135/AC6
if err == nil || !strings.Contains(err.Error(), "MESH_BROKER_FILE") {
t.Fatalf("run = %v, want a refusal naming MESH_BROKER_FILE", err)
}
t.Run("A2j: a want stood since the assigner's bus wrote it, through the first view", func(t *testing.T) {
// The watchman's want wait starts at the entry's time (createdOr): through mesh-sdk's views.ProcessReader it is
// the time the owner's bus wrote the key, never the watchman's start.
written := time.Date(2026, 11, 4, 9, 0, 0, 0, time.UTC)
r := &views.ProcessReader{Module: delivery.WatchmanModule, Call: func(string, any) (json.RawMessage, error) {
return json.Marshal(views.Answer{V: views.Version, Owner: delivery.AssignerModule, State: wantsState, Revision: 3, Created: written,
At: written.Add(time.Hour), Values: map[string]json.RawMessage{"ace.mail": json.RawMessage(`{}`)}, Written: map[string]time.Time{"ace.mail": written}})
}}
p, err := r.Follow(delivery.AssignerModule, wantsState, delivery.AssignerModule+".assigner_read_wants", assignerViews)
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
r.Ask(ctx, nil)
select {
case <-r.Ready():
case <-ctx.Done():
t.Fatal("the first view was not taken")
}
e, err := p.Get(ctx, "ace.mail")
if err != nil {
t.Fatal(err)
}
if got := createdOr(e, time.Now()); !got.Equal(written) {
t.Errorf("the want stands since %v, want %v, when the assigner's bus wrote it", got, written)
}
})
}
// refusingBus refuses every bucket with err (issue 1945).
@@ -103,7 +103,8 @@ func TestManifestStateAndReads(t *testing.T) { // 1135/AC6
t.Errorf("reads %s: the indexer's and the assigner's state is read by their tools and views families alone", r)
}
}
if want := []string{"mesh-delivery-assigner.assigner_read_wants", "mesh-delivery-indexer.indexer_read_modules"}; !reflect.DeepEqual(m.Invokes, want) {
// The join: its first view asked again by hand is a hand act, recorded through the seat (views.StatusTools).
if want := []string{"mesh-delivery-assigner.assigner_read_wants", "mesh-delivery-indexer.indexer_read_modules", "seat:mesh-hand-acts.hand-act"}; !reflect.DeepEqual(m.Invokes, want) {
t.Errorf("invokes = %v, want %v", m.Invokes, want)
}
// DLV-M5 its first views, and DLV-W1 (ADR 0595 §4): the accepted verdicts and the bus module's created and refused, durably.
+2 -1
View File
@@ -26,7 +26,8 @@
],
"invokes": [
"mesh-delivery-assigner.assigner_read_wants",
"mesh-delivery-indexer.indexer_read_modules"
"mesh-delivery-indexer.indexer_read_modules",
"seat:mesh-hand-acts.hand-act"
],
"consumes": [
"mesh-delivery-assigner.views.wants",