Files
mesh-delivery/modules/mesh-grants/internal/grants/records.go
T
jschoubben d29e1426d0 Asker rename R2: every asker of a build seat asks under its seat's own name, and its step is keyed so
A-163: the asker in `accept.<queue>.<asker>` is the asking seat's name (mesh-pipeline-runner,
mesh-catalog, mesh-wants, mesh-builds), never a nickname: the builder asks as mesh-builds (was
"builder"), the indexer's validation as mesh-catalog (was "indexer"), the assigner's runs as
mesh-wants (was "assigner"), the runner's steps on `accept.check.mesh-pipeline-runner` and
`accept.delivery.mesh-pipeline-runner` (were `accept.check.step` and `accept.delivery.step`, where
the holder read `step` as the asker). An asker naming none (the checker) asks on main's subject
(A-184). A build seat's queues are delivery, check and recheck alone (A-170, A-184): buildstep's
AskKinds holds no step, a holder takes a step from check or delivery, and the grants part makes a
worker per queue, never per kind. A holder keys a named ask's step `<asker>.<node>.<id>` and main's
`<node>.<id>` (§481, §483); readers watch their own name's keys. The composition step's step-progress
names the one caller mesh-wants' definition lists for step, read from the seat list (A-294).

Builds: design 47 G §481, §483, §543, §600, A-163, A-170, A-184, A-248, A-294

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 18:37:29 +01:00

779 lines
32 KiB
Go

