Files
mesh-sdk/go/delivery/enginepackages.go
T
jschoubben 0ef7c0caa4 Work out a node's declared packages and their difference with the mirror in the SDK, so each engine writes its own key (ADR 0565 clause 6, issue 1953)
The engine composes its node's wants itself, so the derivation the controller keeps
privately moves beside the layout: DeclaredPackagesOf gives the mirror's bytes from the
compositions, DiffDeclaredPackages what the node seat's verb packages compares until the
lease write, and EnginePackagesRead names the state as readers declare it (mesh-host.packages,
as mesh-host.data). Each engine may read the controller mirror's key of its own node alone.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 16:02:06 +01:00

138 lines
4.6 KiB
Go

package delivery
import (
"encoding/json"
"fmt"
"slices"
"sort"
"git.novox.be/novox/mesh-sdk/go/declaration"
)
// **A node's declared packages, worked out by its engine** (novox/hq ADR 0565 §1, §6; issue 1953): the derivation and
// the comparison of the node-engine's state `packages` (EnginePackagesState, layout DeclaredPackages), so the engine
// that writes the key, the node seat's verb that compares it with the controller's mirror, and any reader work them out
// one way. The controller's own copy of the derivation (mesh-controller internal/inventory DeclaredPackagesValue) is
// not changed (ADR 0547 §1) and ends with its lease.
// EnginePackagesRead is the engine's state `packages` as a reader names it in `reads`: the node-engine's manifest is
// mesh-host's, so its bucket is mesh-host_packages, read as disk-load reads mesh-host.data (EngineDataRead).
const EnginePackagesRead = "mesh-host." + EnginePackagesState
// DeclaredPackagesOf is a node's key of `packages` from the compositions its engine composed for the node's wants:
// each resource of type package, by its name and whether it is declared absent, with the modules that declare it so,
// in the mirror's layout and order (names sorted, absent after present, modules sorted). A resource's module is the
// one it names, else its composition's, else the guess from its id (declaration.ModuleGuess); a package of no module
// lists none.
func DeclaredPackagesOf(node string, compositions []Composed) (DeclaredPackages, error) {
by := map[packageKey]map[string]bool{}
for _, c := range compositions {
for _, raw := range c.Resources {
var r struct {
ID string `json:"id"`
Type string `json:"type"`
Package string `json:"package"`
Absent bool `json:"absent"`
Module string `json:"module"`
}
if err := json.Unmarshal(raw, &r); err != nil {
return DeclaredPackages{}, fmt.Errorf("delivery: a resource of %s's composition %q is not readable: %w", node, c.Module, err)
}
if r.Type != "package" || r.Package == "" {
continue
}
k := packageKey{r.Package, r.Absent}
if by[k] == nil {
by[k] = map[string]bool{}
}
module := r.Module
if module == "" {
module = c.Module
}
if module == "" {
module, _ = declaration.ModuleGuess(r.ID)
}
if module != "" {
by[k][module] = true
}
}
}
v := DeclaredPackages{Node: node, Packages: []PackageDeclared{}}
for _, k := range sortedPackageKeys(by) {
modules := make([]string, 0, len(by[k]))
for m := range by[k] {
modules = append(modules, m)
}
sort.Strings(modules)
v.Packages = append(v.Packages, PackageDeclared{Package: k.name, Modules: modules, Absent: k.absent})
}
return v, nil
}
// PackageDifference is one package two declarations of a node hold differently: the modules each side declares it
// with (present, or absent when Absent), none on a side that does not hold it.
type PackageDifference struct {
Package string `json:"package"`
Absent bool `json:"absent"`
Engine []string `json:"engine"`
Mirror []string `json:"mirror"`
}
// DiffDeclaredPackages is every package the engine's key and the controller's mirror hold differently for one node, in
// the layout's order: what the node seat's verb packages answers until the lease write (ADR 0565 §6). Empty when they
// agree.
func DiffDeclaredPackages(engine, mirror DeclaredPackages) []PackageDifference {
index := func(d DeclaredPackages) map[packageKey][]string {
out := map[packageKey][]string{}
for _, p := range d.Packages {
out[packageKey{p.Package, p.Absent}] = append([]string{}, p.Modules...)
}
return out
}
e, m := index(engine), index(mirror)
union := map[packageKey]bool{}
for k := range e {
union[k] = true
}
for k := range m {
union[k] = true
}
out := []PackageDifference{}
for _, k := range sortedPackageKeys(union) {
em, inE := e[k]
mm, inM := m[k]
if inE && inM && slices.Equal(em, mm) {
continue
}
if em == nil {
em = []string{}
}
if mm == nil {
mm = []string{}
}
out = append(out, PackageDifference{Package: k.name, Absent: k.absent, Engine: em, Mirror: mm})
}
return out
}
// packageKey is a package as the layout tells it apart: its name, and whether it is declared absent.
type packageKey struct {
name string
absent bool
}
// sortedPackageKeys is the keys of m by name, absent after present.
func sortedPackageKeys[V any](m map[packageKey]V) []packageKey {
keys := make([]packageKey, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].name != keys[j].name {
return keys[i].name < keys[j].name
}
return !keys[i].absent && keys[j].absent
})
return keys
}