From 06ebd7e8cf49addbc0d89649117d352e5fd5c85a Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 22:03:09 +0100 Subject: [PATCH] G findings: the writers name the ask's subject from the contract; the readers take the writer whole MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WriterConfig.Asked is the generated mesh-conditions contract's SubjectFindingsAsked (A-17), a reader's Findings is the module's writer itself (A-22, A-26), and mesh-build-go's single manifest ask writes and clears its first-view-unanswered finding through the writer with the contract's Finding (A-28). mesh-config and mesh-vault's own followers (A-28) are not yet moved onto a views.Reader: they do not compile at mesh-sdk 24f876a. Builds: design 47 G §687, §689, A-17, A-22, A-26, A-28 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- .../cmd/artifact-store-tools/main.go | 4 +- modules/claude-code/cmd/claude-code/main.go | 4 +- .../cmd/distribution-tools/main.go | 2 +- modules/distribution/cmd/distribution/main.go | 2 +- modules/gitea/cmd/package-registry/main.go | 2 +- .../cmd/mesh-build-go/compose.go | 39 ++++++++++++++----- .../cmd/mesh-build-go/compose_test.go | 12 ++++-- .../mesh-build-go/cmd/mesh-build-go/main.go | 3 +- modules/mesh-config/cmd/mesh-config/main.go | 4 +- .../cmd/mesh-provisioner/main.go | 4 +- modules/mesh-vault/cmd/mesh-vault/main.go | 4 +- modules/mesh-watcher/cmd/mesh-watcher/main.go | 4 +- modules/minio/cmd/object-store/main.go | 4 +- modules/nfs-server/cmd/nfs-server/main.go | 4 +- 14 files changed, 59 insertions(+), 33 deletions(-) diff --git a/modules/artifact-store-tools/cmd/artifact-store-tools/main.go b/modules/artifact-store-tools/cmd/artifact-store-tools/main.go index 611ef1130..14394b18b 100644 --- a/modules/artifact-store-tools/cmd/artifact-store-tools/main.go +++ b/modules/artifact-store-tools/cmd/artifact-store-tools/main.go @@ -32,13 +32,13 @@ func main() { h.Acts = meshhandactsclient.Recorder{Ask: stdio.Ask} // The module's one writer (design 47 G §689, A-16): the holder's findings and the reader's go through it. w, err := views.NewStdioWriter(views.WriterConfig[meshconditions.Finding, meshconditions.FindingCleared, meshconditions.FindingsStated, meshconditions.FindingsAskedEvent]{ - Module: Module, State: stdio.State("findings"), Kinds: []string{ImageMissing, SweepRefused}, Observe: h.FindingsObserved}) + Asked: meshconditions.SubjectFindingsAsked, Module: Module, State: stdio.State("findings"), Kinds: []string{ImageMissing, SweepRefused}, Observe: h.FindingsObserved}) if err != nil { logf("%v", err) os.Exit(1) } h.Findings = w - v.Findings = views.WriterFindings{Writer: w} + v.Findings = w // the runtime answers the writer's subscription only once Serve has its channel go func() { <-stdio.Served() diff --git a/modules/claude-code/cmd/claude-code/main.go b/modules/claude-code/cmd/claude-code/main.go index 2ed3b5b03..df34d25ac 100644 --- a/modules/claude-code/cmd/claude-code/main.go +++ b/modules/claude-code/cmd/claude-code/main.go @@ -234,7 +234,7 @@ var meshViews *views.Reader // observes none of its own. func newFindingsWriter() (*views.Writer[condseat.Finding, condseat.FindingCleared, condseat.FindingsStated, condseat.FindingsAskedEvent], error) { return views.NewStdioWriter(views.WriterConfig[condseat.Finding, condseat.FindingCleared, condseat.FindingsStated, condseat.FindingsAskedEvent]{ - Module: "claude-code", State: stdio.State("findings"), + Asked: condseat.SubjectFindingsAsked, Module: "claude-code", State: stdio.State("findings"), Observe: func(context.Context) (views.Observed[condseat.Finding], error) { return views.Observed[condseat.Finding]{}, nil }}) @@ -503,7 +503,7 @@ func main() { say("%v", err) os.Exit(1) } - meshViews.Findings = views.WriterFindings{Writer: findings} + meshViews.Findings = findings // the runtime answers the writer's subscription only once Serve has its channel go func() { <-stdio.Served() diff --git a/modules/distribution/cmd/distribution-tools/main.go b/modules/distribution/cmd/distribution-tools/main.go index e43ca6b53..f99403064 100644 --- a/modules/distribution/cmd/distribution-tools/main.go +++ b/modules/distribution/cmd/distribution-tools/main.go @@ -508,7 +508,7 @@ func main() { // The tools bundle's findings go through the module's writer (design 47 G §689, A-16); it owns the transfers' kinds, the // program's writer the driver's. w, err := views.NewStdioWriter(views.WriterConfig[meshconditions.Finding, meshconditions.FindingCleared, meshconditions.FindingsStated, meshconditions.FindingsAskedEvent]{ - Module: holder.Self, State: stdio.State("findings"), Kinds: holder.TransferFindingKinds, Observe: findings.Observed, Logf: logf}) + Asked: meshconditions.SubjectFindingsAsked, Module: holder.Self, State: stdio.State("findings"), Kinds: holder.TransferFindingKinds, Observe: findings.Observed, Logf: logf}) if err != nil { logf("%v", err) os.Exit(1) diff --git a/modules/distribution/cmd/distribution/main.go b/modules/distribution/cmd/distribution/main.go index 4cedc92d9..42e2aabc8 100644 --- a/modules/distribution/cmd/distribution/main.go +++ b/modules/distribution/cmd/distribution/main.go @@ -68,7 +68,7 @@ func run(ctx context.Context, getenv func(string) string) error { // bundle's writer the transfers'. findings := &holder.Findings{Logf: logger.Printf} w, err := views.NewWriter(views.WriterConfig[meshconditions.Finding, meshconditions.FindingCleared, meshconditions.FindingsStated, meshconditions.FindingsAskedEvent]{ - Module: holder.Self, Node: cred.Node, State: calls.State("findings"), Kinds: holder.DriverFindingKinds, + Asked: meshconditions.SubjectFindingsAsked, Module: holder.Self, Node: cred.Node, State: calls.State("findings"), Kinds: holder.DriverFindingKinds, JS: js, Conn: conn, Observe: findings.Observed, Logf: logger.Printf}) if err != nil { return err diff --git a/modules/gitea/cmd/package-registry/main.go b/modules/gitea/cmd/package-registry/main.go index ebcbed3ab..e07e52e3c 100644 --- a/modules/gitea/cmd/package-registry/main.go +++ b/modules/gitea/cmd/package-registry/main.go @@ -31,7 +31,7 @@ var findings = &Findings{Logf: func(f string, a ...any) { fmt.Fprintf(os.Stderr, func main() { // The bundle's findings go through the module's writer (design 47 G A-16). w, err := views.NewStdioWriter(views.WriterConfig[meshconditions.Finding, meshconditions.FindingCleared, meshconditions.FindingsStated, meshconditions.FindingsAskedEvent]{ - Module: packageseats.Self, State: stdio.State("findings"), Kinds: findingKinds, Observe: findings.Observed}) + Asked: meshconditions.SubjectFindingsAsked, Module: packageseats.Self, State: stdio.State("findings"), Kinds: findingKinds, Observe: findings.Observed}) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) diff --git a/modules/mesh-build-go/cmd/mesh-build-go/compose.go b/modules/mesh-build-go/cmd/mesh-build-go/compose.go index bae34801d..35afca719 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/compose.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/compose.go @@ -30,6 +30,7 @@ import ( sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/composer" stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/keys" "git.novox.be/novox/mesh-sdk/go/views" ) @@ -37,6 +38,9 @@ import ( // module invokes it, module.json invokes). A manifest by its digest never changes and a compose ask is rare, so the // module asks for the one key it needs when it needs it, and keeps no picture of the indexer's whole state (ADR 0615). // indexerModule is the module that holds the seat mesh-catalog, whose manifests state a compose ask reads. +// findingsModule is the module whose findings these are. +const findingsModule = "mesh-build-go" + const indexerModule = "mesh-delivery-indexer" const ManifestsTool = indexerModule + ".indexer_read_manifests" @@ -88,31 +92,48 @@ func askManifest(key string) (json.RawMessage, error) { // manifestFindings is where askManifest writes its finding: the module's writer once main has started it (design 47 G §689, // A-16); outside a launch by the runtime, which has no node and no state, nothing is written. -var manifestFindings views.FindingWriter = noFindings{} +var manifestFindings findingWriter = noFindings{} + +// findingWriter is the module's writer (views.Writer) as askManifestVia writes and clears through it. +type findingWriter interface { + Write(ctx context.Context, finding seat.Finding) error + Clear(ctx context.Context, scope, id, kind, why string) error +} type noFindings struct{} -func (noFindings) Put(views.Finding) error { return nil } -func (noFindings) Delete(views.Finding) error { return nil } +func (noFindings) Write(context.Context, seat.Finding) error { return nil } +func (noFindings) Clear(context.Context, string, string, string, string) error { return nil } + +// unansweredManifest is the finding of an ask the indexer did not answer, laid out as mesh-sdk lays out a views reader's +// first-view-unanswered (design 47 G §687, A-28): the id is the view's name, escaped as a key's name is. +func unansweredManifest(err string, asked, at time.Time) seat.Finding { + id := keys.EscapeName(findingsModule + "." + indexerModule + ".manifests") + return seat.Finding{Kind: views.UnansweredKind, Scope: "module", ID: id, Severity: seat.FindingSeverityWarning, Source: "views", + Subject: seat.Subject{Scope: "module", ID: findingsModule, Machine: os.Getenv("MESH_NODE")}, + Summary: fmt.Sprintf("%s has no first view of %s's manifests: %s has not answered since %s (%s)", findingsModule, indexerModule, ManifestsTool, + asked.Format(time.RFC3339), strings.TrimSpace(err)), + Headline: fmt.Sprintf("%s waits for %s", findingsModule, indexerModule), At: at.UTC()} +} // newFindingsWriter is this module's finding writer: it states the finding of askManifest and observes none of its own. func newFindingsWriter() (*views.Writer[seat.Finding, seat.FindingCleared, seat.FindingsStated, seat.FindingsAskedEvent], error) { return views.NewStdioWriter(views.WriterConfig[seat.Finding, seat.FindingCleared, seat.FindingsStated, seat.FindingsAskedEvent]{ - Module: "mesh-build-go", State: stdio.State("findings"), + Asked: seat.SubjectFindingsAsked, Module: findingsModule, State: stdio.State("findings"), Observe: func(context.Context) (views.Observed[seat.Finding], error) { return views.Observed[seat.Finding]{}, nil }}) } -func askManifestVia(ask func(string, any) (json.RawMessage, error), findings views.FindingWriter, now func() time.Time, key string) (json.RawMessage, error) { - st := views.Status{Owner: indexerModule, Tool: ManifestsTool, State: "manifests", Asked: now().UTC()} +func askManifestVia(ask func(string, any) (json.RawMessage, error), findings findingWriter, now func() time.Time, key string) (json.RawMessage, error) { + asked := now().UTC() raw, err := ask(ManifestsTool, map[string]any{"key": key}) + f := unansweredManifest("", asked, now()) if err != nil { - st.Error = err.Error() - _ = findings.Put(views.UnansweredFinding("mesh-build-go", st, now())) + _ = findings.Write(context.Background(), unansweredManifest(err.Error(), asked, now())) return nil, err } - _ = findings.Delete(views.UnansweredFinding("mesh-build-go", st, now())) + _ = findings.Clear(context.Background(), f.Scope, f.ID, f.Kind, "the indexer answered") var a struct { Found bool `json:"found"` Value json.RawMessage `json:"value"` diff --git a/modules/mesh-build-go/cmd/mesh-build-go/compose_test.go b/modules/mesh-build-go/cmd/mesh-build-go/compose_test.go index 722b5abc7..332f60d72 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/compose_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/compose_test.go @@ -11,12 +11,12 @@ import ( "testing" "time" + seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" catalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" meshcat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/composer" "git.novox.be/novox/mesh-sdk/go/forgekey" - "git.novox.be/novox/mesh-sdk/go/views" ) // nodeEngineComposer is the composer command of the node-engine's repository, whose Go module is at its root (ADR 0593 §7): @@ -187,8 +187,14 @@ func TestAnIndexerThatDoesNotAnswerIsAFinding(t *testing.T) { type mapFindings map[string][]byte -func (m mapFindings) Put(f views.Finding) error { m[f.ID+"."+f.Kind] = []byte(f.Summary); return nil } -func (m mapFindings) Delete(f views.Finding) error { delete(m, f.ID+"."+f.Kind); return nil } +func (m mapFindings) Write(_ context.Context, f seat.Finding) error { + m[f.ID+"."+f.Kind] = []byte(f.Summary) + return nil +} +func (m mapFindings) Clear(_ context.Context, _, id, kind, _ string) error { + delete(m, id+"."+kind) + return nil +} // ADR 0593 §7 (C9): a compose ask naming the node-engine's repository builds its composer command in the Go module at the // repository's root, under a key of that repository, commit and package, and runs the program over the request with its diff --git a/modules/mesh-build-go/cmd/mesh-build-go/main.go b/modules/mesh-build-go/cmd/mesh-build-go/main.go index a571e8538..8817fc799 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/main.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/main.go @@ -17,7 +17,6 @@ import ( sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/busstate" - "git.novox.be/novox/mesh-sdk/go/views" ) // The directories the module's manifest places outside its home (ADR 0466 §1): the workspaces, and the Go module @@ -77,7 +76,7 @@ func main() { logf("the finding writer: %v", err) os.Exit(1) } - manifestFindings = views.WriterFindings{Writer: findings} + manifestFindings = findings // the runtime answers the writer's subscription only once Serve has its channel go func() { <-stdio.Served() diff --git a/modules/mesh-config/cmd/mesh-config/main.go b/modules/mesh-config/cmd/mesh-config/main.go index e45094a12..44d1b28d0 100644 --- a/modules/mesh-config/cmd/mesh-config/main.go +++ b/modules/mesh-config/cmd/mesh-config/main.go @@ -123,7 +123,7 @@ func main() { Delivery: toolDelivery{ask: stdio.Ask}, Vault: toolVault{ask: stdio.Ask, recorder: recorder}} // The module's one writer (design 47 G §689, A-16): the renames' and retirements' findings and the views reader's go through it. w, err := views.NewStdioWriter(views.WriterConfig[meshconditions.Finding, meshconditions.FindingCleared, meshconditions.FindingsStated, meshconditions.FindingsAskedEvent]{ - Module: Module, State: stdio.State(FindingsState), Observe: c.FindingsObserved, + Asked: meshconditions.SubjectFindingsAsked, Module: Module, State: stdio.State(FindingsState), Observe: c.FindingsObserved, Kinds: []string{FindingRenameRefused, FindingWindowHeld, FindingRetireUnanswered, FindingRetireHeld}}) if err != nil { logf("%v", err) @@ -136,7 +136,7 @@ func main() { case moved <- struct{}{}: default: } - }, views.WriterFindings{Writer: w}) + }, w) // the runtime answers the writer's subscription only once Serve has its channel go func() { <-stdio.Served() diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/main.go b/modules/mesh-provisioner/cmd/mesh-provisioner/main.go index 8ffad9c06..83f513caf 100644 --- a/modules/mesh-provisioner/cmd/mesh-provisioner/main.go +++ b/modules/mesh-provisioner/cmd/mesh-provisioner/main.go @@ -219,7 +219,7 @@ func watchedOver(v *views.Reader) func(string, func(stdio.StateChange) error) er // observes none of its own. func newFindingsWriter() (*views.Writer[seat.Finding, seat.FindingCleared, seat.FindingsStated, seat.FindingsAskedEvent], error) { return views.NewStdioWriter(views.WriterConfig[seat.Finding, seat.FindingCleared, seat.FindingsStated, seat.FindingsAskedEvent]{ - Module: "mesh-provisioner", State: stdio.State("findings"), + Asked: seat.SubjectFindingsAsked, Module: "mesh-provisioner", State: stdio.State("findings"), Observe: func(context.Context) (views.Observed[seat.Finding], error) { return views.Observed[seat.Finding]{}, nil }}) @@ -233,7 +233,7 @@ func main() { logf("%v", err) os.Exit(1) } - v.Findings = views.WriterFindings{Writer: findings} + v.Findings = findings // One copy of what it reads, each state by its watch (ADR 0580 clause 2); the indexer's and the assigner's by their // pictures, a manifest by its digest from the picture too. ws := stdio.WatchedStatesOver(watchedOver(v), func(state, key string) ([]byte, bool, error) { diff --git a/modules/mesh-vault/cmd/mesh-vault/main.go b/modules/mesh-vault/cmd/mesh-vault/main.go index 171e66138..2026a44c4 100644 --- a/modules/mesh-vault/cmd/mesh-vault/main.go +++ b/modules/mesh-vault/cmd/mesh-vault/main.go @@ -122,7 +122,7 @@ func watchLeavers(env *Env) bool { // The vault's finding writer (design 47 G §689, A-16): it states the assignments view's findings and observes none of // its own, and names no secret. w, err := views.NewStdioWriter(views.WriterConfig[meshconditions.Finding, meshconditions.FindingCleared, meshconditions.FindingsStated, meshconditions.FindingsAskedEvent]{ - Module: Seat, State: stdio.State(FindingsState), + Asked: meshconditions.SubjectFindingsAsked, Module: Seat, State: stdio.State(FindingsState), Observe: func(context.Context) (views.Observed[meshconditions.Finding], error) { return views.Observed[meshconditions.Finding]{}, nil }}) @@ -137,7 +137,7 @@ func watchLeavers(env *Env) bool { env.Vault.NodeLeft(node) } }, env.Vault.AssignmentsChanged, - views.WriterFindings{Writer: w}) + w) beginAssignmentsView(assignmentsView) } return true diff --git a/modules/mesh-watcher/cmd/mesh-watcher/main.go b/modules/mesh-watcher/cmd/mesh-watcher/main.go index d3a34095c..9e659b913 100644 --- a/modules/mesh-watcher/cmd/mesh-watcher/main.go +++ b/modules/mesh-watcher/cmd/mesh-watcher/main.go @@ -82,7 +82,7 @@ func main() { logf("%v", err) os.Exit(1) } - assignments.Findings = views.WriterFindings{Writer: findings} + assignments.Findings = findings assignments.Follow(assignerModule, mesh.StateSeatHolders, SeatHoldersTool) // the runtime answers the writer's subscription only once Serve has its channel go func() { @@ -103,7 +103,7 @@ func main() { // observes none of its own. func newFindingsWriter() (*views.Writer[seat.Finding, seat.FindingCleared, seat.FindingsStated, seat.FindingsAskedEvent], error) { return views.NewStdioWriter(views.WriterConfig[seat.Finding, seat.FindingCleared, seat.FindingsStated, seat.FindingsAskedEvent]{ - Module: "mesh-watcher", State: stdio.State("findings"), + Asked: seat.SubjectFindingsAsked, Module: "mesh-watcher", State: stdio.State("findings"), Observe: func(context.Context) (views.Observed[seat.Finding], error) { return views.Observed[seat.Finding]{}, nil }}) diff --git a/modules/minio/cmd/object-store/main.go b/modules/minio/cmd/object-store/main.go index 193c790bb..e043b6639 100644 --- a/modules/minio/cmd/object-store/main.go +++ b/modules/minio/cmd/object-store/main.go @@ -36,7 +36,7 @@ func main() { fmt.Fprintf(os.Stderr, "[object-store] %v\n", err) os.Exit(1) } - seats.Findings = views.WriterFindings{Writer: findings} + seats.Findings = findings seats.Follow(assignerModule, mesh.StateSeatHolders, SeatHoldersTool) s, err := newServer(os.Getenv(ConfigWord), os.Getenv(LinksWord), os.Getenv(ReceivesWord), func() (map[string][]byte, error) { return seats.Values(assignerModule + "." + mesh.StateSeatHolders) }) @@ -70,7 +70,7 @@ const SeatHoldersTool = assignerModule + ".assigner_read_seat_holders" // observes none of its own. func newFindingsWriter() (*views.Writer[seat.Finding, seat.FindingCleared, seat.FindingsStated, seat.FindingsAskedEvent], error) { return views.NewStdioWriter(views.WriterConfig[seat.Finding, seat.FindingCleared, seat.FindingsStated, seat.FindingsAskedEvent]{ - Module: "minio", State: stdio.State("findings"), + Asked: seat.SubjectFindingsAsked, Module: "minio", State: stdio.State("findings"), Observe: func(context.Context) (views.Observed[seat.Finding], error) { return views.Observed[seat.Finding]{}, nil }}) diff --git a/modules/nfs-server/cmd/nfs-server/main.go b/modules/nfs-server/cmd/nfs-server/main.go index 3a85efeb4..6b4d106df 100644 --- a/modules/nfs-server/cmd/nfs-server/main.go +++ b/modules/nfs-server/cmd/nfs-server/main.go @@ -52,7 +52,7 @@ func main() { } m := Here() w, err := views.NewStdioWriter(views.WriterConfig[seat.Finding, seat.FindingCleared, seat.FindingsStated, seat.FindingsAskedEvent]{ - Module: "nfs-server", State: stdio.State("findings"), + Asked: seat.SubjectFindingsAsked, Module: "nfs-server", State: stdio.State("findings"), Observe: func(context.Context) (views.Observed[seat.Finding], error) { return views.Observed[seat.Finding]{}, nil }}) @@ -60,7 +60,7 @@ func main() { fmt.Fprintln(os.Stderr, err) os.Exit(1) } - seats.Findings = views.WriterFindings{Writer: w} + seats.Findings = w seats.Follow(assignerModule, mesh.StateSeatHolders, SeatHoldersTool) seats.Start(context.Background()) // the runtime answers the writer's subscription only once Serve has its channel