package grants
import (
"fmt"
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
sdkbc "git.novox.be/novox/mesh-sdk/go/buscompose"
"maps"
"reflect"
"slices"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"mesh-grants/internal/buscompose"
)
// Records is what the bus composer composes from, rebuilt from the bus (mesh-controller internal/inventory busrecords.go,
// BusRecords, read the same way from Inputs instead of the controller's store). Pure. An assigned module whose manifest
// the indexer does not hold is left out and named, where the controller refuses the whole composition: the shadow
// composes what it can and says what it could not.
func Records(in Inputs) (buscompose.Records, []Gap) { return records(in, false) }
// ListRecords is Records for the user list and the grants (issue 774): a module whose wanted build's manifest is not the
// trunk's is declared with what both grant (unionDeclared), so the list carries the trunk manifest's subjects before the
// assigner moves the want to it, and a grant at the trunk's fingerprint waits for nothing the want's move would bring.
// Memberships are composed from it too, and from the build each machine runs now (Inputs.Had): while a machine's build
// differs from the want's or the trunk's, neither loses a read, call or consume (ADR 0660 §2 applied to memberships,
// issue 1966). Consumers stay with Records. Pure.
func ListRecords(in Inputs) (buscompose.Records, []Gap) { return records(in, true) }
func records(in Inputs, union bool) (buscompose.Records, []Gap) {
var gaps []Gap
seats := map[string]catalogue.SeatDeclaration{}
declarers := map[string]string{}
declared := in.declared()
for _, m := range declared {
for _, s := range m.DefinesSeats {
seats[s.Name] = s
declarers[s.Name] = m.Module
}
}
for _, own := range catalogue.SeatsWithAProtocol() {
seats[own.Name] = catalogue.SeatDeclaration{Name: own.Name, Scope: own.Scope, Accepts: own.Accepts, Emits: own.Emits, Serves: own.Serves}
}
// The enrolment users, from the seat mesh-enrolment's keys, enrolling through its redeem alone (ADR 0587 §4, §6).
out := buscompose.Records{Assigned: map[string][]buscompose.Declared{}, People: map[string][]string{},
Interchangeable: map[string]bool{}, Enrolling: slices.Clone(in.Enrolling),
// And each node's logins (ADR 0583 §1): both while it rotates.
NodeLogins: in.NodeLogins, Kept: in.Kept, Chunked: in.Chunked, EngineEvents: in.EventSubjects[buscompose.EngineSeat],
// What the bus module's process may define: every manifest's bus state, and what it said it created (DLV-G2).
BusState: map[string][]catalogue.BusStateEntry{}, Bound: in.Bound,
// Each node's capability reading and what each node-engine said running (design 46 approval mark §6).
Readings: in.Readings, Running: map[string]string{}}
for key, h := range in.Heard {
if !h.Stopped {
out.Running[key] = h.Fingerprint
}
}
for module, m := range in.Manifests {
if len(m.BusState) > 0 {
out.BusState[module] = m.BusState
}
}
// The bus names of kept data the recovery module's account is granted (design 47 G §554, §565): those of every manifest of the
// catalog graph the part holds (the trunk's, each node's wanted, ran and had builds), and those restore points name.
out.RecoveryNames = recoveryNamesOf(in)
// The holders on record, after the seats whose holder has left are passed (ADR 0660): the one rule of mesh-sdk, the
// node-engine's too, read with the manifest each node runs, else the trunk's. Composed, never recorded.
holders, passed := holdersAfterPass(in)
gaps = append(gaps, passed...)
values := func(provider, setting string) []any {
var vs []any
for _, node := range in.Nodes {
if v, ok := in.Values[node+"/"+provider+"/"+setting]; ok && slices.Contains(in.Assigned[node], provider) {
vs = append(vs, v)
}
}
return vs
}
for _, node := range in.Nodes {
out.Nodes = append(out.Nodes, node)
manifestOf := func(module string) (catalogue.Manifest, bool) {
if m, ok := in.Running[node+"."+module]; ok {
return m, true
}
m, ok := in.Manifests[module]
return m, ok
}
var here []catalogue.Manifest
for _, module := range in.Assigned[node] {
if m, ok := manifestOf(module); ok {
here = append(here, m)
}
}
for _, module := range in.Assigned[node] {
m, known := manifestOf(module)
if !known {
gaps = append(gaps, Gap{Input: module + " on " + node, Why: "left out of the shadow list: no manifest to derive what it may say from"})
continue
}
declare := func(m catalogue.Manifest) buscompose.Declared {
m.Consumes = resolveSeatViews(m.Consumes, holders)
m.Reads = resolveSeatReads(m.Reads, holders)
d := declaredFor(m, seats, declarers)
if len(in.Placed) > 0 {
d.Holds = heldWherePlaced(d.Holds, holders, node, m.Module)
}
for _, sr := range catalogue.ReadsGranted(m, here, node) {
d.KeyedReads = append(d.KeyedReads, buscompose.KeyedRead{Read: sr.Read, Key: sr.Key})
}
d.Hears, d.HearsAStream, d.Consumes = hearsOf(m, declared, node, values, d.Consumes, func(event string) bool { return in.Defined[event] })
var keptHeard bool
d.Watches, d.Consumes, d.Hears, keptHeard = keptOutOfHearing(in.Kept, d.Watches, d.Consumes, d.Hears)
d.HearsAStream = d.HearsAStream || keptHeard
d.Reads, d.ReadsObjects, d.ReadsRefused = readsOfObjects(m.Module, node, d.Reads, d.ReadsRefused, declared, values)
// The sent stores of the modules it is for (design 47 G §59, §124), read from the catalog graph's manifests and the
// definitions' callers.
d.ReadsObjects = append(d.ReadsObjects, sentStoresRead(m, in.Manifests, in.VerbCallers)...)
// Its bindings as kept (bindings.go): the same source its membership's `bindings` is written from in this pass,
// so a grant and its binding are written together, never one before the other (issue 1966, the kind of issue 249).
d.Bindings = in.Bindings[m.Module]
withEventSubjects(in.EventSubjects, &d)
withAcceptCallers(in.AcceptCallers, &d)
withVerbCallers(in.VerbCallers, &d)
withNoNamedAsks(NamedAsksUnused(in), &d)
return d
}
d := declare(m)
trunk, onTrunk := in.Manifests[module]
// What a node composing a consumer reads of it is the trunk manifest's served keys (frombus reads the indexer's
// manifest of another node's module), and what runs may still be another build: both are granted (issue 1126).
if onTrunk {
d.Served = slices.Compact(slices.Sorted(slices.Values(append(slices.Clone(d.Served), catalogue.ServedSettings(trunk)...))))
}
if union && onTrunk && !reflect.DeepEqual(m, trunk) {
d = unionDeclared(d, declare(trunk))
}
if had, ok := in.Had[node+"."+module]; union && ok && !reflect.DeepEqual(m, had) {
d = unionDeclared(d, declare(had))
}
out.Assigned[node] = append(out.Assigned[node], d)
if m.Instances == catalogue.InstancesInterchangeable || union && onTrunk && trunk.Instances == catalogue.InstancesInterchangeable {
out.Interchangeable[m.Module] = true
}
}
}
// The node-engine's own manifest, placed on every node (ADR 0563): its state, logs and streams alone, created by this
// part; it adds no principal, since each node's engine speaks as the node's account.
if m, ok := declared[buscompose.EngineModule]; ok {
// What it consumes and invokes are its node account's: a seat's event also on the seat's own subject, as a module's
// consumes are split (consumedOf), every consume still on its emitter's as before, and each verb it invokes in the
// node's own name (issue 1966: the seat list's verb and events).
_, watches := consumedOf(m, seats, declarers)
out.Engine = &buscompose.Declared{Module: m.Module, Consumes: m.Consumes, Watches: watches, Invokes: m.Invokes,
State: bucketsFor(m, in.Nodes), Logs: logsOf(m), Streams: streamsOf(m)}
withEventSubjects(in.EventSubjects, out.Engine)
withAcceptCallers(in.AcceptCallers, out.Engine)
withVerbCallers(in.VerbCallers, out.Engine)
}
return out, gaps
}
// declaredFor is mesh-controller's declaredFor (internal/inventory/busrecords.go), copied: one module's manifest as the
// composer needs it.
func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaration, declarers map[string]string) buscompose.Declared {
fromModules, watches := consumedOf(m, seats, declarers)
d := buscompose.Declared{
Module: m.Module, ToolRunner: catalogue.IsToolRunner(m), Emits: m.EmitsAll(), Consumes: fromModules, Watches: watches, Serves: m.Tools,
Invokes: m.Invokes, State: bucketsOf(m), Reads: m.Reads, Logs: logsOf(m),
Streams: streamsOf(m), Queues: queueOwnersOf(m), Processes: processesOf(m), ServesOwn: ownServesOf(m), Checks: catalogue.HealthChecks(m),
RunsAs: m.RunsAs, Issued: issuedTools(m),
// The keys its served values fill, which every node may read where it runs (issue 1126).
Served: catalogue.ServedSettings(m),
// Its own object buckets (ADR 0592 §5).
Objects: objectsOf(m),
// The ledgers it asks for in `requires` (issue 1966 D5): each assignment gets a ledger account.
Ledgers: ledgersOf(m),
// The events of its finding stream asked for in `requires` (lead answer A-280).
FindingEvents: findingEventsOf(m),
}
d.Loads = loadsABundle(m)
// Its marked tools (design 46 approval mark §1, §6): a name its `needs-approval` gives that it serves as a tool.
for _, mark := range m.NeedsApproval {
if slices.Contains(m.Tools, mark) {
d.Marked = append(d.Marked, mark)
}
}
d.MarkedVerbs = markedNamed(d, seats)
d.CatchUps = catchUpsOf(m)
if _, reads := m.OwnSecrets["broker"]; !reads {
d.NoAccount = true
}
if _, reads := m.OwnSecrets[buscompose.ProvisionerSecret]; reads && m.ClaimsSeat(buscompose.NodeProvisionerSeat) {
d.ProvisionsNode = true
}
d.SnapshotsTheBus = m.ClaimsSeat(meshbroker.Seat)
d.HoldsRecovery = m.ClaimsSeat(buscompose.RecoverySeat)
for _, c := range m.Claims {
if s, hasAProtocol := seats[c.Name]; hasAProtocol {
held := asSeat(s, declarers[s.Name])
held.Kind = c.Kind
held.Capabilities = c.Capabilities
held.Marked = markedVerbs(m, s)
d.Holds = append(d.Holds, held)
}
}
for _, name := range m.Uses {
if s, declaredSomewhere := seats[name]; declaredSomewhere {
d.Uses = append(d.Uses, asSeat(s, declarers[s.Name]))
}
}
return d
}
// seatViewConsume is a `consumes` entry naming a seat's view as `seat:<seat>.views.<state>` (design 47 G §616).
const seatViewConsume = "seat:"
// resolveSeatViews resolves each `seat:<seat>.views.<state>` of a module's consumes to `<holder>.views.<state>`, the module
// holding the seat on record, as it resolves a seat's events: a new holder is granted with no change of the manifest (design
// 47 G §616). An entry whose seat has no holder on record stays as written, and grants nothing. Pure.
func resolveSeatViews(consumes []string, holders []catalogue.Held) []string {
var out []string
for i, c := range consumes {
rest, isSeat := strings.CutPrefix(c, seatViewConsume)
seatName, view, named := strings.Cut(rest, ".views.")
if !isSeat || !named || view == "" {
if out != nil {
out = append(out, c)
}
continue
}
if out == nil {
out = slices.Clone(consumes[:i])
}
holder := slices.IndexFunc(holders, func(h catalogue.Held) bool { return sameSeat(h.Claim, seatName) })
if holder < 0 {
out = append(out, c)
continue
}
out = append(out, holders[holder].Module+".views."+view)
}
if out == nil {
return consumes
}
return out
}
// resolveSeatReads resolves each `seat:<seat>.<state>` of a module's reads to `<holder>.<state>`, the module holding the seat
// on record, as resolveSeatViews does a view: a new holder is read with no change of the manifest (design 47 G A-116). An entry
// whose seat has no holder on record stays as written, and grants nothing. Pure.
func resolveSeatReads(reads []string, holders []catalogue.Held) []string {
var out []string
for i, r := range reads {
rest, isSeat := strings.CutPrefix(r, seatViewConsume)
seatName, state, named := strings.Cut(rest, ".")
if !isSeat || !named || state == "" || strings.Contains(state, ".") {
if out != nil {
out = append(out, r)
}
continue
}
if out == nil {
out = slices.Clone(reads[:i])
}
holder := slices.IndexFunc(holders, func(h catalogue.Held) bool { return sameSeat(h.Claim, seatName) })
if holder < 0 {
out = append(out, r)
continue
}
out = append(out, holders[holder].Module+"."+state)
}
if out == nil {
return reads
}
return out
}
// consumedOf splits what a manifest consumes as the composer grants it: a seat's event on the seat's own subject (its
// watches), every other by its emitter's (novox/hq ADR 0118). A placed module and the node-engine's own manifest are split
// alike.
func consumedOf(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaration, declarers map[string]string) ([]string, []buscompose.Seat) {
var fromModules []string
var watches []buscompose.Seat
for _, c := range m.Consumes {
emitter, event, named := strings.Cut(c, ".")
if named {
if s, isASeat := seats[emitter]; isASeat && s.Kinded {
ev := strings.TrimSuffix(event, ".*")
if catalogue.SeatSays(s.Emits, ev) {
watches = append(watches, buscompose.Seat{Name: s.Name, Scope: s.Scope, Emits: []string{ev},
Kinded: true, Proofs: s.Proofs, DeclaredBy: declarers[s.Name]})
continue
}
}
if s, isASeat := seats[emitter]; isASeat && catalogue.SeatSays(s.Emits, event) {
// Its scope, and whether the node-engine holds it, place an event that names its holder's machine (ADR 0618 §4).
compiled, _ := catalogue.SeatNamed(s.Name)
watches = append(watches, buscompose.Seat{Name: s.Name, Scope: s.Scope, Emits: []string{event},
HeldByTheEngine: compiled.HeldByTheEngine})
continue
}
}
fromModules = append(fromModules, c)
}
return fromModules, watches
}
// loadsABundle says the node's tool runtime loads a bundle of the module. The controller reads it from the resolved
// manifest's Bundles, which a build derives and the indexer's manifest does not carry (the builder's build record names
// the repository's manifest digest), so the same answer is derived here from `build.artifacts` by mesh-sdk's rule
// (the build's own rule for what a bundle loads): a bundle that says `loads`, else every bundle of a module that declares tools and that no
// resource of the module runs. Without it a module whose only runtime-loaded part is a helper (route-proxy's
// route-provider) was left out of node-tools' grants (issue 1945).
func loadsABundle(m catalogue.Manifest) bool {
for _, b := range m.Bundles {
if len(b.Loads) > 0 {
return true
}
}
if m.Build == nil {
return false
}
run := map[string]bool{}
for _, r := range m.Resources {
if a, ok := r["artifact"].(string); ok && a != "" {
run[a] = true
}
}
for _, a := range m.Build.Artifacts {
if a.Kind != catalogue.ArtifactBundle {
continue
}
if len(a.Loads) > 0 || (a.Loads == nil && len(m.Tools) > 0 && !run[a.Name]) {
return true
}
}
return false
}
func issuedTools(m catalogue.Manifest) []string {
var out []string
for _, o := range m.Provides {
if tool, issued := m.IssuedCredentialOf(o.Name); issued && !slices.Contains(out, tool) {
out = append(out, tool)
}
}
return out
}
func bucketsOf(m catalogue.Manifest) []buscompose.Bucket {
var out []buscompose.Bucket
for _, s := range m.State {
out = append(out, buscompose.Bucket{Module: m.Module, Name: s.Name, History: s.History, TTLSeconds: s.TTLSeconds, MaxMiB: s.MaxMiB,
PerMachine: s.PerMachine, KeyTTL: s.KeyTTL, ReadOnly: s.ReadOnly})
}
return out
}
// bucketsFor is a manifest's buckets as this part creates them: bucketsOf, and for the node-engine every node's bucket of
// compositions, `composed-<node>`, in place of a state it names so (novox/hq ADR 0593 §6): the engine writes its own node's
// and never creates it.
func bucketsFor(m catalogue.Manifest, nodes []string) []buscompose.Bucket {
out := bucketsOf(m)
if m.Module != buscompose.EngineModule {
return out
}
out = slices.DeleteFunc(out, func(b buscompose.Bucket) bool { return b.Name == buscompose.ComposedState })
return append(out, buscompose.EngineComposedBuckets(nodes)...)
}
// objectsOf is a manifest's own object buckets (ADR 0592 §5).
func objectsOf(m catalogue.Manifest) []buscompose.ObjectBucket {
var out []buscompose.ObjectBucket
for _, o := range m.Objects {
out = append(out, buscompose.ObjectBucket{Module: m.Module, Name: o.Name, MaxMiB: o.MaxMiB, Restricted: o.Grant != nil})
}
return out
}
// readsOfObjects takes out of a module's reads each that names another module's object bucket (ADR 0592 §9): read, by
// its bus name, where the owner's trusted config value of the bucket's grant names the module on this node, and refused
// otherwise, a bucket with no grant included (its owner alone reads it). Every other read stays as it was: a state's.
func readsOfObjects(module, node string, reads, refused []string, manifests map[string]catalogue.Manifest,
values func(provider, setting string) []any) (states, objects, refusedOut []string) {
refusedOut = refused
for _, r := range reads {
owner, name, ok := strings.Cut(strings.TrimSpace(r), ".")
o, isObjects := catalogue.ObjectsDeclaration{}, false
if ok {
o, isObjects = manifests[owner].ObjectsNamed(name)
}
if !isObjects {
states = append(states, r)
continue
}
if o.Grant != nil && o.ObjectsReader(values(owner, o.Grant.Config), module, node) {
objects = append(objects, o.Bucket(owner))
} else {
refusedOut = append(refusedOut, r)
}
}
return states, objects, refusedOut
}
// ledgersOf are the local names of the ledgers a manifest asks for in `requires`.
func ledgersOf(m catalogue.Manifest) []string {
var out []string
for _, e := range m.BusState {
if e.Storage == catalogue.StorageLedger {
out = append(out, e.Name)
}
}
return out
}
func logsOf(m catalogue.Manifest) []buscompose.Log {
var out []buscompose.Log
for _, l := range m.Logs {
out = append(out, buscompose.Log{Module: m.Module, Name: l.Name, MaxMiB: l.Cap(), MaxAgeDays: l.MaxAgeDays})
}
return out
}
// markedVerbs are the verbs of a seat the module holds that need the operator's approval: those its definition marks for every
// holder, and those the module's `needs-approval` names as `<seat>.<verb>` (design 46 approval mark §2, §6).
func markedVerbs(m catalogue.Manifest, s catalogue.SeatDeclaration) []string {
var out []string
for _, v := range s.Serves {
if v.NeedsApproval || slices.Contains(m.NeedsApproval, s.Name+"."+v.Name) {
out = append(out, v.Name)
}
}
return out
}
// markedNamed are the verbs a seat definition marks (design 46 approval mark §2; design 47 G §677) that the declaration's module
// or one of its processes invokes by name or answers, each `seat:<seat>.<verb>`, sorted.
func markedNamed(d buscompose.Declared, seats map[string]catalogue.SeatDeclaration) []string {
named := slices.Concat(d.Invokes, d.ServesOwn)
for _, pr := range d.Processes {
named = append(named, slices.Concat(pr.Invokes, pr.Serves)...)
}
var out []string
for _, v := range named {
seat, verb, ok := catalogue.SeatInvoke(v)
if !ok {
continue
}
if i := slices.IndexFunc(seats[seat].Serves, func(s catalogue.Verb) bool { return s.Name == verb }); i >= 0 && seats[seat].Serves[i].NeedsApproval {
out = append(out, v)
}
}
slices.Sort(out)
return slices.Compact(out)
}
// queuesOf are a pulled-by-kind seat's queues, in the order of its accepted kinds: a kind's stream, or its own name where it
// rides none (step and run ride check or delivery, so a build seat's are delivery, check and recheck). Each is one subject
// and one worker of each form (design 47 G §600, §544, A-170, A-184).
func queuesOf(own catalogue.Seat) []string {
streamed := map[string]bool{}
var out []string
for _, on := range own.AcceptedOn {
if stream, kind, ok := strings.Cut(on, "."); ok {
streamed[kind] = true
if !slices.Contains(out, stream) {
out = append(out, stream)
}
}
}
var queues []string
for _, a := range own.Accepts {
if !streamed[a] && !slices.Contains(queues, a) {
queues = append(queues, a)
}
}
for _, q := range out {
if !slices.Contains(queues, q) {
queues = append(queues, q)
}
}
return queues
}
func asSeat(s catalogue.SeatDeclaration, declarer string) buscompose.Seat {
seat := buscompose.Seat{Name: s.Name, Scope: s.Scope, Accepts: s.Accepts, Emits: s.Emits,
Serves: catalogue.VerbNames(s.Serves), Kinded: s.Kinded, ByCaller: s.ByCaller, Proofs: s.Proofs, DeclaredBy: declarer}
if own, ok := catalogue.SeatNamed(s.Name); ok && own.PulledByKind {
seat.PulledByKind = true
seat.Accepts = queuesOf(own)
}
for _, r := range s.Records {
if declarer != "" {
seat.Records = append(seat.Records, buscompose.BucketName(declarer, r))
}
}
return seat
}
func streamsOf(m catalogue.Manifest) []buscompose.ModuleStream {
var out []buscompose.ModuleStream
for _, s := range m.Streams {
events := make([]string, 0, len(s.Events))
for _, e := range s.Events {
events = append(events, sdkbc.WirePattern(e))
}
out = append(out, buscompose.ModuleStream{Module: m.Module, Name: s.BusName(), Events: events,
MaxAgeDays: s.Days(), MaxMiB: s.Cap(), Restricted: s.Grant != nil,
WorkQueue: s.Retention == catalogue.StreamRetentionWorkQueue, Duplicates: s.Duplicates, History: s.KeepsHistory(), LastPerSubject: s.LastPerSubject})
}
return out
}
func queueOwnersOf(m catalogue.Manifest) []buscompose.QueueOwner {
buses := m.ProcessBuses()
if len(buses) == 0 {
return nil
}
byName := map[string]catalogue.StreamDeclaration{}
for _, s := range m.Streams {
byName[s.Name] = s
}
var out []buscompose.QueueOwner
for _, r := range m.Resources {
id := fmt.Sprint(r["id"])
b, declared := buses[id]
if !declared {
continue
}
for _, q := range b.Queues {
s, known := byName[q]
if !known || s.Retention != catalogue.StreamRetentionWorkQueue {
continue
}
out = append(out, buscompose.QueueOwner{Process: id, Stream: s.BusName(), MaxDeliver: s.MaxDeliveries})
}
}
return out
}
// ownServesOf is the seat verbs of the module's processes that declare no bus, which run on the module's own account (design
// 47 G A-115, A-127); a process with a bus carries its own serves (processesOf).
func ownServesOf(m catalogue.Manifest) []string {
buses := m.ProcessBuses()
var out []string
for _, r := range m.Resources {
id := fmt.Sprint(r["id"])
if _, bus := buses[id]; bus {
continue
}
out = append(out, m.ProcessServes()[id]...)
}
return out
}
func processesOf(m catalogue.Manifest) []buscompose.ProcessPrincipal {
accounts := m.BusAccounts()
if len(accounts) == 0 {
return nil
}
queues := queueOwnersOf(m)
var out []buscompose.ProcessPrincipal
// Every own bus account, processes first and then tools bundles (catalogue.BusAccounts): a tools bundle's account carries
// what it may remove from the bus (ADR 0564 Limits 4, issue 1952 S5).
for _, a := range accounts {
pr := buscompose.ProcessPrincipal{Process: a.ID, Emits: a.Bus.Emits, State: a.Bus.State, Asks: a.Bus.Asks,
Invokes: a.Bus.Invokes, Serves: a.Serves, Removes: a.Bus.Removes, Tools: a.Tools, Keeps: a.Bus.Keeps}
for _, q := range queues {
if q.Process == a.ID {
pr.Queues = append(pr.Queues, q)
}
}
out = append(out, pr)
}
return out
}
func hearsOf(m catalogue.Manifest, declared map[string]catalogue.Manifest, node string, values func(provider, setting string) []any,
consumes []string, defined func(event string) bool) ([]buscompose.Heard, bool, []string) {
reading := m
reading.Consumes = consumes
parts, touches, onEvents := catalogue.Hearing(reading, declared, node, values, defined)
var out []buscompose.Heard
for _, p := range parts {
h := buscompose.Heard{Stream: p.Stream.BusName(), Emitter: p.Emitter, Restricted: p.Stream.Grant != nil, LastPerSubject: p.Stream.LastPerSubject}
for _, pattern := range p.Patterns {
h.Filters = append(h.Filters, buscompose.ModuleStreamSubject(p.Emitter, sdkbc.WirePattern(pattern)))
}
out = append(out, h)
}
// **Either form of a part's finding stream** (design 47 G §386, §529): a `*` emitter before an event reaches, beside each
// module's old stream, the stream a part asked for in `requires` by the template events (its `finding-events`) that carries
// the event, one durable consumer on it, heard on the binding's subject `mesh.bus.<module>.<local>.<event>`.
for _, c := range reading.Consumes {
emitter, event, named := strings.Cut(c, ".")
if !named || emitter != catalogue.OneName || event == "" || strings.ContainsAny(event, "*>") || defined == nil || !defined(event) {
continue
}
for _, module := range slices.Sorted(maps.Keys(declared)) {
for _, e := range declared[module].BusState {
if e.Storage != catalogue.StorageStream || e.Template != catalogue.TemplateEvents {
continue
}
h := buscompose.Heard{Stream: catalogue.BusName(module, e.Name), Emitter: module}
for _, ev := range e.Events {
// An event is said on its binding's subject with the machine last (§529): `<event>.<node>`.
if ev == event || ev == event+".*" {
h.Filters = append(h.Filters, "mesh.bus."+module+"."+e.Name+"."+event+".*")
}
}
if len(h.Filters) > 0 {
out = append(out, h)
touches = true
}
}
}
}
return out, touches, onEvents
}
// holdersAfterPass is the holders on record (each node's assignments, as the seat mesh-module-assigner holds them) as they
// stand after catalogue.SeatsToPassOn (novox/hq ADR 0660): a holder whose every assignment's manifest no longer claims
// its seat has left it, and the seat is the only claimant's. Each assignment is read with the manifest its node runs
// (Inputs.Ran: the digest its node-engine says it runs), else the trunk's, else the running build of a held module; one with none is unread and keeps what it
// holds. A pass is said as a gap and recorded nowhere (ADR 0660 §4). Pure.
func holdersAfterPass(in Inputs) ([]catalogue.Held, []Gap) {
var held []catalogue.Held
for _, node := range slices.Sorted(maps.Keys(in.Placed)) {
for _, x := range in.Placed[node].Holds {
if x.Scope != "" && x.Scope != catalogue.ScopeMesh {
continue
}
held = append(held, catalogue.Held{Claim: x.Claim, Scope: catalogue.ScopeMesh, Node: node, Module: x.Module, Site: x.Site, Kind: x.Kind})
}
}
var assignments []catalogue.Assignment
for _, node := range slices.Sorted(maps.Keys(in.Assigned)) {
for _, module := range in.Assigned[node] {
assignments = append(assignments, catalogue.Assignment{Node: node, Module: module})
}
}
heard := func(node, module string) (catalogue.Heard, bool) {
h, ok := in.Heard[node+"."+module]
return h, ok
}
passes, said := catalogue.SeatsToPassOn(held, assignments, heard, ranEntries{in})
var gaps []Gap
for _, p := range passes {
held = slices.DeleteFunc(held, func(h catalogue.Held) bool { return h.Kind == p.From.Kind && sameSeat(h.Claim, p.Seat.Name) })
p.To.Site = p.From.Site
held = append(held, p.To)
gaps = append(gaps, Gap{Input: "seat " + p.Seat.Name, Why: fmt.Sprintf(
"%s on %s is on record as its holder and its manifest no longer claims it: composed as held by %s on %s, not recorded (ADR 0660)",
p.From.Module, p.From.Node, p.To.Module, p.To.Node)})
}
for _, w := range said {
gaps = append(gaps, Gap{Input: "seat passes", Why: w})
}
return held, gaps
}
// ranEntries is the reader of entries SeatsToPassOn takes (design 47 G §459): the manifest of a fingerprint a node-engine
// said it runs, and a module's manifest where none was heard: the trunk's, else the running build of a held module.
type ranEntries struct{ in Inputs }
func (r ranEntries) ByFingerprint(fingerprint string) (catalogue.Manifest, bool) {
m, ok := r.in.RanByFingerprint[fingerprint]
return m, ok
}
func (r ranEntries) OnBranch(module, _ string) (catalogue.Manifest, bool) {
if m, ok := r.in.Manifests[module]; ok {
return m, true
}
m, ok := r.in.Held[module]
return m, ok
}
// heldWherePlaced is a module's holds on a node as the catalogue's assignments says them (ADR 0549 §1, §3). The assignments'
// holds are the holders on record, as catalogue.World's Holdings are, after the passes of holdersAfterPass: a seat held
// once for the mesh that some node's assignments holds is held only where assignments names it (this node's naming this
// module), so a module claiming it on two machines holds it on the one assignments names, not on both. A seat no
// assignments holds is held by derivation, by the module claiming it wherever it is placed, as it was before assignments:
// the seeded assignments hold only the seats the controller had on record (twelve on the control-node), and treating their
// holds as every holder left nine seats with no holder in the user list of 2026-10-27 09:21 (issue 1945). A seat of every
// node is held wherever the module runs.
func heldWherePlaced(holds []buscompose.Seat, holders []catalogue.Held, node, module string) []buscompose.Seat {
same := func(x catalogue.Held, h buscompose.Seat) bool { return sameSeat(x.Claim, h.Name) && x.Kind == h.Kind }
var out []buscompose.Seat
for _, h := range holds {
if h.Scope != "" && h.Scope != catalogue.ScopeMesh {
out = append(out, h)
continue
}
onRecord := slices.ContainsFunc(holders, func(x catalogue.Held) bool { return same(x, h) })
if !onRecord || slices.ContainsFunc(holders, func(x catalogue.Held) bool { return same(x, h) && x.Node == node && x.Module == module }) {
out = append(out, h)
}
}
return out
}
// sameSeat says two claims name one seat, under either of its names (catalogue.SeatNamed, ADR 0122).
func sameSeat(a, b string) bool {
if a == b {
return true
}
sa, oka := catalogue.SeatNamed(a)
sb, okb := catalogue.SeatNamed(b)
return oka && okb && sa.Name == sb.Name
}
// unionDeclared is what either declaration grants (issue 774): every list of b's entries a lacks appended to a's, a
// process of both merged by its name, a flag that adds a grant set where either sets it, and an account kept where
// either has one (NoAccount only where both say none). a's names and RunsAs stand. Pure.
func unionDeclared(a, b buscompose.Declared) buscompose.Declared {
out := a
unionFields(reflect.ValueOf(&out).Elem(), reflect.ValueOf(b))
out.NoAccount = a.NoAccount && b.NoAccount
procs := slices.Clone(a.Processes)
for _, bp := range b.Processes {
i := slices.IndexFunc(procs, func(ap buscompose.ProcessPrincipal) bool { return ap.Process == bp.Process })
if i < 0 {
procs = append(procs, bp)
continue
}
merged := procs[i]
unionFields(reflect.ValueOf(&merged).Elem(), reflect.ValueOf(bp))
procs[i] = merged
}
out.Processes = procs
return out
}
// unionFields appends to each slice field of dst the elements of src's it lacks, and sets each bool field src sets.
func unionFields(dst, src reflect.Value) {
for i := 0; i < dst.NumField(); i++ {
f, g := dst.Field(i), src.Field(i)
switch f.Kind() {
case reflect.Slice:
merged := reflect.AppendSlice(reflect.MakeSlice(f.Type(), 0, f.Len()+g.Len()), f)
for j := 0; j < g.Len(); j++ {
held := false
for k := 0; k < merged.Len() && !held; k++ {
held = reflect.DeepEqual(merged.Index(k).Interface(), g.Index(j).Interface())
}
if !held {
merged = reflect.Append(merged, g.Index(j))
}
}
f.Set(merged)
case reflect.Bool:
f.SetBool(f.Bool() || g.Bool())
}
}
}
// catchUpsOf are the catch-up tools a manifest's kept data name, each once (design 47 G §406, §631): the module's own tools
// the node-engine calls after a restore of that data, which it is granted to call on its own node (A-108).
func catchUpsOf(m catalogue.Manifest) []string {
var tools []string
for _, k := range catalogue.KeptDeclarations(m) {
if k.CatchUp != nil && k.CatchUp.Tool != "" && !slices.Contains(tools, k.CatchUp.Tool) {
tools = append(tools, k.CatchUp.Tool)
}
}
return tools
}
// findingEventsOf are the events of m's `finding-events` entry, an `events` template it requires (design 47 G §529; lead
// answer A-280), none where it asks for none.
func findingEventsOf(m catalogue.Manifest) []string {
for _, e := range m.BusState {
if e.Name == buscompose.FindingEventsLocal && e.Storage == catalogue.StorageStream && e.Template == catalogue.TemplateEvents {
return append([]string(nil), e.Events...)
}
}
return nil
}