package delivery import ( "encoding/json" "fmt" "sort" "time" "git.novox.be/novox/mesh-sdk/go/catalogue" ) // **Who holds which seat, and the builder's log** (novox/hq ADR 0571 clauses 1 and 4, issue 1955). // SeatHoldersState is the state the seat mesh-placement's holder writes of who holds which seat on which node (clause 4): // one key per seat held once for the mesh, ``, and one per node of a seat held on every node, `.`. const ( SeatHoldersState = "seat-holders" SeatHoldersBucket = AssignerModule + "_" + SeatHoldersState SeatHoldersRead = AssignerModule + "." + SeatHoldersState ) // SeatHolder is one module holding a seat on a node. type SeatHolder struct { Module string `json:"module"` Node string `json:"node"` } // SeatHolders is one key of seat-holders: the seat, its scope, the node for a node seat's key, and its holders // (several only for a replicated mesh seat), sorted. Outside says the claim names no seat of the set (catalogue.Seats): // a module registered before the set closed, or built outside the indexed repositories, keeps its claim listed, as the // controller's `seats` listed it under outside. type SeatHolders struct { V int `json:"v"` Seat string `json:"seat"` Scope string `json:"scope"` Node string `json:"node,omitempty"` Outside bool `json:"outside,omitempty"` Holders []SeatHolder `json:"holders"` } // SeatHoldersKey is a seat's key: `` for a seat held once for the mesh (node empty), `.` for a node // seat on one node. func SeatHoldersKey(seat, node string) (string, error) { if err := checkName("seat", seat); err != nil { return "", err } if node == "" { return seat, nil } if err := checkName("node", node); err != nil { return "", err } return seat + "." + node, nil } // SeatHoldersOf is the state composed from every node's placement (ADR 0567 ยง1): each holding by the seat it claims, // keyed as SeatHoldersKey says, with the module holding it and its node. A claim naming a seat's former name is keyed by // the seat's name (ADR 0122); one naming no seat is kept, marked Outside. Deterministic, so a writer writes only a key // that differs. func SeatHoldersOf(placements map[string]Placement) map[string]SeatHolders { out := map[string]SeatHolders{} nodes := make([]string, 0, len(placements)) for n := range placements { nodes = append(nodes, n) } sort.Strings(nodes) for _, n := range nodes { for _, h := range placements[n].Holds { node := "" if h.Scope == "node" { node = n } seat, known := catalogue.SeatNamed(h.Claim) name := h.Claim if known { name = seat.Name } key, err := SeatHoldersKey(name, node) if err != nil { continue } sh := out[key] sh.V, sh.Seat, sh.Scope, sh.Node, sh.Outside = Version, name, h.Scope, node, !known sh.Holders = append(sh.Holders, SeatHolder{Module: h.Module, Node: n}) out[key] = sh } } for k, sh := range out { sort.Slice(sh.Holders, func(i, j int) bool { return sh.Holders[i].Node+"/"+sh.Holders[i].Module < sh.Holders[j].Node+"/"+sh.Holders[j].Module }) out[k] = sh } return out } // HoldersOf reads who holds seat (on node, for a node seat) from the state's keys as a reader watched them: the node's // key first, then the seat's. None when nobody holds it. func HoldersOf(keys map[string][]byte, seat, node string) ([]SeatHolder, error) { for _, n := range []string{node, ""} { key, err := SeatHoldersKey(seat, n) if err != nil { if n == "" { return nil, err } continue } raw, ok := keys[key] if !ok { continue } var sh SeatHolders if err := json.Unmarshal(raw, &sh); err != nil { return nil, fmt.Errorf("delivery: the holders of %s are not readable: %w", key, err) } if sh.V != Version { return nil, fmt.Errorf("delivery: the holders of %s are of version %d, and this reads %d", key, sh.V, Version) } return sh.Holders, nil } return nil, nil } // The builder's log (clause 1; the lead and the operator, 2026-10-29): the builder's own module log (ADR 0297) `build-log`, // kept 30 days (max-age-days), one entry under the key `` when a build of a fingerprint starts and one when it // ends, so the newest entry of a key is the module's newest start or end. The builder's tools read it through the // runtime's log verbs; another module reads it by naming BuildLogRead in its manifest's `reads`: a bundle through the // runtime (stdio.Log(BuildLogRead)), a process on its own credential (process.Conn.ReadLog), each by newest per key, // from a time, or following new entries. The catalogue module asks the builder's tool BuildsSinceTool instead. const ( BuilderLog = "build-log" BuildLogRead = BuilderModule + "." + BuilderLog BuildStartedEvent = "build.started" BuildEndedEvent = "build.ended" // BuilderLogDays is how long the log keeps its entries. BuilderLogDays = 30 // BuildsSinceTool is the builder's tool answering every ended build since a time, in order (clause 9): its // argument `since` (RFC 3339), its answer BuildsSince. BuildsSinceTool = "builder_builds_since" ) // BuildLogKey is the key of a module's entries in the builder's log: the module's name, refused when a log key could not // hold it. func BuildLogKey(module string) (string, error) { if err := checkName("module", module); err != nil { return "", err } return module, nil } // BuildLogEntry is one entry of the builder's log: the module, the fingerprint, the source revision, where the module is // built from (its repository, its directory there, the commit and the ref), the build node and the time; at the end, // the outcome, the manifest built (its digest, read from the indexer's state `manifests` under ManifestsKey), what it was // built against (the bases' references, as the controller's built-before said them) and each artifact made // (BuiltArtifact, the done key's: its name, kind, digest and reference). The repository, directory and commit are what // the indexer's PathOf, the outside-built registrations and the assigner's running commits read; the manifest, against // and made what the catalogue module registers a build from (the lead, 2026-10-29). type BuildLogEntry struct { V int `json:"v"` Event string `json:"event"` Module string `json:"module"` FP string `json:"fp"` Source string `json:"source"` Repository string `json:"repository"` Directory string `json:"directory"` Commit string `json:"commit"` Ref string `json:"ref,omitempty"` Node string `json:"node"` At time.Time `json:"at"` Outcome string `json:"outcome,omitempty"` Manifest string `json:"manifest,omitempty"` Against []string `json:"against,omitempty"` Made []BuiltArtifact `json:"made,omitempty"` } // BuildsSince is the answer of BuildsSinceTool: every ended build since a time, oldest first, and whether the log held // more than one answer carries (then ask again from the last one's time). type BuildsSince struct { Since time.Time `json:"since"` Builds []BuildLogEntry `json:"builds"` More bool `json:"more,omitempty"` } // NewestPerModule is each module's newest entry of entries (one kind, read from the last per subject or whole), by // module: the reader's helper for the five newest and the running commits. func NewestPerModule(entries []BuildLogEntry) map[string]BuildLogEntry { out := map[string]BuildLogEntry{} for _, e := range entries { if have, ok := out[e.Module]; !ok || e.At.After(have.At) { out[e.Module] = e } } return out } // EndedSince is, of entries, the ended builds at or after since, oldest first: the reader's helper for history and for // BuildsSinceTool's answer. func EndedSince(entries []BuildLogEntry, since time.Time) []BuildLogEntry { var out []BuildLogEntry for _, e := range entries { if e.Event == BuildEndedEvent && !e.At.Before(since) { out = append(out, e) } } sort.SliceStable(out, func(i, j int) bool { return out[i].At.Before(out[j].At) }) return out } // EncodeBuildLogEntry is e as JSON at Version; it refuses an entry a reader could not place. func EncodeBuildLogEntry(e BuildLogEntry) ([]byte, error) { e.V = Version switch { case e.Event != BuildStartedEvent && e.Event != BuildEndedEvent: return nil, fmt.Errorf("delivery: a build log entry is %s or %s, not %q", BuildStartedEvent, BuildEndedEvent, e.Event) case e.Module == "" || e.FP == "" || e.Node == "" || e.At.IsZero() || e.Repository == "" || e.Commit == "": return nil, fmt.Errorf("delivery: a build log entry names its module, fingerprint, repository, commit, node and time") case e.Event == BuildEndedEvent && e.Outcome == "": return nil, fmt.Errorf("delivery: a build's end names its outcome") case e.Event == BuildStartedEvent && (e.Outcome != "" || len(e.Made) > 0 || e.Manifest != "" || len(e.Against) > 0): return nil, fmt.Errorf("delivery: a build's start names no outcome, manifest, base or artifact") } return json.Marshal(e) } // DecodeBuildLogEntry reads one entry. func DecodeBuildLogEntry(data []byte) (BuildLogEntry, error) { var e BuildLogEntry if err := json.Unmarshal(data, &e); err != nil { return BuildLogEntry{}, fmt.Errorf("delivery: a build log entry is not readable: %w", err) } if e.V != Version { return BuildLogEntry{}, fmt.Errorf("delivery: a build log entry of version %d, and this reads %d", e.V, Version) } return e, nil } // BuildDuration is a build's duration, its end less its start (clause 1): false unless both are the same module's // build of the same fingerprint, the start first. func BuildDuration(start, end BuildLogEntry) (time.Duration, bool) { if start.Event != BuildStartedEvent || end.Event != BuildEndedEvent || start.Module != end.Module || start.FP != end.FP || end.At.Before(start.At) { return 0, false } return end.At.Sub(start.At), true } // **Which modules exist and which machines the mesh has, from the bus** (novox/hq ADR 0571 clauses 4, 5; the lead for a6, // 2026-10-29): the tool runner reads the modules from the indexer's state `modules` and the machines from the placement // seat's keys, and no longer asks the controller's `modules` or `nodes`. // ModuleNames is every module the indexer holds, sorted, from its state `modules` as a reader holds its keys (key to // value; a deleted key absent). A key that is not a module's is refused, naming it. func ModuleNames(keys map[string][]byte) ([]string, error) { out := make([]string, 0, len(keys)) for k := range keys { module, err := ParseModulesKey(k) if err != nil { return nil, err } out = append(out, module) } sort.Strings(out) return out, nil } // MachineNames is every machine the placement seat places modules on, sorted, from its state `placement` as a reader // holds its keys: each with a node key `.node` or, in the form before, `placement.` (placement_keys.go). // Module keys and keys of the state's other kinds (a proposal's) are passed over. func MachineNames(keys map[string][]byte) []string { seen := map[string]bool{} out := []string{} for k := range keys { if node, _, kind, err := ParsePlacementKey(k); err == nil && kind != PlacementModuleKind && !seen[node] { seen[node] = true out = append(out, node) } } sort.Strings(out) return out }