GR11 (A-51, §388): the grants part's first-view-unanswered, first-view-empty and binding-unlisted through the held finding writer

The part's state `findings`, its finding stream `mesh-grants-findings` and the consume of `mesh-conditions.findings-asked`
are declared in module.json; the process makes mesh-sdk's held writer (Asked from the contract) and starts it once its
state is bound. Its other conditions stay said to the operator channel. gofmt of bus.go and grants_test.go.

Builds: design 47 G §388 (G-8), §529 (G-12), A-51 (G-20)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-11 22:45:48 +01:00
parent 81d6d548aa
commit 78825dc580
11 changed files with 226 additions and 10 deletions
+3 -1
View File
@@ -223,4 +223,6 @@ type emitting struct{ c *process.Conn }
func (e emitting) Emit(event string, body any) error { return e.c.Emit(event, body) }
func (e emitting) EmitSeat(seat, event string, body any) error { return e.c.EmitSeat(seat, event, body) }
func (e emitting) EmitSeat(seat, event string, body any) error {
return e.c.EmitSeat(seat, event, body)
}
@@ -85,6 +85,8 @@ func TestTheGrantsPartWhole(t *testing.T) {
for _, c := range []jetstream.StreamConfig{
{Name: "EVENTS", Subjects: []string{"mesh.mod.*.event.>", "mesh.seat.*.event.>"}},
{Name: "CONDITIONS", Subjects: []string{"mesh.seat.operator-channel.accept.condition.>"}},
// The part's finding stream (design 47 G §529): where its four findings are said by the held finding writer.
{Name: grants.Module + "-findings", Subjects: []string{"mesh.stream." + grants.Module + ".event.>"}},
} {
if _, err := js.CreateStream(ctx, c); err != nil {
t.Fatal(err)
@@ -280,6 +282,34 @@ func TestTheGrantsPartWhole(t *testing.T) {
out[c.Key] = string(c.State)
}
}
// The four findings the part moved to its writer (design 47 G §388, A-51) are said on its finding stream, never to the
// operator channel: they show here as the conditions they were, as last said.
fs, err := js.Stream(ctx, grants.Module+"-findings")
if err != nil {
return out
}
finfo, _ := fs.Info(ctx)
for seq := finfo.State.FirstSeq; seq != 0 && seq <= finfo.State.LastSeq; seq++ {
raw, err := fs.GetMsg(ctx, seq)
if err != nil {
continue
}
var f struct{ Kind, ID string }
if json.Unmarshal(raw.Data, &f) != nil || f.Kind == "" {
continue
}
id, err := keys.UnescapeName(f.ID)
if err != nil {
continue
}
state := "raised"
if strings.Contains(raw.Subject, ".event.finding-cleared.") {
state = "cleared"
} else if !strings.Contains(raw.Subject, ".event.finding-written.") {
continue
}
out[grants.Module+"."+f.Kind+"."+id] = state
}
return out
}
bindingsBoundAndWaits := func() {
+21 -3
View File
@@ -36,6 +36,7 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
mcseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/announce"
@@ -125,9 +126,26 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// The owners' five states by their first views (ADR 0615, ADR 0622): asked through the part's own account, then
// followed on the owners' views families, which the hearing below hands them.
sayCond := sayVia(ctx, js, account.Module)
// The four findings the part says of what it waits for go through mesh-sdk's held finding writer (design 47 G §388, §529,
// A-51): it holds them, and states them at its start and when the keeper asks. It starts once its state is bound; what the
// part writes before is held and written at its start.
fw, err := views.NewHeldWriter(views.WriterConfig[mcseat.Finding, mcseat.FindingCleared, mcseat.FindingsStated, mcseat.FindingsAskedEvent]{
Module: account.Module, State: conn.State(grants.StateFindings), Node: account.Node, Kinds: grants.FindingKinds,
Asked: mcseat.SubjectFindingsAsked, JS: js, Conn: nc, Logf: logger.Printf})
if err != nil {
return err
}
findingsKV := process.AwaitBucket(ctx, js, nc, account.Module+"_"+grants.StateFindings,
"the part's findings are held and said when its state is bound", logger.Printf)
findingsKV.Ready(func(jetstream.KeyValue) {
if err := fw.Start(ctx); err != nil {
logger.Printf("the finding writer cannot start, its findings are kept in the state alone: %v", err)
}
})
sayFinding := grants.SayFinding(fw, account.Node, time.Now)
fv := grants.NewViews(func(tool string) func(context.Context, string) (views.Answer, error) {
return grants.TimedAsk(tool, views.AskVia(grants.Unwrapped(conn.Call), tool), time.Now, logger.Printf)
}, sayCond, f.Wake, logger.Printf)
}, sayFinding, f.Wake, logger.Printf)
fv.Rest, fv.Watching = bus, bus
byState := map[string]*views.Follower{}
for _, v := range grants.FirstViews {
@@ -143,7 +161,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
f.HearKept = func() error { return hearRunning(ctx, account, js, f, logger.Printf) }
// The first view of the bindings (design 25 bindings-first-view): the seat mesh-broker's list, asked at each start and by
// hand, with the seat's events held until its answer is applied or its bound passes.
f.Part.Bindings = &grants.BindingsView{Say: sayCond, Changed: f.Wake, Logf: logger.Printf,
f.Part.Bindings = &grants.BindingsView{Say: sayFinding, Changed: f.Wake, Logf: logger.Printf,
Bound: bindingsTiming.bound, Wait: bindingsTiming.wait, MaxWait: bindingsTiming.max,
Ask: func(context.Context) (json.RawMessage, error) {
return grants.Unwrapped(conn.Call)(grants.CallBrokerList, map[string]any{})
@@ -153,7 +171,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
gate := newSeatGate()
gate.Follow(seatlist.ProcessBus(conn.Call, hub), f.Wake, logger.Printf)
f.Part.SeatList = gate
f.Part.Say = sayCond
f.Part.Say, f.Part.SayFinding = sayCond, sayFinding
fv.Start(ctx, f.Wake)
f.Asked = func(wake func()) error {
return conn.State(grants.StateUserList).Watch(grants.KeyAsked, func(c stdio.StateChange) error {
+2 -2
View File
@@ -64,7 +64,7 @@ type fakeBus struct {
events []string
// seatEvents are the seat events said, as seat.event.
seatEvents []string
writes []string
writes []string
// Each membership's subject: the last membership said there, every notice, and whether it is refused.
stream map[string][]byte
publishes []string
@@ -3660,7 +3660,7 @@ func TestGrants(t *testing.T) {
}
waitFor(t, refusedOnce.Load)
p := partOn(b)
p.Read, p.Views, p.Say = fv, fv, say
p.Read, p.Views, p.Say, p.SayFinding = fv, fv, say, say
before := p.Compose()
// The raise the condition keeper did not take was said again by the pass.
if !raised(grants.FirstViewUnanswered+".mesh-delivery-indexer.verdicts", occ.StateRaised) {
@@ -33,6 +33,10 @@ func Announced() []announce.State {
out = append(out, announce.State{Name: s, Readers: slices.Clone(AccountsReaders)})
continue
}
if s == StateFindings {
out = append(out, announce.State{Name: s}) // the keeper hears findings as events: nobody reads the state
continue
}
out = append(out, announce.State{Name: s, Readable: true})
}
return out
@@ -0,0 +1,72 @@
package grants
import (
"context"
"fmt"
"strings"
"time"
mcseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/views"
)
// **The grants part's findings** (design 47 G §388, §529, A-51). The four kinds the part says of what it waits for (`view-waits`,
// `first-view-unanswered`, `first-view-empty`, `binding-unlisted`) are written through mesh-sdk's held finding writer of a process,
// in the part's own state `findings` and on its finding stream `mesh-grants-findings`; every other condition of the part stays one
// said to the operator channel (Part.Say).
// StateFindings is the part's state of findings (design 47 G §529, a foundation part's old `state` section).
const StateFindings = "findings"
// FindingKinds are the finding kinds the part writes beside the four of a views reader (views.ViewKinds), which its writer always
// owns: first-view-empty and binding-unlisted.
var FindingKinds = []string{kindOf(FirstViewEmpty), kindOf(BindingUnlisted)}
// FindingWriter is the part's held finding writer (views.HeldWriter): what the part writes and clears its findings through.
type FindingWriter interface {
Write(ctx context.Context, f mcseat.Finding) error
Clear(ctx context.Context, scope, id, kind, why string) error
}
// kindOf is the finding kind of one of the part's condition keys: the key `mesh-grants.<kind>` without its module.
func kindOf(key string) string { return strings.TrimPrefix(key, Module+".") }
// SayFinding makes the part's say of the conditions that are findings: a condition whose key is `mesh-grants.<kind>.<id>` is the
// finding of that kind and id (the id escaped as a key's name is), written while the condition is raised and cleared when it is
// cleared. node is the machine the part runs on. A condition of any other key is refused, so none goes through by mistake.
func SayFinding(w FindingWriter, node string, now func() time.Time) func(oc.Condition) error {
if now == nil {
now = time.Now
}
return func(c oc.Condition) error {
rest, ok := strings.CutPrefix(c.Key, Module+".")
kind, id, hasID := strings.Cut(rest, ".")
if !ok || !hasID || !isFindingKind(kind) {
return fmt.Errorf("%q is not one of the part's findings", c.Key)
}
id = keys.EscapeName(id)
ctx := context.Background()
if c.State != occ.StateRaised {
return w.Clear(ctx, views.FindingScope, id, kind, c.Resolved)
}
sev := c.Severity
if sev == "" {
sev = mcseat.FindingSeverityWarning
}
return w.Write(ctx, mcseat.Finding{Scope: views.FindingScope, ID: id, Kind: kind, Severity: sev, Source: "mesh-grants",
Summary: c.Summary, Headline: c.Headline, Needs: c.Needs, Explanation: c.Explanation, Observer: Module, At: now().UTC(),
Subject: mcseat.Subject{Scope: views.FindingScope, ID: Module, Machine: node}})
}
}
func isFindingKind(kind string) bool {
for _, k := range append([]string{views.ViewWaitsKind, views.UnansweredKind}, FindingKinds...) {
if k == kind {
return true
}
}
return false
}
@@ -0,0 +1,67 @@
package grants
import (
"context"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/views"
mcseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
)
type findingsFake struct {
written []mcseat.Finding
cleared [][4]string
}
func (f *findingsFake) Write(_ context.Context, w mcseat.Finding) error {
f.written = append(f.written, w)
return nil
}
func (f *findingsFake) Clear(_ context.Context, scope, id, kind, why string) error {
f.cleared = append(f.cleared, [4]string{scope, id, kind, why})
return nil
}
// Design 47 G §388, §529, A-51: the part's first-view-unanswered, first-view-empty and binding-unlisted are findings of the part's
// module, written while raised and cleared when cleared, never conditions said to the operator channel; any other key is refused.
func TestTheFourKindsAreSaidAsFindings(t *testing.T) {
at := time.Date(2026, 11, 11, 22, 0, 0, 0, time.UTC)
f := &findingsFake{}
say := SayFinding(f, "ace", func() time.Time { return at })
fv := FirstViews[0]
if err := say(unansweredCondition(fv, views.Status{}, true)); err != nil {
t.Fatal(err)
}
if err := say(emptyCondition(fv, true)); err != nil {
t.Fatal(err)
}
if err := say(unlistedCondition("mod-c", "kept", true)); err != nil {
t.Fatal(err)
}
want := []struct{ kind, id string }{
{"first-view-unanswered", "mesh-delivery-assigner.assignments"}, {"first-view-empty", "mesh-delivery-assigner.assignments"}, {"binding-unlisted", "mod-c.kept"}}
if len(f.written) != 3 {
t.Fatalf("written %+v", f.written)
}
for i, w := range want {
got := f.written[i]
if got.Kind != w.kind || got.Scope != "module" || got.Observer != Module || got.Subject.Machine != "ace" || !got.At.Equal(at) ||
got.Severity != mcseat.FindingSeverityWarning || got.Summary == "" || got.Headline == "" {
t.Errorf("finding %d = %+v, want kind %s", i, got, w.kind)
}
}
if err := say(unlistedCondition("mod-c", "kept", false)); err != nil {
t.Fatal(err)
}
if len(f.cleared) != 1 || f.cleared[0][0] != "module" || f.cleared[0][2] != "binding-unlisted" || f.cleared[0][3] == "" {
t.Errorf("cleared %+v", f.cleared)
}
if err := say(oc.Condition{Key: RefusedCondition, State: occ.StateRaised}); err == nil || len(f.written) != 3 {
t.Errorf("a condition that is no finding was taken: %v", err)
}
}
@@ -440,7 +440,7 @@ func (p *Part) sayFirstViews(res *Result) {
if s.Answered.IsZero() && !p.fvSaid.known {
continue // not answered yet: nothing to say of its emptiness
}
if p.Say == nil || p.Say(emptyCondition(fv, empty)) == nil {
if p.SayFinding == nil || p.SayFinding(emptyCondition(fv, empty)) == nil {
p.fvSaid.empty[fv.Read] = empty
}
}
+4 -2
View File
@@ -35,7 +35,7 @@ const (
)
// States is every state the part keeps.
var States = []string{StateAccounts, StateAssertions, StateGrants, StateMemberships, StateRunning, StateUserList, StateWithheld}
var States = []string{StateAccounts, StateAssertions, StateFindings, StateGrants, StateMemberships, StateRunning, StateUserList, StateWithheld}
// The events the part says.
const (
@@ -171,7 +171,9 @@ type Part struct {
Bus BusAddress
// Say says a condition to the operator: the process's; nil in the tools.
Say func(oc.Condition) error
Now func() time.Time
// SayFinding says the part's conditions that are findings (SayFinding, design 47 G §388): first-view-empty; nil in the tools.
SayFinding func(oc.Condition) error
Now func() time.Time
// SeatList is the seat list reader, which gates a module whose seats the list has not named (inputs.go, SeatGate); nil
// gates none.
SeatList SeatGate
@@ -43,7 +43,11 @@ const EventAccountIssued = "mesh-vault." + buscompose.AccountIssuedEvent
// Consumes is every event the part hears, as its manifest's consumes name them.
// The seat mesh-broker's created, recreated and dropped are heard as every seat's event, `<seat>.<event>` on EVENTS (the
// lead's ruling, registry milestone 13:0x), never on the bus module's own stream.
var Consumes = append(append(append(slices.Clone(seatEvents), seatListEvents...), viewsEvents()...), ConsumesRunning)
var Consumes = append(append(append(slices.Clone(seatEvents), seatListEvents...), viewsEvents()...), ConsumesRunning, ConsumesFindingsAsked)
// ConsumesFindingsAsked is the keeper's ask the part's finding writer hears on a consumer of its own (design 47 G §390, §529): in
// the manifest's consumes for the grant, never handed to the event hub.
const ConsumesFindingsAsked = meshconditions.Seat + "." + meshconditions.EventFindingsAsked
// ConsumesRunning is the seat node-engine's event running, in the local form the part's durable on its kept stream
// filters (running.go): it is heard on that stream alone, never on the shared events stream (SeatEventSubjects leaves it out).
+17
View File
@@ -13,6 +13,7 @@
"consumes": [
"mesh-conditions.condition-raised",
"mesh-conditions.condition-cleared",
"mesh-conditions.findings-asked",
"mesh-vault.account-issued",
"mesh-broker.created",
"mesh-broker.dropped",
@@ -77,6 +78,10 @@
"name": "assertions",
"history": 5
},
{
"name": "findings",
"history": 5
},
{
"name": "grants",
"history": 5
@@ -98,6 +103,18 @@
"history": 5
}
],
"streams": [
{
"name": "mesh-grants-findings",
"events": [
"finding-written.*",
"finding-cleared.*",
"findings-stated.*"
],
"max-age-days": 7,
"max-mib": 16
}
],
"own-secrets": {
"broker": "${dir:state}/broker"
},