mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer delivery to the same trunk took over its walk
A seat's verb names the shell commands it is the mesh's way to do, and a module says it for its own tools in its manifest; the tools verb and module list --json carry both, for the mesh MCP server's search, the agent's instructions and the guard on its shell.
112 lines
3.6 KiB
Go
112 lines
3.6 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// What a move does to a module's containers (novox/hq ADR 0245).
|
|
//
|
|
// A container whose declaration changes is recreated, whatever changed in it: an image, an environment
|
|
// variable, a health check adopted. A module whose containers are recreated in one send is down while
|
|
// they start again — on 2026-10-07 a catalogue change that only adopted the images' own health checks
|
|
// recreated nine of mail's containers at once, and the operator's phone could not reach the mail. So a
|
|
// send says, per module, how many of its containers it recreates and whether any image changed, before
|
|
// it is sent and in the plan that sent it.
|
|
|
|
// Recreation is what moving a module from one build's manifest to another's does to its containers.
|
|
type Recreation struct {
|
|
// Containers is how many containers the new build declares.
|
|
Containers int
|
|
// Recreated are the ids of the containers whose declaration differs — changed, added or gone —
|
|
// sorted.
|
|
Recreated []string
|
|
// Images are the ids among them whose image changed (or that are added or gone).
|
|
Images []string
|
|
}
|
|
|
|
// Recreates compares two builds of a module, container by container, by resource id.
|
|
func Recreates(from, to Manifest) Recreation {
|
|
before := containersByID(from)
|
|
after := containersByID(to)
|
|
var r Recreation
|
|
r.Containers = len(after)
|
|
for id, now := range after {
|
|
was, held := before[id]
|
|
switch {
|
|
case !held:
|
|
r.Recreated = append(r.Recreated, id)
|
|
r.Images = append(r.Images, id)
|
|
case !sameDeclaration(was, now):
|
|
r.Recreated = append(r.Recreated, id)
|
|
if !sameDeclaration(was["image"], now["image"]) {
|
|
r.Images = append(r.Images, id)
|
|
}
|
|
}
|
|
}
|
|
for id := range before {
|
|
if _, kept := after[id]; !kept {
|
|
r.Recreated = append(r.Recreated, id)
|
|
r.Images = append(r.Images, id)
|
|
}
|
|
}
|
|
sort.Strings(r.Recreated)
|
|
sort.Strings(r.Images)
|
|
return r
|
|
}
|
|
|
|
// SpecOnly is whether the move recreates containers without any new image: a change to how they are
|
|
// declared only — a health check adopted, a variable — which a person may well not expect to interrupt.
|
|
func (r Recreation) SpecOnly() bool { return len(r.Recreated) > 0 && len(r.Images) == 0 }
|
|
|
|
// Say is the recreation in the mesh's words, for a module: empty when the move recreates nothing.
|
|
func (r Recreation) Say(module string) string {
|
|
if len(r.Recreated) == 0 {
|
|
return ""
|
|
}
|
|
what := "with a new image"
|
|
switch {
|
|
case r.SpecOnly():
|
|
what = "no new image, only their declaration"
|
|
case len(r.Images) < len(r.Recreated):
|
|
what = fmt.Sprintf("%d with a new image", len(r.Images))
|
|
}
|
|
whole := ""
|
|
if len(r.Recreated) > 1 && len(r.Recreated) >= r.Containers {
|
|
whole = ", every one at once: its service is interrupted until they are up again"
|
|
} else if len(r.Recreated) > 1 {
|
|
whole = ", at once: what they serve is interrupted until they are up again"
|
|
}
|
|
return fmt.Sprintf("recreates %d of %s's %d container(s) (%s: %s)%s", len(r.Recreated), module, r.Containers,
|
|
what, strings.Join(r.Recreated, ", "), whole)
|
|
}
|
|
|
|
func containersByID(m Manifest) map[string]map[string]any {
|
|
out := map[string]map[string]any{}
|
|
for _, r := range m.Resources {
|
|
if t, _ := r["type"].(string); t != "container" {
|
|
continue
|
|
}
|
|
id, _ := r["id"].(string)
|
|
out[id] = r
|
|
}
|
|
return out
|
|
}
|
|
|
|
// sameDeclaration compares two declarations as JSON, so a number read as an int and one read as a
|
|
// float are one value.
|
|
func sameDeclaration(a, b any) bool {
|
|
ra, errA := json.Marshal(a)
|
|
rb, errB := json.Marshal(b)
|
|
if errA != nil || errB != nil {
|
|
return reflect.DeepEqual(a, b)
|
|
}
|
|
var na, nb any
|
|
_ = json.Unmarshal(ra, &na)
|
|
_ = json.Unmarshal(rb, &nb)
|
|
return reflect.DeepEqual(na, nb)
|
|
}
|